Migrate all device config tests to integration.

This means that configuration backend tests no longer use stubs and
(mostly) avoid internal dependencies in the tests. Instead of stubs and
mock-fs, the tests use [testfs](https://github.com/balena-io-modules/mocha-pod#working-with-the-filesystem)
which allows working with a real filesystem and ensuring everything is
re-set between tests.

This is the last change needed in order to be able to merge #1971. Here is the list of changes

- [x] Migrate splash image backend tests
- [x] Migrate extlinux backend tests
- [x] Migrate config.txt backend tests
- [x] Migrate extra-uenv config tests
- [x] Migrate odmdata config tests
- [x] Migrate config utils tests
- [x] Migrate device-config tests

Change-type: patch
This commit is contained in:
pipex 2022-10-21 10:59:51 -03:00 committed by Felipe Lalanne
parent 83c856ae4b
commit 827f892c13
15 changed files with 1190 additions and 1074 deletions

View File

@ -7,12 +7,13 @@ testfs:
# in the `testfs` configuration.
filesystem:
/mnt/root:
/mnt/boot/config.json:
from: test/data/testconfig.json
/mnt/boot/config.txt:
from: test/data/mnt/boot/config.txt
/mnt/boot/device-type.json:
from: test/data/mnt/boot/device-type.json
/mnt/boot:
config.json:
from: test/data/testconfig.json
config.txt:
from: test/data/mnt/boot/config.txt
device-type.json:
from: test/data/mnt/boot/device-type.json
/etc/os-release:
from: test/data/etc/os-release
# The `keep` list defines files that already exist in the
@ -24,3 +25,4 @@ testfs:
- /data/database.sqlite
- /data/apps.json.preloaded
- /mnt/root/tmp/balena-supervisor/**/*.lock
- /mnt/root/mnt/boot/splash/*.png

18
package-lock.json generated
View File

@ -81,7 +81,7 @@
"lodash": "^4.17.21",
"memoizee": "^0.4.14",
"mocha": "^8.3.2",
"mocha-pod": "^0.8.0",
"mocha-pod": "^0.9.0",
"mock-fs": "^4.14.0",
"morgan": "^1.10.0",
"network-checker": "^0.1.1",
@ -8247,9 +8247,9 @@
}
},
"node_modules/mocha-pod": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/mocha-pod/-/mocha-pod-0.8.0.tgz",
"integrity": "sha512-0jhPpQMWCduiEFFFPrWWdKonwmyC6TFwgZEo7G/JhpIsmmfQm2cZGpoJ2HfUCXT1bcOuinSUPI8cweG+1fbbhw==",
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/mocha-pod/-/mocha-pod-0.9.0.tgz",
"integrity": "sha512-km2XTNEbxjxrbq3P5XoGm1kHBszQJZXEg0OXMkN/ZTG0g5mKWX6RQPNlfp6m9lRgRvxwdAVVWIPNAo+q/CsbkQ==",
"dev": true,
"dependencies": {
"@balena/compose": "^2.1.0",
@ -8259,7 +8259,7 @@
"dockerode": "^3.3.2",
"fast-glob": "^3.2.11",
"js-yaml": "^4.1.0",
"nanoid": "^3.3.4",
"nanoid": "3.3.4",
"tar-fs": "^2.1.1"
},
"engines": {
@ -20183,9 +20183,9 @@
}
},
"mocha-pod": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/mocha-pod/-/mocha-pod-0.8.0.tgz",
"integrity": "sha512-0jhPpQMWCduiEFFFPrWWdKonwmyC6TFwgZEo7G/JhpIsmmfQm2cZGpoJ2HfUCXT1bcOuinSUPI8cweG+1fbbhw==",
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/mocha-pod/-/mocha-pod-0.9.0.tgz",
"integrity": "sha512-km2XTNEbxjxrbq3P5XoGm1kHBszQJZXEg0OXMkN/ZTG0g5mKWX6RQPNlfp6m9lRgRvxwdAVVWIPNAo+q/CsbkQ==",
"dev": true,
"requires": {
"@balena/compose": "^2.1.0",
@ -20195,7 +20195,7 @@
"dockerode": "^3.3.2",
"fast-glob": "^3.2.11",
"js-yaml": "^4.1.0",
"nanoid": "^3.3.4",
"nanoid": "3.3.4",
"tar-fs": "^2.1.1"
},
"dependencies": {

View File

@ -107,7 +107,7 @@
"lodash": "^4.17.21",
"memoizee": "^0.4.14",
"mocha": "^8.3.2",
"mocha-pod": "^0.8.0",
"mocha-pod": "^0.9.0",
"mock-fs": "^4.14.0",
"morgan": "^1.10.0",
"network-checker": "^0.1.1",

View File

@ -0,0 +1,78 @@
import { testfs } from 'mocha-pod';
import { stripIndent } from 'common-tags';
import { expect } from 'chai';
import * as hostUtils from '~/lib/host-utils';
import { ConfigTxt } from '~/src/config/backends/config-txt';
describe('config/config-txt', () => {
it('correctly parses a config.txt file', async () => {
const tfs = await testfs({
[hostUtils.pathOnBoot('config.txt')]: stripIndent`
initramfs initramf.gz 0x00800000
dtparam=i2c=on
dtparam=audio=on
dtoverlay=ads7846
enable_uart=1
avoid_warnings=1
gpu_mem=16
hdmi_force_hotplug:1=1
dtoverlay=lirc-rpi,gpio_out_pin=17,gpio_in_pin=13`,
}).enable();
const configTxt = new ConfigTxt();
// Will try to parse /test/data/mnt/boot/config.txt
await expect(configTxt.getBootConfig()).to.eventually.deep.equal({
dtparam: ['i2c=on', 'audio=on'],
dtoverlay: ['ads7846', 'lirc-rpi,gpio_out_pin=17,gpio_in_pin=13'],
enable_uart: '1',
avoid_warnings: '1',
gpu_mem: '16',
initramfs: 'initramf.gz 0x00800000',
// This syntax is supported by the backend but not the cloud side
'hdmi_force_hotplug:1': '1',
});
await tfs.restore();
});
it('ensures required fields are written to config.txt', async () => {
const tfs = await testfs({
[hostUtils.pathOnBoot('config.txt')]: stripIndent`
enable_uart=1
dtparam=i2c_arm=on
dtparam=spi=on
disable_splash=1
dtparam=audio=on
gpu_mem=16
`,
}).enable();
const configTxt = new ConfigTxt();
await configTxt.setBootConfig({
dtparam: ['i2c=on', 'audio=on'],
dtoverlay: ['ads7846', 'lirc-rpi,gpio_out_pin=17,gpio_in_pin=13'],
enable_uart: '1',
avoid_warnings: '1',
gpu_mem: '256',
initramfs: 'initramf.gz 0x00800000',
'hdmi_force_hotplug:1': '1',
});
// Will try to parse /test/data/mnt/boot/config.txt
await expect(configTxt.getBootConfig()).to.eventually.deep.equal({
dtparam: ['i2c=on', 'audio=on'],
dtoverlay: ['ads7846', 'lirc-rpi,gpio_out_pin=17,gpio_in_pin=13'],
enable_uart: '1',
avoid_warnings: '1',
gpu_mem: '256',
initramfs: 'initramf.gz 0x00800000',
'hdmi_force_hotplug:1': '1',
});
await tfs.restore();
});
});

View File

@ -1,195 +1,122 @@
import { promises as fs } from 'fs';
import { stripIndent } from 'common-tags';
import { SinonStub, stub } from 'sinon';
import { expect } from 'chai';
import { testfs } from 'mocha-pod';
import * as fsUtils from '~/lib/fs-utils';
import * as hostUtils from '~/lib/host-utils';
import { Extlinux } from '~/src/config/backends/extlinux';
describe('Extlinux Configuration', () => {
const backend = new Extlinux();
describe('config/extlinux', () => {
it('should correctly parse an extlinux.conf file', async () => {
const tfs = await testfs({
[hostUtils.pathOnBoot('extlinux/extlinux.conf')]: stripIndent`
DEFAULT primary
# CommentExtlinux files
it('should parse a extlinux.conf file', () => {
const text = stripIndent`\
DEFAULT primary
# CommentExtlinux files
TIMEOUT 30
MENU TITLE Boot Options
LABEL primary
MENU LABEL primary Image
LINUX /Image
FDT /boot/mycustomdtb.dtb
APPEND \${cbootargs} \${resin_kernel_root} ro rootwait isolcpus=3\
`,
}).enable();
const extLinux = new Extlinux();
TIMEOUT 30
MENU TITLE Boot Options
LABEL primary
MENU LABEL primary Image
LINUX /Image
FDT /boot/mycustomdtb.dtb
APPEND \${cbootargs} \${resin_kernel_root} ro rootwait\
`;
await expect(extLinux.getBootConfig()).to.eventually.deep.equal({
fdt: '/boot/mycustomdtb.dtb',
isolcpus: '3',
});
// @ts-expect-error accessing private method
const parsed = Extlinux.parseExtlinuxFile(text);
expect(parsed.globals).to.have.property('DEFAULT').that.equals('primary');
expect(parsed.globals).to.have.property('TIMEOUT').that.equals('30');
expect(parsed.globals)
.to.have.property('MENU TITLE')
.that.equals('Boot Options');
expect(parsed.labels).to.have.property('primary');
const { primary } = parsed.labels;
expect(primary).to.have.property('MENU LABEL').that.equals('primary Image');
expect(primary).to.have.property('LINUX').that.equals('/Image');
expect(primary)
.to.have.property('FDT')
.that.equals('/boot/mycustomdtb.dtb');
expect(primary)
.to.have.property('APPEND')
.that.equals('${cbootargs} ${resin_kernel_root} ro rootwait');
await tfs.restore();
});
it('should parse multiple service entries', () => {
const text = stripIndent`\
DEFAULT primary
# Comment
it('should parse multiple service entries', async () => {
const tfs = await testfs({
[hostUtils.pathOnBoot('extlinux/extlinux.conf')]: stripIndent`
DEFAULT primary
# Comment
TIMEOUT 30
MENU TITLE Boot Options
LABEL primary
LINUX test1
FDT /boot/mycustomdtb.dtb
APPEND test2
LABEL secondary
LINUX test3
FDT /boot/mycustomdtb.dtb
APPEND test4\
`;
TIMEOUT 30
MENU TITLE Boot Options
LABEL primary
LINUX test1
FDT /boot/mycustomdtb.dtb
APPEND test2
LABEL secondary
LINUX test3
FDT /boot/mycustomdtb.dtb
APPEND test4\
`,
}).enable();
const extLinux = new Extlinux();
// @ts-expect-error accessing private method
const parsed = Extlinux.parseExtlinuxFile(text);
expect(parsed.labels).to.have.property('primary').that.deep.equals({
LINUX: 'test1',
FDT: '/boot/mycustomdtb.dtb',
APPEND: 'test2',
await expect(extLinux.getBootConfig()).to.eventually.deep.equal({
fdt: '/boot/mycustomdtb.dtb',
});
expect(parsed.labels).to.have.property('secondary').that.deep.equals({
LINUX: 'test3',
FDT: '/boot/mycustomdtb.dtb',
APPEND: 'test4',
});
});
it('should parse configuration options from an extlinux.conf file', async () => {
let text = stripIndent`\
DEFAULT primary
# Comment
TIMEOUT 30
MENU TITLE Boot Options
LABEL primary
MENU LABEL primary Image
LINUX /Image
FDT /boot/mycustomdtb.dtb
APPEND \${cbootargs} \${resin_kernel_root} ro rootwait isolcpus=3\
`;
let readFileStub = stub(fs, 'readFile').resolves(text);
let parsed = backend.getBootConfig();
await expect(parsed)
.to.eventually.have.property('isolcpus')
.that.equals('3');
await expect(parsed)
.to.eventually.have.property('fdt')
.that.equals('/boot/mycustomdtb.dtb');
readFileStub.restore();
text = stripIndent`\
DEFAULT primary
# Comment
TIMEOUT 30
MENU TITLE Boot Options
LABEL primary
MENU LABEL primary Image
LINUX /Image
FDT /boot/mycustomdtb.dtb
APPEND \${cbootargs} \${resin_kernel_root} ro rootwait isolcpus=3,4,5\
`;
readFileStub = stub(fs, 'readFile').resolves(text);
parsed = backend.getBootConfig();
readFileStub.restore();
await expect(parsed)
.to.eventually.have.property('isolcpus')
.that.equals('3,4,5');
await tfs.restore();
});
it('only matches supported devices', async () => {
const extLinux = new Extlinux();
for (const { deviceType, metaRelease, supported } of MATCH_TESTS) {
await expect(
backend.matches(deviceType, metaRelease),
extLinux.matches(deviceType, metaRelease),
).to.eventually.equal(supported);
}
});
it('errors when cannot find extlinux.conf', async () => {
// The file does not exist before the test
await expect(fs.access(hostUtils.pathOnBoot('extlinux/extlinux.conf'))).to
.be.rejected;
const extLinux = new Extlinux();
// Stub readFile to reject much like if the file didn't exist
stub(fs, 'readFile').rejects();
await expect(backend.getBootConfig()).to.eventually.be.rejectedWith(
await expect(extLinux.getBootConfig()).to.eventually.be.rejectedWith(
'Could not find extlinux file. Device is possibly bricked',
);
// Restore stub
(fs.readFile as SinonStub).restore();
});
it('throws error for malformed extlinux.conf', async () => {
for (const badConfig of MALFORMED_CONFIGS) {
// Stub bad config
stub(fs, 'readFile').resolves(badConfig.contents);
const tfs = await testfs({
[hostUtils.pathOnBoot('extlinux/extlinux.conf')]: badConfig,
}).enable();
const extLinux = new Extlinux();
// Expect correct rejection from the given bad config
try {
await backend.getBootConfig();
} catch (e: any) {
expect(e.message).to.equal(badConfig.reason);
}
// Restore stub
(fs.readFile as SinonStub).restore();
await expect(extLinux.getBootConfig()).to.be.rejectedWith(
badConfig.reason,
);
await tfs.restore();
}
});
it('parses supported config values from bootConfigPath', async () => {
// Will try to parse /test/data/mnt/boot/extlinux/extlinux.conf
await expect(backend.getBootConfig()).to.eventually.deep.equal({}); // None of the values are supported so returns empty
// Stub readFile to return a config that has supported values
stub(fs, 'readFile').resolves(stripIndent`
DEFAULT primary
TIMEOUT 30
MENU TITLE Boot Options
LABEL primary
MENU LABEL primary Image
LINUX /Image
FDT /boot/mycustomdtb.dtb
APPEND ro rootwait isolcpus=0,4
`);
await expect(backend.getBootConfig()).to.eventually.deep.equal({
isolcpus: '0,4',
fdt: '/boot/mycustomdtb.dtb',
});
// Restore stub
(fs.readFile as SinonStub).restore();
});
it('sets new config values', async () => {
stub(fsUtils, 'writeAndSyncFile').resolves();
const tfs = await testfs({
[hostUtils.pathOnBoot('extlinux/extlinux.conf')]: stripIndent`
DEFAULT primary
TIMEOUT 30
MENU TITLE Boot Options
LABEL primary
MENU LABEL primary Image
LINUX /Image
APPEND \${cbootargs} \${resin_kernel_root} ro rootwait\
`,
}).enable();
await backend.setBootConfig({
const extLinux = new Extlinux();
await extLinux.setBootConfig({
fdt: '/boot/mycustomdtb.dtb',
isolcpus: '2',
});
expect(fsUtils.writeAndSyncFile).to.be.calledWith(
'test/data/mnt/boot/extlinux/extlinux.conf',
await expect(
fs.readFile(hostUtils.pathOnBoot('extlinux/extlinux.conf'), 'utf8'),
).to.eventually.equal(
stripIndent`
DEFAULT primary
TIMEOUT 30
@ -202,11 +129,11 @@ describe('Extlinux Configuration', () => {
` + '\n', // add newline because stripIndent trims last newline
);
// Restore stubs
(fsUtils.writeAndSyncFile as SinonStub).restore();
await tfs.restore();
});
it('only allows supported configuration options', () => {
const extLinux = new Extlinux();
[
{ configName: 'isolcpus', supported: true },
{ configName: 'fdt', supported: true },
@ -214,26 +141,28 @@ describe('Extlinux Configuration', () => {
{ configName: 'ro', supported: false }, // not allowed to configure
{ configName: 'rootwait', supported: false }, // not allowed to configure
].forEach(({ configName, supported }) =>
expect(backend.isSupportedConfig(configName)).to.equal(supported),
expect(extLinux.isSupportedConfig(configName)).to.equal(supported),
);
});
it('correctly detects boot config variables', () => {
const extLinux = new Extlinux();
[
{ config: 'HOST_EXTLINUX_isolcpus', valid: true },
{ config: 'HOST_EXTLINUX_fdt', valid: true },
{ config: 'HOST_EXTLINUX_rootwait', valid: true },
{ config: 'HOST_EXTLINUX_5', valid: true },
// TO-DO: { config: 'HOST_EXTLINUX', valid: false },
// TO-DO: { config: 'HOST_EXTLINUX_', valid: false },
// TODO: { config: 'HOST_EXTLINUX', valid: false },
// TODO: { config: 'HOST_EXTLINUX_', valid: false },
{ config: 'DEVICE_EXTLINUX_isolcpus', valid: false },
{ config: 'isolcpus', valid: false },
].forEach(({ config, valid }) =>
expect(backend.isBootConfigVar(config)).to.equal(valid),
expect(extLinux.isBootConfigVar(config)).to.equal(valid),
);
});
it('converts variable to backend formatted name', () => {
const extLinux = new Extlinux();
[
{ input: 'HOST_EXTLINUX_isolcpus', output: 'isolcpus' },
{ input: 'HOST_EXTLINUX_fdt', output: 'fdt' },
@ -243,20 +172,22 @@ describe('Extlinux Configuration', () => {
{ input: 'HOST_EXTLINUX_ ', output: ' ' },
{ input: 'ROOT_EXTLINUX_isolcpus', output: 'ROOT_EXTLINUX_isolcpus' },
].forEach(({ input, output }) =>
expect(backend.processConfigVarName(input)).to.equal(output),
expect(extLinux.processConfigVarName(input)).to.equal(output),
);
});
it('normalizes variable value', () => {
const extLinux = new Extlinux();
[{ input: { key: 'key', value: 'value' }, output: 'value' }].forEach(
({ input, output }) =>
expect(backend.processConfigVarValue(input.key, input.value)).to.equal(
expect(extLinux.processConfigVarValue(input.key, input.value)).to.equal(
output,
),
);
});
it('returns the environment name for config variable', () => {
const extLinux = new Extlinux();
[
{ input: 'isolcpus', output: 'HOST_EXTLINUX_isolcpus' },
{ input: 'fdt', output: 'HOST_EXTLINUX_fdt' },
@ -264,7 +195,7 @@ describe('Extlinux Configuration', () => {
{ input: '', output: 'HOST_EXTLINUX_' },
{ input: '5', output: 'HOST_EXTLINUX_5' },
].forEach(({ input, output }) =>
expect(backend.createConfigVarName(input)).to.equal(output),
expect(extLinux.createConfigVarName(input)).to.equal(output),
);
});
});

View File

@ -1,23 +1,14 @@
import { promises as fs } from 'fs';
import { stripIndent } from 'common-tags';
import { SinonStub, spy, stub } from 'sinon';
import { expect } from 'chai';
import { testfs } from 'mocha-pod';
import { promises as fs } from 'fs';
import * as fsUtils from '~/lib/fs-utils';
import Log from '~/lib/supervisor-console';
import * as hostUtils from '~/lib/host-utils';
import log from '~/lib/supervisor-console';
import { ExtraUEnv } from '~/src/config/backends/extra-uEnv';
describe('extra_uEnv Configuration', () => {
describe('config/extra-uEnv', () => {
const backend = new ExtraUEnv();
let readFileStub: SinonStub;
beforeEach(() => {
readFileStub = stub(fs, 'readFile');
});
afterEach(() => {
readFileStub.restore();
});
it('should parse extra_uEnv string', () => {
const fileContents = stripIndent`\
@ -35,89 +26,112 @@ describe('extra_uEnv Configuration', () => {
});
it('should only parse supported configuration options from bootConfigPath', async () => {
readFileStub.resolves(stripIndent`\
custom_fdt_file=mycustom.dtb
extra_os_cmdline=isolcpus=3,4
`);
let tfs = await testfs({
[hostUtils.pathOnBoot(`extra_uEnv.txt`)]: stripIndent`
custom_fdt_file=mycustom.dtb
extra_os_cmdline=isolcpus=3,4
`,
}).enable();
await expect(backend.getBootConfig()).to.eventually.deep.equal({
fdt: 'mycustom.dtb',
isolcpus: '3,4',
});
await tfs.restore();
// Add other options that will get filtered out because they aren't supported
readFileStub.resolves(stripIndent`\
custom_fdt_file=mycustom.dtb
extra_os_cmdline=isolcpus=3,4 console=tty0 splash
`);
tfs = await testfs({
[hostUtils.pathOnBoot(`extra_uEnv.txt`)]: stripIndent`
custom_fdt_file=mycustom.dtb
extra_os_cmdline=isolcpus=3,4 console=tty0 splash
`,
}).enable();
await expect(backend.getBootConfig()).to.eventually.deep.equal({
fdt: 'mycustom.dtb',
isolcpus: '3,4',
});
// Stub with no supported values
readFileStub.resolves(stripIndent`\
fdt=something_else
isolcpus
123.12=5
`);
await tfs.restore();
// Configuration with no supported values
tfs = await testfs({
[hostUtils.pathOnBoot(`extra_uEnv.txt`)]: stripIndent`
fdt=something_else
isolcpus
123.12=5
`,
}).enable();
await expect(backend.getBootConfig()).to.eventually.deep.equal({});
await tfs.restore();
});
it('only matches supported devices', async () => {
const existsStub = stub(fsUtils, 'exists');
// The file exists before
const tfs = await testfs({
[hostUtils.pathOnBoot(`extra_uEnv.txt`)]: stripIndent`
custom_fdt_file=mycustom.dtb
extra_os_cmdline=isolcpus=3,4
`,
}).enable();
for (const device of MATCH_TESTS) {
// Test device that has extra_uEnv.txt
let hasExtraUEnv = true;
existsStub.resolves(hasExtraUEnv);
await expect(backend.matches(device.type)).to.eventually.equal(
device.supported && hasExtraUEnv,
);
// Test same device but without extra_uEnv.txt
hasExtraUEnv = false;
existsStub.resolves(hasExtraUEnv);
await expect(backend.matches(device.type)).to.eventually.equal(
device.supported && hasExtraUEnv,
device.supported,
);
}
existsStub.restore();
await tfs.restore();
// The file no longer exists
await expect(
fs.access(hostUtils.pathOnBoot(`extra_uEnv.txt`)),
'extra_uEnv.txt does not exist before the test',
).to.be.rejected;
for (const device of MATCH_TESTS) {
// Test same device but without extra_uEnv.txt
await expect(backend.matches(device.type)).to.eventually.be.false;
}
});
it('errors when cannot find extra_uEnv.txt', async () => {
// Stub readFile to reject much like if the file didn't exist
readFileStub.rejects();
// The file no longer exists
await expect(
fs.access(hostUtils.pathOnBoot(`extra_uEnv.txt`)),
'extra_uEnv.txt does not exist before the test',
).to.be.rejected;
await expect(backend.getBootConfig()).to.eventually.be.rejectedWith(
'Could not find extra_uEnv file. Device is possibly bricked',
);
});
it('logs warning for malformed extra_uEnv.txt', async () => {
spy(Log, 'warn');
for (const badConfig of MALFORMED_CONFIGS) {
// Stub bad config
readFileStub.resolves(badConfig.contents);
// Setup the environment with a bad config
const tfs = await testfs({
[hostUtils.pathOnBoot(`extra_uEnv.txt`)]: badConfig.contents,
}).enable();
// Expect warning log from the given bad config
await backend.getBootConfig();
// @ts-expect-error
expect(Log.warn.lastCall?.lastArg).to.equal(badConfig.reason);
expect(log.warn).to.have.been.calledWith(badConfig.reason);
await tfs.restore();
}
// @ts-expect-error
Log.warn.restore();
});
it('sets new config values', async () => {
stub(fsUtils, 'writeAndSyncFile').resolves();
const logWarningStub = spy(Log, 'warn');
// This config contains a value set from something else
// We to make sure the Supervisor is enforcing the source of truth (the cloud)
// So after setting new values this unsupported/not set value should be gone
readFileStub.resolves(stripIndent`\
extra_os_cmdline=rootwait isolcpus=3,4
other_service=set_this_value
`);
const tfs = await testfs({
// This config contains a value set from something else
// We to make sure the Supervisor is enforcing the source of truth (the cloud)
// So after setting new values this unsupported/not set value should be gone
[hostUtils.pathOnBoot(`extra_uEnv.txt`)]: stripIndent`
extra_os_cmdline=rootwait isolcpus=3,4
other_service=set_this_value
`,
}).enable();
// Sets config with mix of supported and not supported values
await backend.setBootConfig({
@ -126,24 +140,21 @@ describe('extra_uEnv Configuration', () => {
console: 'tty0', // not supported so won't be set
});
expect(fsUtils.writeAndSyncFile).to.be.calledWith(
'test/data/mnt/boot/extra_uEnv.txt',
// Confirm that the file was written correctly
await expect(
fs.readFile(hostUtils.pathOnBoot(`extra_uEnv.txt`), 'utf8'),
).to.eventually.equal(
'custom_fdt_file=/boot/mycustomdtb.dtb\nextra_os_cmdline=isolcpus=2\n',
);
expect(logWarningStub.lastCall?.lastArg).to.equal(
expect(log.warn).to.have.been.calledWith(
'Not setting unsupported value: { console: tty0 }',
);
// Restore stubs
(fsUtils.writeAndSyncFile as SinonStub).restore();
logWarningStub.restore();
await tfs.restore();
});
it('sets new config values containing collections', async () => {
stub(fsUtils, 'writeAndSyncFile').resolves();
const logWarningStub = spy(Log, 'warn');
// @ts-expect-error accessing private value
const previousSupportedConfigs = ExtraUEnv.supportedConfigs;
// Stub isSupportedConfig so we can confirm collections work
@ -155,6 +166,12 @@ describe('extra_uEnv Configuration', () => {
splash: { key: 'extra_os_cmdline', collection: true },
};
const tfs = await testfs({
[hostUtils.pathOnBoot(`extra_uEnv.txt`)]: stripIndent`
other_service=set_this_value
`,
}).enable();
// Set config again
await backend.setBootConfig({
fdt: '/boot/mycustomdtb.dtb',
@ -163,72 +180,16 @@ describe('extra_uEnv Configuration', () => {
splash: '', // collection entry so should be concatted to other collections of this entry
});
expect(fsUtils.writeAndSyncFile).to.be.calledWith(
'test/data/mnt/boot/extra_uEnv.txt',
await expect(
fs.readFile(hostUtils.pathOnBoot(`extra_uEnv.txt`), 'utf8'),
).to.eventually.equal(
'custom_fdt_file=/boot/mycustomdtb.dtb\nextra_os_cmdline=isolcpus=2 console=tty0 splash\n',
);
// Restore stubs
(fsUtils.writeAndSyncFile as SinonStub).restore();
logWarningStub.restore();
// @ts-expect-error accessing private value
ExtraUEnv.supportedConfigs = previousSupportedConfigs;
});
it('only allows supported configuration options', () => {
[
{ configName: 'fdt', supported: true },
{ configName: 'isolcpus', supported: true },
{ configName: 'custom_fdt_file', supported: false },
{ configName: 'splash', supported: false },
{ configName: '', supported: false },
].forEach(({ configName, supported }) =>
expect(backend.isSupportedConfig(configName)).to.equal(supported),
);
});
it('correctly detects boot config variables', () => {
[
{ config: 'HOST_EXTLINUX_isolcpus', valid: true },
{ config: 'HOST_EXTLINUX_fdt', valid: true },
{ config: 'HOST_EXTLINUX_rootwait', valid: true },
{ config: 'HOST_EXTLINUX_5', valid: true },
{ config: 'DEVICE_EXTLINUX_isolcpus', valid: false },
{ config: 'isolcpus', valid: false },
].forEach(({ config, valid }) =>
expect(backend.isBootConfigVar(config)).to.equal(valid),
);
});
it('converts variable to backend formatted name', () => {
[
{ input: 'HOST_EXTLINUX_isolcpus', output: 'isolcpus' },
{ input: 'HOST_EXTLINUX_fdt', output: 'fdt' },
{ input: 'HOST_EXTLINUX_', output: null },
{ input: 'value', output: null },
].forEach(({ input, output }) =>
expect(backend.processConfigVarName(input)).to.equal(output),
);
});
it('normalizes variable value', () => {
[{ input: { key: 'key', value: 'value' }, output: 'value' }].forEach(
({ input, output }) =>
expect(backend.processConfigVarValue(input.key, input.value)).to.equal(
output,
),
);
});
it('returns the environment name for config variable', () => {
[
{ input: 'isolcpus', output: 'HOST_EXTLINUX_isolcpus' },
{ input: 'fdt', output: 'HOST_EXTLINUX_fdt' },
{ input: 'rootwait', output: 'HOST_EXTLINUX_rootwait' },
{ input: '', output: null },
].forEach(({ input, output }) =>
expect(backend.createConfigVarName(input)).to.equal(output),
);
await tfs.restore();
});
});

View File

@ -0,0 +1,75 @@
import { promises as fs } from 'fs';
import { expect } from 'chai';
import { testfs } from 'mocha-pod';
import * as hostUtils from '~/lib/host-utils';
import log from '~/lib/supervisor-console';
import { Odmdata } from '~/src/config/backends/odmdata';
describe('config/odmdata', () => {
const backend = new Odmdata();
it('logs error when unable to open boot config file', async () => {
await expect(
fs.access(hostUtils.pathOnRoot('/dev/mmcblk0boot0')),
'file does not exist before the test',
).to.be.rejected;
await expect(backend.getBootConfig()).to.be.rejected;
expect(log.error).to.have.been.calledWith(
`File not found at: ${hostUtils.pathOnRoot('/dev/mmcblk0boot0')}`,
);
});
it('logs error for malformed configuration mode', async () => {
// Logs when configuration mode is unknown
// @ts-expect-error accessing private value
expect(() => backend.parseOptions(Buffer.from([0x9, 0x9, 0x9]))).to.throw();
expect(log.error).to.have.been.calledWith(
'ODMDATA is set with an unsupported byte: 0x9',
);
// @ts-expect-error accessing private value
expect(() => backend.parseOptions(Buffer.from([0x1, 0x0, 0x0]))).to.throw();
expect(log.error).to.have.been.calledWith(
'Unable to parse ODMDATA configuration. Data at offsets do not match.',
);
});
it('should parse configuration options from bootConfigPath', async () => {
const tfs = await testfs({
[hostUtils.pathOnRoot('/dev/mmcblk0boot0')]: testfs.from(
'test/data/boot0.img',
),
}).enable();
// Restore openFile so test actually uses testConfigPath
await expect(backend.getBootConfig()).to.eventually.deep.equal({
configuration: '2',
});
await tfs.restore();
});
it('sets new config values', async () => {
const tfs = await testfs({
[hostUtils.pathOnRoot('/dev/mmcblk0boot0')]: testfs.from(
'test/data/boot0.img',
),
[hostUtils.pathOnRoot('/sys/block/mmcblk0boot0/force_ro')]: '0',
}).enable();
// Sets a new configuration
await backend.setBootConfig({
configuration: '4',
});
// Check that new configuration was set correctly
await expect(backend.getBootConfig()).to.eventually.deep.equal({
configuration: '4',
});
await tfs.restore();
});
});

View File

@ -0,0 +1,441 @@
import { promises as fs } from 'fs';
import { testfs, TestFs } from 'mocha-pod';
import { expect } from 'chai';
import * as hostUtils from '~/lib/host-utils';
import { SplashImage } from '~/src/config/backends/splash-image';
import log from '~/lib/supervisor-console';
describe('config/splash-image', () => {
const backend = new SplashImage();
const defaultLogo =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/wQDLA+84AAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==';
const logo =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=';
const uri = `data:image/png;base64,${logo}`;
describe('initialise', () => {
let tfs: TestFs.Enabled;
beforeEach(async () => {
tfs = await testfs(
{
[hostUtils.pathOnBoot('splash/balena-logo.png')]: Buffer.from(
defaultLogo,
'base64',
),
},
{ cleanup: [hostUtils.pathOnBoot('splash/balena-logo-default.png')] },
).enable();
});
afterEach(async () => {
await tfs.restore();
});
it('should make a copy of the existing boot image on initialise if not yet created', async () => {
await expect(
fs.access(hostUtils.pathOnBoot('splash/balena-logo-default.png')),
'logo copy should not exist before first initialization',
).to.be.rejected;
// Do the initialization
await backend.initialise();
// The copy should exist after the test and equal defaultLogo
await expect(
fs.access(hostUtils.pathOnBoot('splash/balena-logo-default.png')),
'logo copy should exist after initialization',
).to.not.be.rejected;
expect(
await fs.readFile(
hostUtils.pathOnBoot('splash/balena-logo-default.png'),
'base64',
),
).to.equal(defaultLogo);
});
it('should skip initialization if the default image already exists', async () => {
// Write a different logo as default
await fs.writeFile(
hostUtils.pathOnBoot('splash/balena-logo-default.png'),
Buffer.from(logo, 'base64'),
);
// Do the initialization
await backend.initialise();
// The copy should exist after the test and be equal to logo (it should not) have
// been changed
await expect(
fs.access(hostUtils.pathOnBoot('splash/balena-logo-default.png')),
'logo copy still exists after initialization',
).to.not.be.rejected;
expect(
await fs.readFile(
hostUtils.pathOnBoot('splash/balena-logo-default.png'),
'base64',
),
).to.equal(logo);
});
it('should warn about failed initialization if there is no default image on the device', async () => {
await fs.unlink(hostUtils.pathOnBoot('splash/balena-logo.png'));
// Do the initialization
await backend.initialise();
expect(log.warn).to.be.calledOnceWith(
'Could not initialise splash image backend',
);
});
});
describe('getBootConfig', () => {
it('should return an empty object if the current logo matches the default logo', async () => {
const tfs = await testfs({
// Both the logo and the copy resolve to the same value
[hostUtils.pathOnBoot('splash/resin-logo.png')]: Buffer.from(
logo,
'base64',
),
[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
logo,
'base64',
),
}).enable();
// The default logo resolves to the same value as the current logo
expect(await backend.getBootConfig()).to.deep.equal({});
await tfs.restore();
});
it('should read the splash image from resin-logo.png if available', async () => {
const tfs = await testfs({
// resin logo and balena-logo-default are different which means the current logo
// is a custom image
[hostUtils.pathOnBoot('splash/resin-logo.png')]: Buffer.from(
logo,
'base64',
),
[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
defaultLogo,
'base64',
),
}).enable();
expect(await backend.getBootConfig()).to.deep.equal({
image: uri,
});
await tfs.restore();
});
it('should read the splash image from balena-logo.png if available', async () => {
const tfs = await testfs({
// balena-logo and balena-logo-default are different which means the current logo
// is a custom image
[hostUtils.pathOnBoot('splash/balena-logo.png')]: Buffer.from(
logo,
'base64',
),
[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
defaultLogo,
'base64',
),
}).enable();
expect(await backend.getBootConfig()).to.deep.equal({
image: uri,
});
await tfs.restore();
});
it('should read the splash image from balena-logo.png even if resin-logo.png exists', async () => {
const tfs = await testfs({
[hostUtils.pathOnBoot('splash/balena-logo.png')]: Buffer.from(
logo,
'base64',
),
// resin-logo has the same value as default, but it is ignored since balena-logo exists
[hostUtils.pathOnBoot('splash/resin-logo.png')]: Buffer.from(
defaultLogo,
'base64',
),
[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
defaultLogo,
'base64',
),
}).enable();
expect(await backend.getBootConfig()).to.deep.equal({
// balena-logo is the value read
image: uri,
});
await tfs.restore();
});
it('should catch readDir errors', async () => {
// Remove the directory before the tests to cause getBootConfig to fail
await fs
.rm(hostUtils.pathOnBoot('splash'), { recursive: true, force: true })
.catch(() => {
/* noop */
});
expect(await backend.getBootConfig()).to.deep.equal({});
expect(log.warn).to.be.calledOnceWith('Failed to read splash image:');
});
it('should catch readFile errors', async () => {
await expect(
fs.access(hostUtils.pathOnBoot('splash/balena-logo.png')),
'logo does not exist before getting boot config',
).to.be.rejected;
await expect(
fs.access(hostUtils.pathOnBoot('splash/resin-logo.png')),
'logo does not exist before getting boot config',
).to.be.rejected;
expect(await backend.getBootConfig()).to.deep.equal({});
expect(log.warn).to.be.calledOnceWith('Failed to read splash image:');
});
});
describe('setBootConfig', () => {
it('should write the given image to resin-logo.png if set', async () => {
const tfs = await testfs({
[hostUtils.pathOnBoot('splash/resin-logo.png')]: Buffer.from(
defaultLogo,
'base64',
),
[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
defaultLogo,
'base64',
),
}).enable();
await backend.setBootConfig({ image: uri });
// Since resin-logo already exists we use that to write
expect(
await fs.readFile(
hostUtils.pathOnBoot('splash/resin-logo.png'),
'base64',
),
).to.equal(logo);
await tfs.restore();
});
it('should write the given image to balena-logo.png if set', async () => {
const tfs = await testfs({
[hostUtils.pathOnBoot('splash/resin-logo.png')]: Buffer.from(
defaultLogo,
'base64',
),
[hostUtils.pathOnBoot('splash/balena-logo.png')]: Buffer.from(
defaultLogo,
'base64',
),
[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
defaultLogo,
'base64',
),
}).enable();
await backend.setBootConfig({ image: uri });
// Resin logo should not have changed
expect(
await fs.readFile(
hostUtils.pathOnBoot('splash/resin-logo.png'),
'base64',
),
).to.equal(defaultLogo);
// balena-logo is used as the correct location
expect(
await fs.readFile(
hostUtils.pathOnBoot('splash/balena-logo.png'),
'base64',
),
).to.equal(logo);
await tfs.restore();
});
it('should accept just a base64 as an image', async () => {
const tfs = await testfs({
[hostUtils.pathOnBoot('splash/balena-logo.png')]: Buffer.from(
defaultLogo,
'base64',
),
[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
defaultLogo,
'base64',
),
}).enable();
// We use just the base64 image instead of the data uri
await backend.setBootConfig({ image: logo });
// The file should have changed
expect(
await fs.readFile(
hostUtils.pathOnBoot('splash/balena-logo.png'),
'base64',
),
).to.equal(logo);
await tfs.restore();
});
it('should restore balena-logo.png if image arg is unset', async () => {
const tfs = await testfs({
[hostUtils.pathOnBoot('splash/balena-logo.png')]: Buffer.from(
logo,
'base64',
),
[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
defaultLogo,
'base64',
),
}).enable();
// setBootConfig with an empty object should delete the iamge
await backend.setBootConfig({});
// The default should have been reverted to the default value
expect(
await fs.readFile(
hostUtils.pathOnBoot('splash/balena-logo.png'),
'base64',
),
).to.equal(defaultLogo);
await tfs.restore();
});
it('should restore resin-logo.png if image arg is unset', async () => {
const tfs = await testfs({
[hostUtils.pathOnBoot('splash/resin-logo.png')]: Buffer.from(
logo,
'base64',
),
[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
defaultLogo,
'base64',
),
}).enable();
// setBootConfig with an empty object should delete the iamge
await backend.setBootConfig({});
// The default should have been reverted to the default value
expect(
await fs.readFile(
hostUtils.pathOnBoot('splash/resin-logo.png'),
'base64',
),
).to.equal(defaultLogo);
await tfs.restore();
});
it('should restore balena-logo.png if image arg is empty', async () => {
const tfs = await testfs({
[hostUtils.pathOnBoot('splash/balena-logo.png')]: Buffer.from(
logo,
'base64',
),
[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
defaultLogo,
'base64',
),
}).enable();
// setBootConfig with an empty image should also restore the default
await backend.setBootConfig({ image: '' });
// The default should have been reverted to the default value
expect(
await fs.readFile(
hostUtils.pathOnBoot('splash/balena-logo.png'),
'base64',
),
).to.equal(defaultLogo);
await tfs.restore();
});
// TODO: note that ignoring a value on the backend will cause the supervisor
// to go into a loop trying to apply target state, as the current will never match
// the target. The image needs to be validated cloud side, and as a last line of defense,
// when receiving the target state
it('should ignore the value if arg is not a valid base64 string', async () => {
const tfs = await testfs({
[hostUtils.pathOnBoot('splash/balena-logo.png')]: Buffer.from(
defaultLogo,
'base64',
),
[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
defaultLogo,
'base64',
),
}).enable();
// We pass something that is not an image
await backend.setBootConfig({ image: 'somestring' });
// The file should NOT have changed
expect(
await fs.readFile(
hostUtils.pathOnBoot('splash/balena-logo.png'),
'base64',
),
).to.equal(defaultLogo);
await tfs.restore();
});
it('should ignore the value if image is not a valid PNG file', async () => {
const tfs = await testfs({
[hostUtils.pathOnBoot('splash/balena-logo.png')]: Buffer.from(
defaultLogo,
'base64',
),
[hostUtils.pathOnBoot('splash/balena-logo-default.png')]: Buffer.from(
defaultLogo,
'base64',
),
}).enable();
// We pass something that is not a PNG
await backend.setBootConfig({ image: 'aGVsbG8=' });
// The file should NOT have changed
expect(
await fs.readFile(
hostUtils.pathOnBoot('splash/balena-logo.png'),
'base64',
),
).to.equal(defaultLogo);
await tfs.restore();
});
});
describe('isBootConfigVar', () => {
it('Accepts any case variable names', () => {
expect(backend.isBootConfigVar('HOST_SPLASH_IMAGE')).to.be.true;
expect(backend.isBootConfigVar('HOST_SPLASH_image')).to.be.true;
expect(backend.isBootConfigVar('HOST_SPLASH_Image')).to.be.true;
expect(backend.isBootConfigVar('HOST_SPLASH_ImAgE')).to.be.true;
});
});
});

