mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-19 05:37:53 +00:00
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:
parent
83c856ae4b
commit
827f892c13
@ -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
18
package-lock.json
generated
@ -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": {
|
||||
|
@ -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",
|
||||
|
78
test/integration/config/config-txt.spec.ts
Normal file
78
test/integration/config/config-txt.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
@ -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),
|
||||
);
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
75
test/integration/config/odmdata.spec.ts
Normal file
75
test/integration/config/odmdata.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
441
test/integration/config/splash-image.spec.ts
Normal file
441
test/integration/config/splash-image.spec.ts
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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 () => {
|
@ -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,
|
||||
},
|
||||
];
|
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
@ -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 ;;
|
||||
|
117
test/unit/config/extra-uenv.spec.ts
Normal file
117
test/unit/config/extra-uenv.spec.ts
Normal 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),
|
||||
);
|
||||
});
|
||||
});
|
138
test/unit/config/odmdata.spec.ts
Normal file
138
test/unit/config/odmdata.spec.ts
Normal 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,
|
||||
},
|
||||
];
|
Loading…
Reference in New Issue
Block a user