View File

@ -1,8 +1,6 @@
import { stub } from 'sinon';
import * as _ from 'lodash';
import { expect } from 'chai';
import { testfs } from 'mocha-pod';
import * as config from '~/src/config';
import * as configUtils from '~/src/config/utils';
import { ExtraUEnv } from '~/src/config/backends/extra-uEnv';
import { Extlinux } from '~/src/config/backends/extlinux';
@ -11,34 +9,43 @@ import { ConfigFs } from '~/src/config/backends/config-fs';
import { SplashImage } from '~/src/config/backends/splash-image';
import { ConfigBackend } from '~/src/config/backends/backend';
describe('Config Utilities', () => {
import * as hostUtils from '~/lib/host-utils';
const keys = <T extends object>(obj: T) => Object.keys(obj) as Array<keyof T>;
describe('config/utils', () => {
it('gets list of supported backends', async () => {
// Stub so that we get an array containing only config-txt backend
const configStub = stub(config, 'get').resolves('raspberry');
const tFs = await testfs({
// This is only needed so config.get doesn't fail
[hostUtils.pathOnBoot('config.json')]: JSON.stringify({
deviceType: 'raspberrypi4',
}),
}).enable();
// Get list of backends
const devices = await configUtils.getSupportedBackends();
expect(devices.length).to.equal(2);
expect(devices[0].constructor.name).to.equal('ConfigTxt');
expect(devices[1].constructor.name).to.equal('SplashImage');
// Restore stub
configStub.restore();
await tFs.restore();
// TO-DO: When we have a device that will match for multiple backends
// add a test that we get more then 1 backend for that device
});
it('transforms environment variables to boot configs', () => {
_.forEach(CONFIGS, (configObj: any, key: string) => {
keys(CONFIGS).forEach((key) => {
expect(
configUtils.envToBootConfig(BACKENDS[key], configObj.envVars),
).to.deep.equal(configObj.bootConfig);
configUtils.envToBootConfig(BACKENDS[key], CONFIGS[key].envVars),
).to.deep.equal(CONFIGS[key].bootConfig);
});
});
it('transforms boot configs to environment variables', () => {
_.forEach(CONFIGS, (configObj: any, key: string) => {
keys(CONFIGS).forEach((key) => {
expect(
configUtils.bootConfigToEnv(BACKENDS[key], configObj.bootConfig),
).to.deep.equal(configObj.envVars);
configUtils.bootConfigToEnv(BACKENDS[key], CONFIGS[key].bootConfig),
).to.deep.equal(CONFIGS[key].envVars);
});
});
});

View File

@ -1,7 +1,7 @@
import { stripIndent } from 'common-tags';
import { promises as fs } from 'fs';
import * as path from 'path';
import { SinonStub, stub, spy, SinonSpy, restore } from 'sinon';
import { SinonStub, stub, spy, SinonSpy } from 'sinon';
import { expect } from 'chai';
import * as deviceConfig from '~/src/device-config';
@ -13,11 +13,8 @@ import { Odmdata } from '~/src/config/backends/odmdata';
import { ConfigFs } from '~/src/config/backends/config-fs';
import { SplashImage } from '~/src/config/backends/splash-image';
import * as constants from '~/lib/constants';
import log from '~/lib/supervisor-console';
import { fnSchema } from '~/src/config/functions';
import prepare = require('~/test-lib/prepare');
import mock = require('mock-fs');
import { testfs } from 'mocha-pod';
const extlinuxBackend = new Extlinux();
const configTxtBackend = new ConfigTxt();
@ -33,7 +30,7 @@ describe('device-config', () => {
constants.rootMountPoint,
constants.bootMountPoint,
);
const configJson = 'test/data/config.json';
const configJson = path.join(bootMountPoint, 'config.json');
const configFsJson = path.join(bootMountPoint, 'configfs.json');
const configTxt = path.join(bootMountPoint, 'config.txt');
const deviceTypeJson = path.join(bootMountPoint, 'device-type.json');
@ -42,29 +39,14 @@ describe('device-config', () => {
let logSpy: SinonSpy;
before(async () => {
// disable log output during testing
stub(log, 'debug');
stub(log, 'warn');
stub(log, 'info');
stub(log, 'event');
stub(log, 'success');
logSpy = spy(logger, 'logSystemMessage');
await prepare();
// clear memoized data from config
fnSchema.deviceType.clear();
fnSchema.deviceArch.clear();
});
after(() => {
restore();
// clear memoized data from config
fnSchema.deviceType.clear();
fnSchema.deviceArch.clear();
logSpy.restore();
});
afterEach(() => {
// Restore stubs
logSpy.resetHistory();
});
@ -118,11 +100,10 @@ describe('device-config', () => {
});
describe('config.txt', () => {
const mockFs = () => {
mock({
// This is only needed so config.get doesn't fail
[configJson]: JSON.stringify({ deviceType: 'fincm3' }),
[configTxt]: stripIndent`
const tFs = testfs({
// This is only needed so config.get doesn't fail
[configJson]: JSON.stringify({ deviceType: 'fincm3' }),
[configTxt]: stripIndent`
enable_uart=1
dtparam=i2c_arm=on
dtparam=spi=on
@ -130,29 +111,24 @@ describe('device-config', () => {
avoid_warnings=1
dtparam=audio=on
gpu_mem=16`,
[osRelease]: stripIndent`
[osRelease]: stripIndent`
PRETTY_NAME="balenaOS 2.88.5+rev1"
META_BALENA_VERSION="2.88.5"
VARIANT_ID="dev"`,
[deviceTypeJson]: JSON.stringify({
slug: 'fincm3',
arch: 'armv7hf',
}),
});
};
const unmockFs = () => {
mock.restore();
};
beforeEach(() => {
mockFs();
[deviceTypeJson]: JSON.stringify({
slug: 'fincm3',
arch: 'armv7hf',
}),
});
afterEach(() => {
beforeEach(async () => {
await tFs.enable();
});
afterEach(async () => {
// Reset the state of the fs after each test to
// prevent tests leaking into each other
unmockFs();
await tFs.restore();
});
it('correctly parses a config.txt file', async () => {
@ -337,20 +313,19 @@ describe('device-config', () => {
describe('extlinux', () => {
const extlinuxConf = path.join(bootMountPoint, 'extlinux/extlinux.conf');
const mockFs = () => {
mock({
// This is only needed so config.get doesn't fail
[configJson]: JSON.stringify({}),
[osRelease]: stripIndent`
const tFs = testfs({
// This is only needed so config.get doesn't fail
[configJson]: JSON.stringify({}),
[osRelease]: stripIndent`
PRETTY_NAME="balenaOS 2.88.5+rev1"
META_BALENA_VERSION="2.88.5"
VARIANT_ID="dev"
`,
[deviceTypeJson]: JSON.stringify({
slug: 'fincm3',
arch: 'armv7hf',
}),
[extlinuxConf]: stripIndent`
[deviceTypeJson]: JSON.stringify({
slug: 'fincm3',
arch: 'armv7hf',
}),
[extlinuxConf]: stripIndent`
DEFAULT primary
TIMEOUT 30
MENU TITLE Boot Options
@ -359,35 +334,24 @@ describe('device-config', () => {
LINUX /Image
APPEND \${cbootargs} \${resin_kernel_root} ro rootwait
`,
});
};
const unmockFs = () => {
mock.restore();
};
beforeEach(() => {
mockFs();
});
afterEach(() => {
beforeEach(async () => {
await tFs.enable();
});
afterEach(async () => {
// Reset the state of the fs after each test to
// prevent tests leaking into each other
unmockFs();
await tFs.restore();
});
it('should correctly write to extlinux.conf files', async () => {
const current = {};
const target = {
HOST_EXTLINUX_isolcpus: '2',
HOST_EXTLINUX_fdt: '/boot/mycustomdtb.dtb',
};
expect(
// @ts-expect-error accessing private value
deviceConfig.bootConfigChangeRequired(extlinuxBackend, current, target),
).to.equal(true);
await deviceConfig.setBootConfig(extlinuxBackend, target);
expect(logSpy).to.be.calledTwice;
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
@ -498,50 +462,44 @@ describe('device-config', () => {
'sys/kernel/config/acpi/table',
);
const mockFs = () => {
mock({
// This is only needed so config.get doesn't fail
[configJson]: JSON.stringify({}),
[osRelease]: stripIndent`
const tFs = testfs({
// This is only needed so config.get doesn't fail
[configJson]: JSON.stringify({}),
[osRelease]: stripIndent`
PRETTY_NAME="balenaOS 2.88.5+rev1"
META_BALENA_VERSION="2.88.5"
VARIANT_ID="dev"
`,
[configFsJson]: JSON.stringify({
ssdt: ['spidev1.1'],
}),
[configFsJson]: JSON.stringify({
ssdt: ['spidev1.1'],
}),
[deviceTypeJson]: JSON.stringify({
slug: 'fincm3',
arch: 'armv7hf',
}),
[acpiTables]: {
'spidev1.0.aml': '',
'spidev1.1.aml': '',
[deviceTypeJson]: JSON.stringify({
slug: 'fincm3',
arch: 'armv7hf',
}),
[acpiTables]: {
'spidev1.0.aml': '',
'spidev1.1.aml': '',
},
[sysKernelAcpiTable]: {
// Add necessary files to avoid the module reporting an error
'spidev1.1': {
oem_id: '',
oem_table_id: '',
oem_revision: '',
},
[sysKernelAcpiTable]: {
// Add necessary files to avoid the module reporting an error
'spidev1.1': {
oem_id: '',
oem_table_id: '',
oem_revision: '',
},
},
});
};
const unmockFs = () => {
mock.restore();
};
beforeEach(() => {
mockFs();
},
});
afterEach(() => {
beforeEach(async () => {
await tFs.enable();
});
afterEach(async () => {
// Reset the state of the fs after each test to
// prevent tests leaking into each other
unmockFs();
await tFs.restore();
});
it('should correctly write to configfs.json files', async () => {
@ -568,13 +526,20 @@ describe('device-config', () => {
});
it('should correctly load the configfs.json file', async () => {
await configFsBackend.initialise();
stub(fsUtils, 'exec').resolves();
await configFsBackend.initialise();
// TODO: unfortunately there is no way to test initialization without stubbing
// modprobe cannot be replaced by something else and the up-board modules
// will not be present within the container environment. OS tests need
// to check that applying configurations actually succeds
expect(fsUtils.exec).to.be.calledWith('modprobe acpi_configfs');
// If the module performs this call, it's because all the prior checks succeeded
expect(fsUtils.exec).to.be.calledWith(
'cat test/data/boot/acpi-tables/spidev1.1.aml > test/data/sys/kernel/config/acpi/table/spidev1.1/aml',
'cat /mnt/root/boot/acpi-tables/spidev1.1.aml > /mnt/root/sys/kernel/config/acpi/table/spidev1.1/aml',
);
// Restore stubs
@ -653,8 +618,8 @@ describe('device-config', () => {
const splash = path.join(bootMountPoint, 'splash');
const mockFs = () => {
mock({
const mockFs = testfs(
{
// This is only needed so config.get doesn't fail
[configJson]: JSON.stringify({}),
[osRelease]: stripIndent`
@ -667,45 +632,33 @@ describe('device-config', () => {
arch: 'aarch64',
}),
[splash]: {
/* empty directory */
dummy: '', // to ensure the directory is created
},
});
};
},
{ cleanup: [`${splash}/*.png`] },
);
const unmockFs = () => {
mock.restore();
};
beforeEach(() => {
mockFs();
beforeEach(async () => {
await mockFs.enable();
});
afterEach(() => {
unmockFs();
afterEach(async () => {
await mockFs.restore();
});
it('should correctly write to resin-logo.png', async () => {
// Devices with balenaOS < 2.51 use resin-logo.png
fs.writeFile(
path.join(splash, 'resin-logo.png'),
Buffer.from(defaultLogo, 'base64'),
);
// Devices with balenaOS >= 2.51 use balena-logo.png
const tTfs = await testfs({
[splash]: {
'resin-logo.png': Buffer.from(png, 'base64'),
},
}).enable();
const current = {};
const target = {
HOST_SPLASH_image: png,
};
// This should work with every device type, but testing on a couple
// of options
expect(
deviceConfig.bootConfigChangeRequired(
splashImageBackend,
current,
target,
'fincm3',
),
).to.equal(true);
await deviceConfig.setBootConfig(splashImageBackend, target);
expect(logSpy).to.be.calledTwice;
@ -713,31 +666,22 @@ describe('device-config', () => {
expect(
await fs.readFile(path.join(splash, 'resin-logo.png'), 'base64'),
).to.equal(png);
await tTfs.restore();
});
it('should correctly write to balena-logo.png', async () => {
// Devices with balenaOS >= 2.51 use balena-logo.png
fs.writeFile(
path.join(splash, 'balena-logo.png'),
Buffer.from(defaultLogo, 'base64'),
);
const tTfs = await testfs({
[splash]: {
'balena-logo.png': Buffer.from(png, 'base64'),
},
}).enable();
const current = {};
const target = {
HOST_SPLASH_image: png,
};
// This should work with every device type, but testing on a couple
// of options
expect(
deviceConfig.bootConfigChangeRequired(
splashImageBackend,
current,
target,
'raspberrypi4-64',
),
).to.equal(true);
await deviceConfig.setBootConfig(splashImageBackend, target);
expect(logSpy).to.be.calledTwice;
@ -745,6 +689,8 @@ describe('device-config', () => {
expect(
await fs.readFile(path.join(splash, 'balena-logo.png'), 'base64'),
).to.equal(png);
await tTfs.restore();
});
it('should correctly write to balena-logo.png if no default logo is found', async () => {
@ -775,72 +721,52 @@ describe('device-config', () => {
});
it('should correctly read the splash logo if different from the default', async () => {
stub(fs, 'readdir').resolves(['balena-logo.png'] as any);
const readFileStub: SinonStub = stub(fs, 'readFile').resolves(
Buffer.from(png, 'base64') as any,
);
readFileStub
.withArgs('test/data/mnt/boot/splash/balena-logo-default.png')
.resolves(Buffer.from(defaultLogo, 'base64') as any);
const tTfs = await testfs({
[splash]: {
'balena-logo.png': Buffer.from(png, 'base64'),
'balena-logo-default.png': Buffer.from(defaultLogo, 'base64'),
},
}).enable();
expect(
await deviceConfig.getBootConfig(splashImageBackend),
).to.deep.equal({
HOST_SPLASH_image: uri,
});
expect(readFileStub).to.be.calledWith(
'test/data/mnt/boot/splash/balena-logo-default.png',
);
expect(readFileStub).to.be.calledWith(
'test/data/mnt/boot/splash/balena-logo.png',
);
// Restore stubs
(fs.readdir as SinonStub).restore();
(fs.readFile as SinonStub).restore();
readFileStub.restore();
await tTfs.restore();
});
});
describe('getRequiredSteps', () => {
const splash = path.join(bootMountPoint, 'splash/balena-logo.png');
// TODO: something like this could be done as a fixture instead of
// doing the file initialisation on 00-init.ts
const mockFs = () => {
mock({
// This is only needed so config.get doesn't fail
[configJson]: JSON.stringify({}),
[configTxt]: stripIndent`
const tFs = testfs({
// This is only needed so config.get doesn't fail
[configJson]: JSON.stringify({}),
[configTxt]: stripIndent`
enable_uart=true
`,
[osRelease]: stripIndent`
[osRelease]: stripIndent`
PRETTY_NAME="balenaOS 2.88.5+rev1"
META_BALENA_VERSION="2.88.5"
VARIANT_ID="dev"
`,
[deviceTypeJson]: JSON.stringify({
slug: 'raspberrypi4-64',
arch: 'aarch64',
}),
[splash]: Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
'base64',
),
});
};
const unmockFs = () => {
mock.restore();
};
beforeEach(() => {
mockFs();
[deviceTypeJson]: JSON.stringify({
slug: 'raspberrypi4-64',
arch: 'aarch64',
}),
[splash]: Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
'base64',
),
});
afterEach(() => {
unmockFs();
beforeEach(async () => {
await tFs.enable();
});
afterEach(async () => {
await tFs.restore();
});
it('returns required steps to config.json first if any', async () => {

View File

@ -1,274 +0,0 @@
import { SinonStub, stub } from 'sinon';
import { promises as fs } from 'fs';
import { resolve } from 'path';
import { expect } from 'chai';
import Log from '~/lib/supervisor-console';
import { Odmdata } from '~/src/config/backends/odmdata';
describe('ODMDATA Configuration', () => {
const backend = new Odmdata();
let logWarningStub: SinonStub;
let logErrorStub: SinonStub;
// @ts-expect-error accessing private vluae
const previousConfigPath = Odmdata.bootConfigPath;
const testConfigPath = resolve(process.cwd(), 'test/data/boot0.img');
before(() => {
// @ts-expect-error setting value of private variable
Odmdata.bootConfigPath = testConfigPath;
});
after(() => {
// @ts-expect-error setting value of private variable
Odmdata.bootConfigPath = previousConfigPath;
});
beforeEach(() => {
logWarningStub = stub(Log, 'warn');
logErrorStub = stub(Log, 'error');
});
afterEach(() => {
logWarningStub.restore();
logErrorStub.restore();
});
it('only matches supported devices', async () => {
for (const { deviceType, match } of MATCH_TESTS) {
await expect(backend.matches(deviceType)).to.eventually.equal(match);
}
});
it('logs error when unable to open boot config file', async () => {
const logs = [
{
error: { code: 'ENOENT' },
message: `File not found at: ${testConfigPath}`,
},
{
error: { code: 'EACCES' },
message: `Permission denied when opening '${testConfigPath}'`,
},
{
error: { code: 'UNKNOWN ISSUE' }, // not a real code
message: `Unknown error when opening '${testConfigPath}'`,
},
];
const openFileStub = stub(fs, 'open');
for (const log of logs) {
// Stub openFileStub with specific error
openFileStub.rejects(log.error);
try {
// @ts-expect-error accessing private value
await backend.getFileHandle(testConfigPath);
} catch {
// noop
}
// Check that correct message was logged
expect(logErrorStub.lastCall?.args[0]).to.equal(log.message);
}
openFileStub.restore();
});
it('should parse configuration options from bootConfigPath', async () => {
// Restore openFile so test actually uses testConfigPath
await expect(backend.getBootConfig()).to.eventually.deep.equal({
configuration: '2',
});
});
it('correctly parses configuration mode', async () => {
for (const config of CONFIG_MODES) {
// @ts-expect-error accessing private value
expect(backend.parseOptions(config.buffer)).to.deep.equal({
configuration: config.mode,
});
}
});
it('logs error for malformed configuration mode', async () => {
// Logs when configuration mode is unknown
try {
// @ts-expect-error accessing private value
backend.parseOptions(Buffer.from([0x9, 0x9, 0x9]));
} catch (e) {
// noop
}
// Check that correct message was logged
expect(logErrorStub.lastCall?.lastArg).to.equal(
'ODMDATA is set with an unsupported byte: 0x9',
);
// Logs when bytes don't match
try {
// @ts-expect-error accessing private value
backend.parseOptions(Buffer.from([0x1, 0x0, 0x0]));
} catch {
// noop
}
// Check that correct message was logged
expect(logErrorStub.lastCall?.lastArg).to.equal(
'Unable to parse ODMDATA configuration. Data at offsets do not match.',
);
});
it('unlock/lock bootConfigPath RO access', async () => {
const writeSpy = stub().resolves();
// @ts-expect-error accessing private value
const handleStub = stub(backend, 'getFileHandle').resolves({
write: writeSpy,
close: async (): Promise<void> => {
// noop
},
});
// @ts-expect-error accessing private value
await backend.setReadOnly(false); // Try to unlock
expect(writeSpy).to.be.calledWith('0');
// @ts-expect-error accessing private value
await backend.setReadOnly(true); // Try to lock
expect(writeSpy).to.be.calledWith('1');
handleStub.restore();
});
it('sets new config values', async () => {
// @ts-expect-error accessing private value
const setROStub = stub(backend, 'setReadOnly');
setROStub.resolves();
// Get current config
const originalConfig = await backend.getBootConfig();
try {
// Sets a new configuration
await backend.setBootConfig({
configuration: '4',
});
// Check that new configuration was set correctly
await expect(backend.getBootConfig()).to.eventually.deep.equal({
configuration: '4',
});
} finally {
// Restore previous value
await backend.setBootConfig(originalConfig);
setROStub.restore();
}
});
it('only allows supported configuration modes', () => {
[
{ configName: 'configuration', supported: true },
{ configName: 'mode', supported: false },
{ configName: '', supported: false },
].forEach(({ configName, supported }) =>
expect(backend.isSupportedConfig(configName)).to.equal(supported),
);
});
it('correctly detects boot config variables', () => {
[
{ config: 'HOST_ODMDATA_configuration', valid: true },
{ config: 'ODMDATA_configuration', valid: false },
{ config: 'HOST_CONFIG_odmdata_configuration', valid: false },
{ config: 'HOST_EXTLINUX_rootwait', valid: false },
{ config: '', valid: false },
].forEach(({ config, valid }) =>
expect(backend.isBootConfigVar(config)).to.equal(valid),
);
});
it('converts variable to backend formatted name', () => {
[
{ input: 'HOST_ODMDATA_configuration', output: 'configuration' },
{ input: 'HOST_ODMDATA_', output: null },
{ input: 'value', output: null },
].forEach(({ input, output }) =>
expect(backend.processConfigVarName(input)).to.equal(output),
);
});
it('normalizes variable value', () => {
[{ input: { key: 'key', value: 'value' }, output: 'value' }].forEach(
({ input, output }) =>
expect(backend.processConfigVarValue(input.key, input.value)).to.equal(
output,
),
);
});
it('returns the environment name for config variable', () => {
[
{ input: 'configuration', output: 'HOST_ODMDATA_configuration' },
{ input: '', output: null },
].forEach(({ input, output }) =>
expect(backend.createConfigVarName(input)).to.equal(output),
);
});
});
const CONFIG_MODES = [
{
mode: '1',
buffer: Buffer.from([0x0, 0x0, 0x0]),
},
{
mode: '2',
buffer: Buffer.from([0x1, 0x1, 0x1]),
},
{
mode: '3',
buffer: Buffer.from([0x6, 0x6, 0x6]),
},
{
mode: '4',
buffer: Buffer.from([0x7, 0x7, 0x7]),
},
{
mode: '5',
buffer: Buffer.from([0x2, 0x2, 0x2]),
},
{
mode: '6',
buffer: Buffer.from([0x3, 0x3, 0x3]),
},
];
const MATCH_TESTS = [
{
deviceType: 'blackboard-tx2',
match: true,
},
{
deviceType: 'jetson-tx2',
match: true,
},
{
deviceType: 'n510-tx2',
match: true,
},
{
deviceType: 'orbitty-tx2',
match: true,
},
{
deviceType: 'spacely-tx2',
match: true,
},
{
deviceType: 'srd3-tx2',
match: true,
},
{
deviceType: 'raspberry-pi',
match: false,
},
{
deviceType: 'up-board',
match: false,
},
{
deviceType: '',
match: false,
},
];

View File

@ -1,289 +0,0 @@
import { promises as fs } from 'fs';
import { SinonStub, stub } from 'sinon';
import { expect } from 'chai';
import * as fsUtils from '~/lib/fs-utils';
import { SplashImage } from '~/src/config/backends/splash-image';
import log from '~/lib/supervisor-console';
describe('Splash image configuration', () => {
const backend = new SplashImage();
const defaultLogo =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/wQDLA+84AAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==';
const logo =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=';
const uri = `data:image/png;base64,${logo}`;
let readDirStub: SinonStub;
let readFileStub: SinonStub;
let writeAndSyncFileStub: SinonStub;
beforeEach(() => {
// Setup stubs
writeAndSyncFileStub = stub(fsUtils, 'writeAndSyncFile').resolves();
readFileStub = stub(fs, 'readFile').resolves(
Buffer.from(logo, 'base64') as any,
);
readFileStub
.withArgs('test/data/mnt/boot/splash/balena-logo-default.png')
.resolves(Buffer.from(defaultLogo, 'base64') as any);
readDirStub = stub(fs, 'readdir').resolves(['balena-logo.png'] as any);
});
afterEach(() => {
// Restore stubs
writeAndSyncFileStub.restore();
readFileStub.restore();
readDirStub.restore();
});
describe('initialise', () => {
it('should make a copy of the existing boot image on initialise if not yet created', async () => {
stub(fsUtils, 'exists').resolves(false);
// Do the initialization
await backend.initialise();
expect(fs.readFile).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo.png',
);
// Should make a copy
expect(writeAndSyncFileStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo-default.png',
Buffer.from(logo, 'base64'),
);
(fsUtils.exists as SinonStub).restore();
});
it('should skip initialization if the default image already exists', async () => {
stub(fsUtils, 'exists').resolves(true);
// Do the initialization
await backend.initialise();
expect(fsUtils.exists).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo-default.png',
);
expect(fs.readFile).to.not.have.been.called;
(fsUtils.exists as SinonStub).restore();
});
it('should fail initialization if there is no default image on the device', async () => {
stub(fsUtils, 'exists').resolves(false);
readDirStub.resolves([]);
readFileStub.rejects();
stub(log, 'warn');
// Do the initialization
await backend.initialise();
expect(readDirStub).to.be.calledOnce;
expect(fs.readFile).to.have.been.calledOnce;
expect(log.warn).to.be.calledOnce;
(log.warn as SinonStub).restore();
});
});
describe('getBootConfig', () => {
it('should return an empty object if the current logo matches the default logo', async () => {
readDirStub.resolves(['resin-logo.png']);
// The default logo resolves to the same value as the current logo
readFileStub
.withArgs('test/data/mnt/boot/splash/balena-logo-default.png')
.resolves(logo);
expect(await backend.getBootConfig()).to.deep.equal({});
expect(readFileStub).to.be.calledWith(
'test/data/mnt/boot/splash/balena-logo-default.png',
);
expect(readFileStub).to.be.calledWith(
'test/data/mnt/boot/splash/resin-logo.png',
);
});
it('should read the splash image from resin-logo.png if available', async () => {
readDirStub.resolves(['resin-logo.png']);
expect(await backend.getBootConfig()).to.deep.equal({
image: uri,
});
expect(readFileStub).to.be.calledWith(
'test/data/mnt/boot/splash/balena-logo-default.png',
);
expect(readFileStub).to.be.calledWith(
'test/data/mnt/boot/splash/resin-logo.png',
);
});
it('should read the splash image from balena-logo.png if available', async () => {
readDirStub.resolves(['balena-logo.png']);
expect(await backend.getBootConfig()).to.deep.equal({
image: uri,
});
expect(readFileStub).to.be.calledWith(
'test/data/mnt/boot/splash/balena-logo-default.png',
);
expect(readFileStub).to.be.calledWith(
'test/data/mnt/boot/splash/balena-logo.png',
);
});
it('should read the splash image from balena-logo.png even if resin-logo.png exists', async () => {
readDirStub.resolves(['balena-logo.png', 'resin-logo.png']);
expect(await backend.getBootConfig()).to.deep.equal({
image: uri,
});
expect(readFileStub).to.be.calledWith(
'test/data/mnt/boot/splash/balena-logo-default.png',
);
expect(readFileStub).to.be.calledWith(
'test/data/mnt/boot/splash/balena-logo.png',
);
});
it('should catch readDir errors', async () => {
stub(log, 'warn');
readDirStub.rejects();
expect(await backend.getBootConfig()).to.deep.equal({});
expect(readDirStub).to.be.called;
expect(log.warn).to.be.calledOnce;
(log.warn as SinonStub).restore();
});
it('should catch readFile errors', async () => {
stub(log, 'warn');
readDirStub.resolves([]);
readFileStub.rejects();
expect(await backend.getBootConfig()).to.deep.equal({});
expect(log.warn).to.be.calledOnce;
(log.warn as SinonStub).restore();
});
});
describe('setBootConfig', () => {
it('should write the given image to resin-logo.png if set', async () => {
readDirStub.resolves(['resin-logo.png']);
await backend.setBootConfig({ image: uri });
expect(writeAndSyncFileStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/resin-logo.png',
Buffer.from(logo, 'base64'),
);
});
it('should write the given image to balena-logo.png if set', async () => {
readDirStub.resolves(['balena-logo.png']);
await backend.setBootConfig({ image: uri });
expect(writeAndSyncFileStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo.png',
Buffer.from(logo, 'base64'),
);
});
it('should write the given image to balena-logo.png by default', async () => {
readDirStub.resolves([]);
await backend.setBootConfig({ image: uri });
expect(writeAndSyncFileStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo.png',
Buffer.from(logo, 'base64'),
);
});
it('should accept just a base64 as an image', async () => {
readDirStub.resolves(['balena-logo.png']);
await backend.setBootConfig({ image: logo });
expect(writeAndSyncFileStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo.png',
Buffer.from(logo, 'base64'),
);
});
it('should restore balena-logo.png if image arg is unset', async () => {
readDirStub.resolves(['balena-logo.png']);
await backend.setBootConfig({});
expect(readFileStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo-default.png',
);
expect(writeAndSyncFileStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo.png',
Buffer.from(defaultLogo, 'base64'),
);
});
it('should restore resin-logo.png if image arg is unset', async () => {
readDirStub.resolves(['resin-logo.png']);
await backend.setBootConfig({});
expect(readFileStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo-default.png',
);
expect(writeAndSyncFileStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/resin-logo.png',
Buffer.from(defaultLogo, 'base64'),
);
});
it('should restore balena-logo.png if image arg is empty', async () => {
readDirStub.resolves(['balena-logo.png']);
await backend.setBootConfig({ image: '' });
expect(readFileStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo-default.png',
);
expect(writeAndSyncFileStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo.png',
Buffer.from(defaultLogo, 'base64'),
);
});
it('should restore resin-logo.png if image arg is empty', async () => {
readDirStub.resolves(['resin-logo.png']);
await backend.setBootConfig({ image: '' });
expect(readFileStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo-default.png',
);
expect(writeAndSyncFileStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/resin-logo.png',
Buffer.from(defaultLogo, 'base64'),
);
});
it('should throw if arg is not a valid base64 string', async () => {
expect(backend.setBootConfig({ image: 'somestring' })).to.be.rejected;
expect(writeAndSyncFileStub).to.not.be.called;
});
it('should throw if image is not a valid PNG file', async () => {
expect(backend.setBootConfig({ image: 'aGVsbG8=' })).to.be.rejected;
expect(writeAndSyncFileStub).to.not.be.called;
});
});
describe('isBootConfigVar', () => {
it('Accepts any case variable names', () => {
expect(backend.isBootConfigVar('HOST_SPLASH_IMAGE')).to.be.true;
expect(backend.isBootConfigVar('HOST_SPLASH_image')).to.be.true;
expect(backend.isBootConfigVar('HOST_SPLASH_Image')).to.be.true;
expect(backend.isBootConfigVar('HOST_SPLASH_ImAgE')).to.be.true;
});
});
});

View File

@ -15,7 +15,9 @@ while :; do
shift
else
printf 'ERROR: "--timeout" requires a non-empty option argument.\n' >&2
exit 1
fi
shift
break
;;
--) # End of all options.
@ -24,6 +26,7 @@ while :; do
;;
-?*)
printf 'WARN: Unknown option (ignored): %s\n' "$1" >&2
shift
;;
*) # Default case: If no more options then break out of the loop.
break ;;

View File

@ -0,0 +1,117 @@
import { expect } from 'chai';
import { ExtraUEnv } from '~/src/config/backends/extra-uEnv';
describe('config/extra-uEnv', () => {
const backend = new ExtraUEnv();
it('only allows supported configuration options', () => {
[
{ configName: 'fdt', supported: true },
{ configName: 'isolcpus', supported: true },
{ configName: 'custom_fdt_file', supported: false },
{ configName: 'splash', supported: false },
{ configName: '', supported: false },
].forEach(({ configName, supported }) =>
expect(backend.isSupportedConfig(configName)).to.equal(supported),
);
});
it('correctly detects boot config variables', () => {
[
{ config: 'HOST_EXTLINUX_isolcpus', valid: true },
{ config: 'HOST_EXTLINUX_fdt', valid: true },
{ config: 'HOST_EXTLINUX_rootwait', valid: true },
{ config: 'HOST_EXTLINUX_5', valid: true },
{ config: 'DEVICE_EXTLINUX_isolcpus', valid: false },
{ config: 'isolcpus', valid: false },
].forEach(({ config, valid }) =>
expect(backend.isBootConfigVar(config)).to.equal(valid),
);
});
it('converts variable to backend formatted name', () => {
[
{ input: 'HOST_EXTLINUX_isolcpus', output: 'isolcpus' },
{ input: 'HOST_EXTLINUX_fdt', output: 'fdt' },
{ input: 'HOST_EXTLINUX_', output: null },
{ input: 'value', output: null },
].forEach(({ input, output }) =>
expect(backend.processConfigVarName(input)).to.equal(output),
);
});
it('normalizes variable value', () => {
[{ input: { key: 'key', value: 'value' }, output: 'value' }].forEach(
({ input, output }) =>
expect(backend.processConfigVarValue(input.key, input.value)).to.equal(
output,
),
);
});
it('returns the environment name for config variable', () => {
[
{ input: 'isolcpus', output: 'HOST_EXTLINUX_isolcpus' },
{ input: 'fdt', output: 'HOST_EXTLINUX_fdt' },
{ input: 'rootwait', output: 'HOST_EXTLINUX_rootwait' },
{ input: '', output: null },
].forEach(({ input, output }) =>
expect(backend.createConfigVarName(input)).to.equal(output),
);
});
it('only allows supported configuration options', () => {
[
{ configName: 'fdt', supported: true },
{ configName: 'isolcpus', supported: true },
{ configName: 'custom_fdt_file', supported: false },
{ configName: 'splash', supported: false },
{ configName: '', supported: false },
].forEach(({ configName, supported }) =>
expect(backend.isSupportedConfig(configName)).to.equal(supported),
);
});
it('correctly detects boot config variables', () => {
[
{ config: 'HOST_EXTLINUX_isolcpus', valid: true },
{ config: 'HOST_EXTLINUX_fdt', valid: true },
{ config: 'HOST_EXTLINUX_rootwait', valid: true },
{ config: 'HOST_EXTLINUX_5', valid: true },
{ config: 'DEVICE_EXTLINUX_isolcpus', valid: false },
{ config: 'isolcpus', valid: false },
].forEach(({ config, valid }) =>
expect(backend.isBootConfigVar(config)).to.equal(valid),
);
});
it('converts variable to backend formatted name', () => {
[
{ input: 'HOST_EXTLINUX_isolcpus', output: 'isolcpus' },
{ input: 'HOST_EXTLINUX_fdt', output: 'fdt' },
{ input: 'HOST_EXTLINUX_', output: null },
{ input: 'value', output: null },
].forEach(({ input, output }) =>
expect(backend.processConfigVarName(input)).to.equal(output),
);
});
it('normalizes variable value', () => {
[{ input: { key: 'key', value: 'value' }, output: 'value' }].forEach(
({ input, output }) =>
expect(backend.processConfigVarValue(input.key, input.value)).to.equal(
output,
),
);
});
it('returns the environment name for config variable', () => {
[
{ input: 'isolcpus', output: 'HOST_EXTLINUX_isolcpus' },
{ input: 'fdt', output: 'HOST_EXTLINUX_fdt' },
{ input: 'rootwait', output: 'HOST_EXTLINUX_rootwait' },
{ input: '', output: null },
].forEach(({ input, output }) =>
expect(backend.createConfigVarName(input)).to.equal(output),
);
});
});

View File

@ -0,0 +1,138 @@
import { expect } from 'chai';
import { Odmdata } from '~/src/config/backends/odmdata';
describe('config/odmdata', () => {
const backend = new Odmdata();
it('only matches supported devices', async () => {
for (const { deviceType, match } of MATCH_TESTS) {
await expect(backend.matches(deviceType)).to.eventually.equal(match);
}
});
it('correctly parses configuration mode', async () => {
for (const config of CONFIG_MODES) {
// @ts-expect-error accessing private value
expect(backend.parseOptions(config.buffer)).to.deep.equal({
configuration: config.mode,
});
}
});
it('only allows supported configuration modes', () => {
[
{ configName: 'configuration', supported: true },
{ configName: 'mode', supported: false },
{ configName: '', supported: false },
].forEach(({ configName, supported }) =>
expect(backend.isSupportedConfig(configName)).to.equal(supported),
);
});
it('correctly detects boot config variables', () => {
[
{ config: 'HOST_ODMDATA_configuration', valid: true },
{ config: 'ODMDATA_configuration', valid: false },
{ config: 'HOST_CONFIG_odmdata_configuration', valid: false },
{ config: 'HOST_EXTLINUX_rootwait', valid: false },
{ config: '', valid: false },
].forEach(({ config, valid }) =>
expect(backend.isBootConfigVar(config)).to.equal(valid),
);
});
it('converts variable to backend formatted name', () => {
[
{ input: 'HOST_ODMDATA_configuration', output: 'configuration' },
{ input: 'HOST_ODMDATA_', output: null },
{ input: 'value', output: null },
].forEach(({ input, output }) =>
expect(backend.processConfigVarName(input)).to.equal(output),
);
});
it('normalizes variable value', () => {
[{ input: { key: 'key', value: 'value' }, output: 'value' }].forEach(
({ input, output }) =>
expect(backend.processConfigVarValue(input.key, input.value)).to.equal(
output,
),
);
});
it('returns the environment name for config variable', () => {
[
{ input: 'configuration', output: 'HOST_ODMDATA_configuration' },
{ input: '', output: null },
].forEach(({ input, output }) =>
expect(backend.createConfigVarName(input)).to.equal(output),
);
});
});
const CONFIG_MODES = [
{
mode: '1',
buffer: Buffer.from([0x0, 0x0, 0x0]),
},
{
mode: '2',
buffer: Buffer.from([0x1, 0x1, 0x1]),
},
{
mode: '3',
buffer: Buffer.from([0x6, 0x6, 0x6]),
},
{
mode: '4',
buffer: Buffer.from([0x7, 0x7, 0x7]),
},
{
mode: '5',
buffer: Buffer.from([0x2, 0x2, 0x2]),
},
{
mode: '6',
buffer: Buffer.from([0x3, 0x3, 0x3]),
},
];
const MATCH_TESTS = [
{
deviceType: 'blackboard-tx2',
match: true,
},
{
deviceType: 'jetson-tx2',
match: true,
},
{
deviceType: 'n510-tx2',
match: true,
},
{
deviceType: 'orbitty-tx2',
match: true,
},
{
deviceType: 'spacely-tx2',
match: true,
},
{
deviceType: 'srd3-tx2',
match: true,
},
{
deviceType: 'raspberry-pi',
match: false,
},
{
deviceType: 'up-board',
match: false,
},
{
deviceType: '',
match: false,
},
];