mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-21 22:47:49 +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.
|
# in the `testfs` configuration.
|
||||||
filesystem:
|
filesystem:
|
||||||
/mnt/root:
|
/mnt/root:
|
||||||
/mnt/boot/config.json:
|
/mnt/boot:
|
||||||
from: test/data/testconfig.json
|
config.json:
|
||||||
/mnt/boot/config.txt:
|
from: test/data/testconfig.json
|
||||||
from: test/data/mnt/boot/config.txt
|
config.txt:
|
||||||
/mnt/boot/device-type.json:
|
from: test/data/mnt/boot/config.txt
|
||||||
from: test/data/mnt/boot/device-type.json
|
device-type.json:
|
||||||
|
from: test/data/mnt/boot/device-type.json
|
||||||
/etc/os-release:
|
/etc/os-release:
|
||||||
from: test/data/etc/os-release
|
from: test/data/etc/os-release
|
||||||
# The `keep` list defines files that already exist in the
|
# The `keep` list defines files that already exist in the
|
||||||
@ -24,3 +25,4 @@ testfs:
|
|||||||
- /data/database.sqlite
|
- /data/database.sqlite
|
||||||
- /data/apps.json.preloaded
|
- /data/apps.json.preloaded
|
||||||
- /mnt/root/tmp/balena-supervisor/**/*.lock
|
- /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",
|
"lodash": "^4.17.21",
|
||||||
"memoizee": "^0.4.14",
|
"memoizee": "^0.4.14",
|
||||||
"mocha": "^8.3.2",
|
"mocha": "^8.3.2",
|
||||||
"mocha-pod": "^0.8.0",
|
"mocha-pod": "^0.9.0",
|
||||||
"mock-fs": "^4.14.0",
|
"mock-fs": "^4.14.0",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"network-checker": "^0.1.1",
|
"network-checker": "^0.1.1",
|
||||||
@ -8247,9 +8247,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mocha-pod": {
|
"node_modules/mocha-pod": {
|
||||||
"version": "0.8.0",
|
"version": "0.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/mocha-pod/-/mocha-pod-0.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/mocha-pod/-/mocha-pod-0.9.0.tgz",
|
||||||
"integrity": "sha512-0jhPpQMWCduiEFFFPrWWdKonwmyC6TFwgZEo7G/JhpIsmmfQm2cZGpoJ2HfUCXT1bcOuinSUPI8cweG+1fbbhw==",
|
"integrity": "sha512-km2XTNEbxjxrbq3P5XoGm1kHBszQJZXEg0OXMkN/ZTG0g5mKWX6RQPNlfp6m9lRgRvxwdAVVWIPNAo+q/CsbkQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@balena/compose": "^2.1.0",
|
"@balena/compose": "^2.1.0",
|
||||||
@ -8259,7 +8259,7 @@
|
|||||||
"dockerode": "^3.3.2",
|
"dockerode": "^3.3.2",
|
||||||
"fast-glob": "^3.2.11",
|
"fast-glob": "^3.2.11",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"nanoid": "^3.3.4",
|
"nanoid": "3.3.4",
|
||||||
"tar-fs": "^2.1.1"
|
"tar-fs": "^2.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -20183,9 +20183,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mocha-pod": {
|
"mocha-pod": {
|
||||||
"version": "0.8.0",
|
"version": "0.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/mocha-pod/-/mocha-pod-0.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/mocha-pod/-/mocha-pod-0.9.0.tgz",
|
||||||
"integrity": "sha512-0jhPpQMWCduiEFFFPrWWdKonwmyC6TFwgZEo7G/JhpIsmmfQm2cZGpoJ2HfUCXT1bcOuinSUPI8cweG+1fbbhw==",
|
"integrity": "sha512-km2XTNEbxjxrbq3P5XoGm1kHBszQJZXEg0OXMkN/ZTG0g5mKWX6RQPNlfp6m9lRgRvxwdAVVWIPNAo+q/CsbkQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@balena/compose": "^2.1.0",
|
"@balena/compose": "^2.1.0",
|
||||||
@ -20195,7 +20195,7 @@
|
|||||||
"dockerode": "^3.3.2",
|
"dockerode": "^3.3.2",
|
||||||
"fast-glob": "^3.2.11",
|
"fast-glob": "^3.2.11",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"nanoid": "^3.3.4",
|
"nanoid": "3.3.4",
|
||||||
"tar-fs": "^2.1.1"
|
"tar-fs": "^2.1.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -107,7 +107,7 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"memoizee": "^0.4.14",
|
"memoizee": "^0.4.14",
|
||||||
"mocha": "^8.3.2",
|
"mocha": "^8.3.2",
|
||||||
"mocha-pod": "^0.8.0",
|
"mocha-pod": "^0.9.0",
|
||||||
"mock-fs": "^4.14.0",
|
"mock-fs": "^4.14.0",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"network-checker": "^0.1.1",
|
"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 { promises as fs } from 'fs';
|
||||||
import { stripIndent } from 'common-tags';
|
import { stripIndent } from 'common-tags';
|
||||||
import { SinonStub, stub } from 'sinon';
|
|
||||||
import { expect } from 'chai';
|
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';
|
import { Extlinux } from '~/src/config/backends/extlinux';
|
||||||
|
|
||||||
describe('Extlinux Configuration', () => {
|
describe('config/extlinux', () => {
|
||||||
const backend = new 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', () => {
|
TIMEOUT 30
|
||||||
const text = stripIndent`\
|
MENU TITLE Boot Options
|
||||||
DEFAULT primary
|
LABEL primary
|
||||||
# CommentExtlinux files
|
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
|
await expect(extLinux.getBootConfig()).to.eventually.deep.equal({
|
||||||
MENU TITLE Boot Options
|
fdt: '/boot/mycustomdtb.dtb',
|
||||||
LABEL primary
|
isolcpus: '3',
|
||||||
MENU LABEL primary Image
|
});
|
||||||
LINUX /Image
|
|
||||||
FDT /boot/mycustomdtb.dtb
|
|
||||||
APPEND \${cbootargs} \${resin_kernel_root} ro rootwait\
|
|
||||||
`;
|
|
||||||
|
|
||||||
// @ts-expect-error accessing private method
|
await tfs.restore();
|
||||||
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');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse multiple service entries', () => {
|
it('should parse multiple service entries', async () => {
|
||||||
const text = stripIndent`\
|
const tfs = await testfs({
|
||||||
DEFAULT primary
|
[hostUtils.pathOnBoot('extlinux/extlinux.conf')]: stripIndent`
|
||||||
# Comment
|
DEFAULT primary
|
||||||
|
# Comment
|
||||||
|
|
||||||
TIMEOUT 30
|
TIMEOUT 30
|
||||||
MENU TITLE Boot Options
|
MENU TITLE Boot Options
|
||||||
LABEL primary
|
LABEL primary
|
||||||
LINUX test1
|
LINUX test1
|
||||||
FDT /boot/mycustomdtb.dtb
|
FDT /boot/mycustomdtb.dtb
|
||||||
APPEND test2
|
APPEND test2
|
||||||
LABEL secondary
|
LABEL secondary
|
||||||
LINUX test3
|
LINUX test3
|
||||||
FDT /boot/mycustomdtb.dtb
|
FDT /boot/mycustomdtb.dtb
|
||||||
APPEND test4\
|
APPEND test4\
|
||||||
`;
|
`,
|
||||||
|
}).enable();
|
||||||
|
const extLinux = new Extlinux();
|
||||||
|
|
||||||
// @ts-expect-error accessing private method
|
await expect(extLinux.getBootConfig()).to.eventually.deep.equal({
|
||||||
const parsed = Extlinux.parseExtlinuxFile(text);
|
fdt: '/boot/mycustomdtb.dtb',
|
||||||
expect(parsed.labels).to.have.property('primary').that.deep.equals({
|
|
||||||
LINUX: 'test1',
|
|
||||||
FDT: '/boot/mycustomdtb.dtb',
|
|
||||||
APPEND: 'test2',
|
|
||||||
});
|
});
|
||||||
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 () => {
|
await tfs.restore();
|
||||||
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');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('only matches supported devices', async () => {
|
it('only matches supported devices', async () => {
|
||||||
|
const extLinux = new Extlinux();
|
||||||
for (const { deviceType, metaRelease, supported } of MATCH_TESTS) {
|
for (const { deviceType, metaRelease, supported } of MATCH_TESTS) {
|
||||||
await expect(
|
await expect(
|
||||||
backend.matches(deviceType, metaRelease),
|
extLinux.matches(deviceType, metaRelease),
|
||||||
).to.eventually.equal(supported);
|
).to.eventually.equal(supported);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors when cannot find extlinux.conf', async () => {
|
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 readFile to reject much like if the file didn't exist
|
||||||
stub(fs, 'readFile').rejects();
|
await expect(extLinux.getBootConfig()).to.eventually.be.rejectedWith(
|
||||||
await expect(backend.getBootConfig()).to.eventually.be.rejectedWith(
|
|
||||||
'Could not find extlinux file. Device is possibly bricked',
|
'Could not find extlinux file. Device is possibly bricked',
|
||||||
);
|
);
|
||||||
// Restore stub
|
|
||||||
(fs.readFile as SinonStub).restore();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws error for malformed extlinux.conf', async () => {
|
it('throws error for malformed extlinux.conf', async () => {
|
||||||
for (const badConfig of MALFORMED_CONFIGS) {
|
for (const badConfig of MALFORMED_CONFIGS) {
|
||||||
// Stub bad config
|
const tfs = await testfs({
|
||||||
stub(fs, 'readFile').resolves(badConfig.contents);
|
[hostUtils.pathOnBoot('extlinux/extlinux.conf')]: badConfig,
|
||||||
|
}).enable();
|
||||||
|
const extLinux = new Extlinux();
|
||||||
|
|
||||||
// Expect correct rejection from the given bad config
|
// Expect correct rejection from the given bad config
|
||||||
try {
|
await expect(extLinux.getBootConfig()).to.be.rejectedWith(
|
||||||
await backend.getBootConfig();
|
badConfig.reason,
|
||||||
} catch (e: any) {
|
);
|
||||||
expect(e.message).to.equal(badConfig.reason);
|
|
||||||
}
|
await tfs.restore();
|
||||||
// Restore stub
|
|
||||||
(fs.readFile as SinonStub).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 () => {
|
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',
|
fdt: '/boot/mycustomdtb.dtb',
|
||||||
isolcpus: '2',
|
isolcpus: '2',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(fsUtils.writeAndSyncFile).to.be.calledWith(
|
await expect(
|
||||||
'test/data/mnt/boot/extlinux/extlinux.conf',
|
fs.readFile(hostUtils.pathOnBoot('extlinux/extlinux.conf'), 'utf8'),
|
||||||
|
).to.eventually.equal(
|
||||||
stripIndent`
|
stripIndent`
|
||||||
DEFAULT primary
|
DEFAULT primary
|
||||||
TIMEOUT 30
|
TIMEOUT 30
|
||||||
@ -202,11 +129,11 @@ describe('Extlinux Configuration', () => {
|
|||||||
` + '\n', // add newline because stripIndent trims last newline
|
` + '\n', // add newline because stripIndent trims last newline
|
||||||
);
|
);
|
||||||
|
|
||||||
// Restore stubs
|
await tfs.restore();
|
||||||
(fsUtils.writeAndSyncFile as SinonStub).restore();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('only allows supported configuration options', () => {
|
it('only allows supported configuration options', () => {
|
||||||
|
const extLinux = new Extlinux();
|
||||||
[
|
[
|
||||||
{ configName: 'isolcpus', supported: true },
|
{ configName: 'isolcpus', supported: true },
|
||||||
{ configName: 'fdt', supported: true },
|
{ configName: 'fdt', supported: true },
|
||||||
@ -214,26 +141,28 @@ describe('Extlinux Configuration', () => {
|
|||||||
{ configName: 'ro', supported: false }, // not allowed to configure
|
{ configName: 'ro', supported: false }, // not allowed to configure
|
||||||
{ configName: 'rootwait', supported: false }, // not allowed to configure
|
{ configName: 'rootwait', supported: false }, // not allowed to configure
|
||||||
].forEach(({ configName, supported }) =>
|
].forEach(({ configName, supported }) =>
|
||||||
expect(backend.isSupportedConfig(configName)).to.equal(supported),
|
expect(extLinux.isSupportedConfig(configName)).to.equal(supported),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('correctly detects boot config variables', () => {
|
it('correctly detects boot config variables', () => {
|
||||||
|
const extLinux = new Extlinux();
|
||||||
[
|
[
|
||||||
{ config: 'HOST_EXTLINUX_isolcpus', valid: true },
|
{ config: 'HOST_EXTLINUX_isolcpus', valid: true },
|
||||||
{ config: 'HOST_EXTLINUX_fdt', valid: true },
|
{ config: 'HOST_EXTLINUX_fdt', valid: true },
|
||||||
{ config: 'HOST_EXTLINUX_rootwait', valid: true },
|
{ config: 'HOST_EXTLINUX_rootwait', valid: true },
|
||||||
{ config: 'HOST_EXTLINUX_5', valid: true },
|
{ config: 'HOST_EXTLINUX_5', valid: true },
|
||||||
// TO-DO: { config: 'HOST_EXTLINUX', valid: false },
|
// TODO: { config: 'HOST_EXTLINUX', valid: false },
|
||||||
// TO-DO: { config: 'HOST_EXTLINUX_', valid: false },
|
// TODO: { config: 'HOST_EXTLINUX_', valid: false },
|
||||||
{ config: 'DEVICE_EXTLINUX_isolcpus', valid: false },
|
{ config: 'DEVICE_EXTLINUX_isolcpus', valid: false },
|
||||||
{ config: 'isolcpus', valid: false },
|
{ config: 'isolcpus', valid: false },
|
||||||
].forEach(({ config, valid }) =>
|
].forEach(({ config, valid }) =>
|
||||||
expect(backend.isBootConfigVar(config)).to.equal(valid),
|
expect(extLinux.isBootConfigVar(config)).to.equal(valid),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('converts variable to backend formatted name', () => {
|
it('converts variable to backend formatted name', () => {
|
||||||
|
const extLinux = new Extlinux();
|
||||||
[
|
[
|
||||||
{ input: 'HOST_EXTLINUX_isolcpus', output: 'isolcpus' },
|
{ input: 'HOST_EXTLINUX_isolcpus', output: 'isolcpus' },
|
||||||
{ input: 'HOST_EXTLINUX_fdt', output: 'fdt' },
|
{ input: 'HOST_EXTLINUX_fdt', output: 'fdt' },
|
||||||
@ -243,20 +172,22 @@ describe('Extlinux Configuration', () => {
|
|||||||
{ input: 'HOST_EXTLINUX_ ', output: ' ' },
|
{ input: 'HOST_EXTLINUX_ ', output: ' ' },
|
||||||
{ input: 'ROOT_EXTLINUX_isolcpus', output: 'ROOT_EXTLINUX_isolcpus' },
|
{ input: 'ROOT_EXTLINUX_isolcpus', output: 'ROOT_EXTLINUX_isolcpus' },
|
||||||
].forEach(({ input, output }) =>
|
].forEach(({ input, output }) =>
|
||||||
expect(backend.processConfigVarName(input)).to.equal(output),
|
expect(extLinux.processConfigVarName(input)).to.equal(output),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('normalizes variable value', () => {
|
it('normalizes variable value', () => {
|
||||||
|
const extLinux = new Extlinux();
|
||||||
[{ input: { key: 'key', value: 'value' }, output: 'value' }].forEach(
|
[{ input: { key: 'key', value: 'value' }, output: 'value' }].forEach(
|
||||||
({ input, output }) =>
|
({ input, output }) =>
|
||||||
expect(backend.processConfigVarValue(input.key, input.value)).to.equal(
|
expect(extLinux.processConfigVarValue(input.key, input.value)).to.equal(
|
||||||
output,
|
output,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the environment name for config variable', () => {
|
it('returns the environment name for config variable', () => {
|
||||||
|
const extLinux = new Extlinux();
|
||||||
[
|
[
|
||||||
{ input: 'isolcpus', output: 'HOST_EXTLINUX_isolcpus' },
|
{ input: 'isolcpus', output: 'HOST_EXTLINUX_isolcpus' },
|
||||||
{ input: 'fdt', output: 'HOST_EXTLINUX_fdt' },
|
{ input: 'fdt', output: 'HOST_EXTLINUX_fdt' },
|
||||||
@ -264,7 +195,7 @@ describe('Extlinux Configuration', () => {
|
|||||||
{ input: '', output: 'HOST_EXTLINUX_' },
|
{ input: '', output: 'HOST_EXTLINUX_' },
|
||||||
{ input: '5', output: 'HOST_EXTLINUX_5' },
|
{ input: '5', output: 'HOST_EXTLINUX_5' },
|
||||||
].forEach(({ input, output }) =>
|
].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 { stripIndent } from 'common-tags';
|
||||||
import { SinonStub, spy, stub } from 'sinon';
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
import { testfs } from 'mocha-pod';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
|
||||||
import * as fsUtils from '~/lib/fs-utils';
|
import * as hostUtils from '~/lib/host-utils';
|
||||||
import Log from '~/lib/supervisor-console';
|
import log from '~/lib/supervisor-console';
|
||||||
import { ExtraUEnv } from '~/src/config/backends/extra-uEnv';
|
import { ExtraUEnv } from '~/src/config/backends/extra-uEnv';
|
||||||
|
|
||||||
describe('extra_uEnv Configuration', () => {
|
describe('config/extra-uEnv', () => {
|
||||||
const backend = new ExtraUEnv();
|
const backend = new ExtraUEnv();
|
||||||
let readFileStub: SinonStub;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
readFileStub = stub(fs, 'readFile');
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
readFileStub.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse extra_uEnv string', () => {
|
it('should parse extra_uEnv string', () => {
|
||||||
const fileContents = stripIndent`\
|
const fileContents = stripIndent`\
|
||||||
@ -35,89 +26,112 @@ describe('extra_uEnv Configuration', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should only parse supported configuration options from bootConfigPath', async () => {
|
it('should only parse supported configuration options from bootConfigPath', async () => {
|
||||||
readFileStub.resolves(stripIndent`\
|
let tfs = await testfs({
|
||||||
custom_fdt_file=mycustom.dtb
|
[hostUtils.pathOnBoot(`extra_uEnv.txt`)]: stripIndent`
|
||||||
extra_os_cmdline=isolcpus=3,4
|
custom_fdt_file=mycustom.dtb
|
||||||
`);
|
extra_os_cmdline=isolcpus=3,4
|
||||||
|
`,
|
||||||
|
}).enable();
|
||||||
|
|
||||||
await expect(backend.getBootConfig()).to.eventually.deep.equal({
|
await expect(backend.getBootConfig()).to.eventually.deep.equal({
|
||||||
fdt: 'mycustom.dtb',
|
fdt: 'mycustom.dtb',
|
||||||
isolcpus: '3,4',
|
isolcpus: '3,4',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await tfs.restore();
|
||||||
|
|
||||||
// Add other options that will get filtered out because they aren't supported
|
// Add other options that will get filtered out because they aren't supported
|
||||||
readFileStub.resolves(stripIndent`\
|
tfs = await testfs({
|
||||||
custom_fdt_file=mycustom.dtb
|
[hostUtils.pathOnBoot(`extra_uEnv.txt`)]: stripIndent`
|
||||||
extra_os_cmdline=isolcpus=3,4 console=tty0 splash
|
custom_fdt_file=mycustom.dtb
|
||||||
`);
|
extra_os_cmdline=isolcpus=3,4 console=tty0 splash
|
||||||
|
`,
|
||||||
|
}).enable();
|
||||||
|
|
||||||
await expect(backend.getBootConfig()).to.eventually.deep.equal({
|
await expect(backend.getBootConfig()).to.eventually.deep.equal({
|
||||||
fdt: 'mycustom.dtb',
|
fdt: 'mycustom.dtb',
|
||||||
isolcpus: '3,4',
|
isolcpus: '3,4',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stub with no supported values
|
await tfs.restore();
|
||||||
readFileStub.resolves(stripIndent`\
|
|
||||||
fdt=something_else
|
|
||||||
isolcpus
|
|
||||||
123.12=5
|
|
||||||
`);
|
|
||||||
|
|
||||||
|
// 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 expect(backend.getBootConfig()).to.eventually.deep.equal({});
|
||||||
|
|
||||||
|
await tfs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('only matches supported devices', async () => {
|
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) {
|
for (const device of MATCH_TESTS) {
|
||||||
// Test device that has extra_uEnv.txt
|
// Test device that has extra_uEnv.txt
|
||||||
let hasExtraUEnv = true;
|
|
||||||
existsStub.resolves(hasExtraUEnv);
|
|
||||||
await expect(backend.matches(device.type)).to.eventually.equal(
|
await expect(backend.matches(device.type)).to.eventually.equal(
|
||||||
device.supported && hasExtraUEnv,
|
device.supported,
|
||||||
);
|
|
||||||
// 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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
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 () => {
|
it('errors when cannot find extra_uEnv.txt', async () => {
|
||||||
// Stub readFile to reject much like if the file didn't exist
|
// The file no longer exists
|
||||||
readFileStub.rejects();
|
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(
|
await expect(backend.getBootConfig()).to.eventually.be.rejectedWith(
|
||||||
'Could not find extra_uEnv file. Device is possibly bricked',
|
'Could not find extra_uEnv file. Device is possibly bricked',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('logs warning for malformed extra_uEnv.txt', async () => {
|
it('logs warning for malformed extra_uEnv.txt', async () => {
|
||||||
spy(Log, 'warn');
|
|
||||||
for (const badConfig of MALFORMED_CONFIGS) {
|
for (const badConfig of MALFORMED_CONFIGS) {
|
||||||
// Stub bad config
|
// Setup the environment with a bad config
|
||||||
readFileStub.resolves(badConfig.contents);
|
const tfs = await testfs({
|
||||||
|
[hostUtils.pathOnBoot(`extra_uEnv.txt`)]: badConfig.contents,
|
||||||
|
}).enable();
|
||||||
|
|
||||||
// Expect warning log from the given bad config
|
// Expect warning log from the given bad config
|
||||||
await backend.getBootConfig();
|
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 () => {
|
it('sets new config values', async () => {
|
||||||
stub(fsUtils, 'writeAndSyncFile').resolves();
|
const tfs = await testfs({
|
||||||
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)
|
||||||
// This config contains a value set from something else
|
// So after setting new values this unsupported/not set value should be gone
|
||||||
// We to make sure the Supervisor is enforcing the source of truth (the cloud)
|
[hostUtils.pathOnBoot(`extra_uEnv.txt`)]: stripIndent`
|
||||||
// So after setting new values this unsupported/not set value should be gone
|
extra_os_cmdline=rootwait isolcpus=3,4
|
||||||
readFileStub.resolves(stripIndent`\
|
other_service=set_this_value
|
||||||
extra_os_cmdline=rootwait isolcpus=3,4
|
`,
|
||||||
other_service=set_this_value
|
}).enable();
|
||||||
`);
|
|
||||||
|
|
||||||
// Sets config with mix of supported and not supported values
|
// Sets config with mix of supported and not supported values
|
||||||
await backend.setBootConfig({
|
await backend.setBootConfig({
|
||||||
@ -126,24 +140,21 @@ describe('extra_uEnv Configuration', () => {
|
|||||||
console: 'tty0', // not supported so won't be set
|
console: 'tty0', // not supported so won't be set
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(fsUtils.writeAndSyncFile).to.be.calledWith(
|
// Confirm that the file was written correctly
|
||||||
'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\n',
|
'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 }',
|
'Not setting unsupported value: { console: tty0 }',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Restore stubs
|
await tfs.restore();
|
||||||
(fsUtils.writeAndSyncFile as SinonStub).restore();
|
|
||||||
logWarningStub.restore();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets new config values containing collections', async () => {
|
it('sets new config values containing collections', async () => {
|
||||||
stub(fsUtils, 'writeAndSyncFile').resolves();
|
|
||||||
const logWarningStub = spy(Log, 'warn');
|
|
||||||
|
|
||||||
// @ts-expect-error accessing private value
|
// @ts-expect-error accessing private value
|
||||||
const previousSupportedConfigs = ExtraUEnv.supportedConfigs;
|
const previousSupportedConfigs = ExtraUEnv.supportedConfigs;
|
||||||
// Stub isSupportedConfig so we can confirm collections work
|
// Stub isSupportedConfig so we can confirm collections work
|
||||||
@ -155,6 +166,12 @@ describe('extra_uEnv Configuration', () => {
|
|||||||
splash: { key: 'extra_os_cmdline', collection: true },
|
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
|
// Set config again
|
||||||
await backend.setBootConfig({
|
await backend.setBootConfig({
|
||||||
fdt: '/boot/mycustomdtb.dtb',
|
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
|
splash: '', // collection entry so should be concatted to other collections of this entry
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(fsUtils.writeAndSyncFile).to.be.calledWith(
|
await expect(
|
||||||
'test/data/mnt/boot/extra_uEnv.txt',
|
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',
|
'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
|
// @ts-expect-error accessing private value
|
||||||
ExtraUEnv.supportedConfigs = previousSupportedConfigs;
|
ExtraUEnv.supportedConfigs = previousSupportedConfigs;
|
||||||
});
|
|
||||||
|
|
||||||
it('only allows supported configuration options', () => {
|
await tfs.restore();
|
||||||
[
|
|
||||||
{ 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),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
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 { expect } from 'chai';
|
||||||
|
import { testfs } from 'mocha-pod';
|
||||||
|
|
||||||
import * as config from '~/src/config';
|
|
||||||
import * as configUtils from '~/src/config/utils';
|
import * as configUtils from '~/src/config/utils';
|
||||||
import { ExtraUEnv } from '~/src/config/backends/extra-uEnv';
|
import { ExtraUEnv } from '~/src/config/backends/extra-uEnv';
|
||||||
import { Extlinux } from '~/src/config/backends/extlinux';
|
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 { SplashImage } from '~/src/config/backends/splash-image';
|
||||||
import { ConfigBackend } from '~/src/config/backends/backend';
|
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 () => {
|
it('gets list of supported backends', async () => {
|
||||||
// Stub so that we get an array containing only config-txt backend
|
const tFs = await testfs({
|
||||||
const configStub = stub(config, 'get').resolves('raspberry');
|
// This is only needed so config.get doesn't fail
|
||||||
|
[hostUtils.pathOnBoot('config.json')]: JSON.stringify({
|
||||||
|
deviceType: 'raspberrypi4',
|
||||||
|
}),
|
||||||
|
}).enable();
|
||||||
|
|
||||||
// Get list of backends
|
// Get list of backends
|
||||||
const devices = await configUtils.getSupportedBackends();
|
const devices = await configUtils.getSupportedBackends();
|
||||||
expect(devices.length).to.equal(2);
|
expect(devices.length).to.equal(2);
|
||||||
expect(devices[0].constructor.name).to.equal('ConfigTxt');
|
expect(devices[0].constructor.name).to.equal('ConfigTxt');
|
||||||
expect(devices[1].constructor.name).to.equal('SplashImage');
|
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
|
// 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
|
// add a test that we get more then 1 backend for that device
|
||||||
});
|
});
|
||||||
|
|
||||||
it('transforms environment variables to boot configs', () => {
|
it('transforms environment variables to boot configs', () => {
|
||||||
_.forEach(CONFIGS, (configObj: any, key: string) => {
|
keys(CONFIGS).forEach((key) => {
|
||||||
expect(
|
expect(
|
||||||
configUtils.envToBootConfig(BACKENDS[key], configObj.envVars),
|
configUtils.envToBootConfig(BACKENDS[key], CONFIGS[key].envVars),
|
||||||
).to.deep.equal(configObj.bootConfig);
|
).to.deep.equal(CONFIGS[key].bootConfig);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('transforms boot configs to environment variables', () => {
|
it('transforms boot configs to environment variables', () => {
|
||||||
_.forEach(CONFIGS, (configObj: any, key: string) => {
|
keys(CONFIGS).forEach((key) => {
|
||||||
expect(
|
expect(
|
||||||
configUtils.bootConfigToEnv(BACKENDS[key], configObj.bootConfig),
|
configUtils.bootConfigToEnv(BACKENDS[key], CONFIGS[key].bootConfig),
|
||||||
).to.deep.equal(configObj.envVars);
|
).to.deep.equal(CONFIGS[key].envVars);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
@ -1,7 +1,7 @@
|
|||||||
import { stripIndent } from 'common-tags';
|
import { stripIndent } from 'common-tags';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import * as path from 'path';
|
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 { expect } from 'chai';
|
||||||
|
|
||||||
import * as deviceConfig from '~/src/device-config';
|
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 { ConfigFs } from '~/src/config/backends/config-fs';
|
||||||
import { SplashImage } from '~/src/config/backends/splash-image';
|
import { SplashImage } from '~/src/config/backends/splash-image';
|
||||||
import * as constants from '~/lib/constants';
|
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 { testfs } from 'mocha-pod';
|
||||||
import mock = require('mock-fs');
|
|
||||||
|
|
||||||
const extlinuxBackend = new Extlinux();
|
const extlinuxBackend = new Extlinux();
|
||||||
const configTxtBackend = new ConfigTxt();
|
const configTxtBackend = new ConfigTxt();
|
||||||
@ -33,7 +30,7 @@ describe('device-config', () => {
|
|||||||
constants.rootMountPoint,
|
constants.rootMountPoint,
|
||||||
constants.bootMountPoint,
|
constants.bootMountPoint,
|
||||||
);
|
);
|
||||||
const configJson = 'test/data/config.json';
|
const configJson = path.join(bootMountPoint, 'config.json');
|
||||||
const configFsJson = path.join(bootMountPoint, 'configfs.json');
|
const configFsJson = path.join(bootMountPoint, 'configfs.json');
|
||||||
const configTxt = path.join(bootMountPoint, 'config.txt');
|
const configTxt = path.join(bootMountPoint, 'config.txt');
|
||||||
const deviceTypeJson = path.join(bootMountPoint, 'device-type.json');
|
const deviceTypeJson = path.join(bootMountPoint, 'device-type.json');
|
||||||
@ -42,29 +39,14 @@ describe('device-config', () => {
|
|||||||
let logSpy: SinonSpy;
|
let logSpy: SinonSpy;
|
||||||
|
|
||||||
before(async () => {
|
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');
|
logSpy = spy(logger, 'logSystemMessage');
|
||||||
await prepare();
|
|
||||||
|
|
||||||
// clear memoized data from config
|
|
||||||
fnSchema.deviceType.clear();
|
|
||||||
fnSchema.deviceArch.clear();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
after(() => {
|
after(() => {
|
||||||
restore();
|
logSpy.restore();
|
||||||
// clear memoized data from config
|
|
||||||
fnSchema.deviceType.clear();
|
|
||||||
fnSchema.deviceArch.clear();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
// Restore stubs
|
|
||||||
logSpy.resetHistory();
|
logSpy.resetHistory();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -118,11 +100,10 @@ describe('device-config', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('config.txt', () => {
|
describe('config.txt', () => {
|
||||||
const mockFs = () => {
|
const tFs = testfs({
|
||||||
mock({
|
// This is only needed so config.get doesn't fail
|
||||||
// This is only needed so config.get doesn't fail
|
[configJson]: JSON.stringify({ deviceType: 'fincm3' }),
|
||||||
[configJson]: JSON.stringify({ deviceType: 'fincm3' }),
|
[configTxt]: stripIndent`
|
||||||
[configTxt]: stripIndent`
|
|
||||||
enable_uart=1
|
enable_uart=1
|
||||||
dtparam=i2c_arm=on
|
dtparam=i2c_arm=on
|
||||||
dtparam=spi=on
|
dtparam=spi=on
|
||||||
@ -130,29 +111,24 @@ describe('device-config', () => {
|
|||||||
avoid_warnings=1
|
avoid_warnings=1
|
||||||
dtparam=audio=on
|
dtparam=audio=on
|
||||||
gpu_mem=16`,
|
gpu_mem=16`,
|
||||||
[osRelease]: stripIndent`
|
[osRelease]: stripIndent`
|
||||||
PRETTY_NAME="balenaOS 2.88.5+rev1"
|
PRETTY_NAME="balenaOS 2.88.5+rev1"
|
||||||
META_BALENA_VERSION="2.88.5"
|
META_BALENA_VERSION="2.88.5"
|
||||||
VARIANT_ID="dev"`,
|
VARIANT_ID="dev"`,
|
||||||
[deviceTypeJson]: JSON.stringify({
|
[deviceTypeJson]: JSON.stringify({
|
||||||
slug: 'fincm3',
|
slug: 'fincm3',
|
||||||
arch: 'armv7hf',
|
arch: 'armv7hf',
|
||||||
}),
|
}),
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const unmockFs = () => {
|
|
||||||
mock.restore();
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockFs();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
beforeEach(async () => {
|
||||||
|
await tFs.enable();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
// Reset the state of the fs after each test to
|
// Reset the state of the fs after each test to
|
||||||
// prevent tests leaking into each other
|
// prevent tests leaking into each other
|
||||||
unmockFs();
|
await tFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('correctly parses a config.txt file', async () => {
|
it('correctly parses a config.txt file', async () => {
|
||||||
@ -337,20 +313,19 @@ describe('device-config', () => {
|
|||||||
describe('extlinux', () => {
|
describe('extlinux', () => {
|
||||||
const extlinuxConf = path.join(bootMountPoint, 'extlinux/extlinux.conf');
|
const extlinuxConf = path.join(bootMountPoint, 'extlinux/extlinux.conf');
|
||||||
|
|
||||||
const mockFs = () => {
|
const tFs = testfs({
|
||||||
mock({
|
// This is only needed so config.get doesn't fail
|
||||||
// This is only needed so config.get doesn't fail
|
[configJson]: JSON.stringify({}),
|
||||||
[configJson]: JSON.stringify({}),
|
[osRelease]: stripIndent`
|
||||||
[osRelease]: stripIndent`
|
|
||||||
PRETTY_NAME="balenaOS 2.88.5+rev1"
|
PRETTY_NAME="balenaOS 2.88.5+rev1"
|
||||||
META_BALENA_VERSION="2.88.5"
|
META_BALENA_VERSION="2.88.5"
|
||||||
VARIANT_ID="dev"
|
VARIANT_ID="dev"
|
||||||
`,
|
`,
|
||||||
[deviceTypeJson]: JSON.stringify({
|
[deviceTypeJson]: JSON.stringify({
|
||||||
slug: 'fincm3',
|
slug: 'fincm3',
|
||||||
arch: 'armv7hf',
|
arch: 'armv7hf',
|
||||||
}),
|
}),
|
||||||
[extlinuxConf]: stripIndent`
|
[extlinuxConf]: stripIndent`
|
||||||
DEFAULT primary
|
DEFAULT primary
|
||||||
TIMEOUT 30
|
TIMEOUT 30
|
||||||
MENU TITLE Boot Options
|
MENU TITLE Boot Options
|
||||||
@ -359,35 +334,24 @@ describe('device-config', () => {
|
|||||||
LINUX /Image
|
LINUX /Image
|
||||||
APPEND \${cbootargs} \${resin_kernel_root} ro rootwait
|
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
|
// Reset the state of the fs after each test to
|
||||||
// prevent tests leaking into each other
|
// prevent tests leaking into each other
|
||||||
unmockFs();
|
await tFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly write to extlinux.conf files', async () => {
|
it('should correctly write to extlinux.conf files', async () => {
|
||||||
const current = {};
|
|
||||||
const target = {
|
const target = {
|
||||||
HOST_EXTLINUX_isolcpus: '2',
|
HOST_EXTLINUX_isolcpus: '2',
|
||||||
HOST_EXTLINUX_fdt: '/boot/mycustomdtb.dtb',
|
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);
|
await deviceConfig.setBootConfig(extlinuxBackend, target);
|
||||||
expect(logSpy).to.be.calledTwice;
|
expect(logSpy).to.be.calledTwice;
|
||||||
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
|
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
|
||||||
@ -498,50 +462,44 @@ describe('device-config', () => {
|
|||||||
'sys/kernel/config/acpi/table',
|
'sys/kernel/config/acpi/table',
|
||||||
);
|
);
|
||||||
|
|
||||||
const mockFs = () => {
|
const tFs = testfs({
|
||||||
mock({
|
// This is only needed so config.get doesn't fail
|
||||||
// This is only needed so config.get doesn't fail
|
[configJson]: JSON.stringify({}),
|
||||||
[configJson]: JSON.stringify({}),
|
[osRelease]: stripIndent`
|
||||||
[osRelease]: stripIndent`
|
|
||||||
PRETTY_NAME="balenaOS 2.88.5+rev1"
|
PRETTY_NAME="balenaOS 2.88.5+rev1"
|
||||||
META_BALENA_VERSION="2.88.5"
|
META_BALENA_VERSION="2.88.5"
|
||||||
VARIANT_ID="dev"
|
VARIANT_ID="dev"
|
||||||
`,
|
`,
|
||||||
[configFsJson]: JSON.stringify({
|
[configFsJson]: JSON.stringify({
|
||||||
ssdt: ['spidev1.1'],
|
ssdt: ['spidev1.1'],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
[deviceTypeJson]: JSON.stringify({
|
[deviceTypeJson]: JSON.stringify({
|
||||||
slug: 'fincm3',
|
slug: 'fincm3',
|
||||||
arch: 'armv7hf',
|
arch: 'armv7hf',
|
||||||
}),
|
}),
|
||||||
[acpiTables]: {
|
[acpiTables]: {
|
||||||
'spidev1.0.aml': '',
|
'spidev1.0.aml': '',
|
||||||
'spidev1.1.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
|
// Reset the state of the fs after each test to
|
||||||
// prevent tests leaking into each other
|
// prevent tests leaking into each other
|
||||||
unmockFs();
|
await tFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly write to configfs.json files', async () => {
|
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 () => {
|
it('should correctly load the configfs.json file', async () => {
|
||||||
|
await configFsBackend.initialise();
|
||||||
|
|
||||||
stub(fsUtils, 'exec').resolves();
|
stub(fsUtils, 'exec').resolves();
|
||||||
await configFsBackend.initialise();
|
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');
|
expect(fsUtils.exec).to.be.calledWith('modprobe acpi_configfs');
|
||||||
|
|
||||||
// If the module performs this call, it's because all the prior checks succeeded
|
// If the module performs this call, it's because all the prior checks succeeded
|
||||||
expect(fsUtils.exec).to.be.calledWith(
|
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
|
// Restore stubs
|
||||||
@ -653,8 +618,8 @@ describe('device-config', () => {
|
|||||||
|
|
||||||
const splash = path.join(bootMountPoint, 'splash');
|
const splash = path.join(bootMountPoint, 'splash');
|
||||||
|
|
||||||
const mockFs = () => {
|
const mockFs = testfs(
|
||||||
mock({
|
{
|
||||||
// This is only needed so config.get doesn't fail
|
// This is only needed so config.get doesn't fail
|
||||||
[configJson]: JSON.stringify({}),
|
[configJson]: JSON.stringify({}),
|
||||||
[osRelease]: stripIndent`
|
[osRelease]: stripIndent`
|
||||||
@ -667,45 +632,33 @@ describe('device-config', () => {
|
|||||||
arch: 'aarch64',
|
arch: 'aarch64',
|
||||||
}),
|
}),
|
||||||
[splash]: {
|
[splash]: {
|
||||||
/* empty directory */
|
dummy: '', // to ensure the directory is created
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
};
|
{ cleanup: [`${splash}/*.png`] },
|
||||||
|
);
|
||||||
|
|
||||||
const unmockFs = () => {
|
beforeEach(async () => {
|
||||||
mock.restore();
|
await mockFs.enable();
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockFs();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(async () => {
|
||||||
unmockFs();
|
await mockFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly write to resin-logo.png', async () => {
|
it('should correctly write to resin-logo.png', async () => {
|
||||||
// Devices with balenaOS < 2.51 use resin-logo.png
|
// Devices with balenaOS < 2.51 use resin-logo.png
|
||||||
fs.writeFile(
|
// Devices with balenaOS >= 2.51 use balena-logo.png
|
||||||
path.join(splash, 'resin-logo.png'),
|
const tTfs = await testfs({
|
||||||
Buffer.from(defaultLogo, 'base64'),
|
[splash]: {
|
||||||
);
|
'resin-logo.png': Buffer.from(png, 'base64'),
|
||||||
|
},
|
||||||
|
}).enable();
|
||||||
|
|
||||||
const current = {};
|
|
||||||
const target = {
|
const target = {
|
||||||
HOST_SPLASH_image: png,
|
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);
|
await deviceConfig.setBootConfig(splashImageBackend, target);
|
||||||
|
|
||||||
expect(logSpy).to.be.calledTwice;
|
expect(logSpy).to.be.calledTwice;
|
||||||
@ -713,31 +666,22 @@ describe('device-config', () => {
|
|||||||
expect(
|
expect(
|
||||||
await fs.readFile(path.join(splash, 'resin-logo.png'), 'base64'),
|
await fs.readFile(path.join(splash, 'resin-logo.png'), 'base64'),
|
||||||
).to.equal(png);
|
).to.equal(png);
|
||||||
|
|
||||||
|
await tTfs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly write to balena-logo.png', async () => {
|
it('should correctly write to balena-logo.png', async () => {
|
||||||
// Devices with balenaOS >= 2.51 use balena-logo.png
|
// Devices with balenaOS >= 2.51 use balena-logo.png
|
||||||
fs.writeFile(
|
const tTfs = await testfs({
|
||||||
path.join(splash, 'balena-logo.png'),
|
[splash]: {
|
||||||
Buffer.from(defaultLogo, 'base64'),
|
'balena-logo.png': Buffer.from(png, 'base64'),
|
||||||
);
|
},
|
||||||
|
}).enable();
|
||||||
|
|
||||||
const current = {};
|
|
||||||
const target = {
|
const target = {
|
||||||
HOST_SPLASH_image: png,
|
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);
|
await deviceConfig.setBootConfig(splashImageBackend, target);
|
||||||
|
|
||||||
expect(logSpy).to.be.calledTwice;
|
expect(logSpy).to.be.calledTwice;
|
||||||
@ -745,6 +689,8 @@ describe('device-config', () => {
|
|||||||
expect(
|
expect(
|
||||||
await fs.readFile(path.join(splash, 'balena-logo.png'), 'base64'),
|
await fs.readFile(path.join(splash, 'balena-logo.png'), 'base64'),
|
||||||
).to.equal(png);
|
).to.equal(png);
|
||||||
|
|
||||||
|
await tTfs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly write to balena-logo.png if no default logo is found', async () => {
|
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 () => {
|
it('should correctly read the splash logo if different from the default', async () => {
|
||||||
stub(fs, 'readdir').resolves(['balena-logo.png'] as any);
|
const tTfs = await testfs({
|
||||||
|
[splash]: {
|
||||||
const readFileStub: SinonStub = stub(fs, 'readFile').resolves(
|
'balena-logo.png': Buffer.from(png, 'base64'),
|
||||||
Buffer.from(png, 'base64') as any,
|
'balena-logo-default.png': Buffer.from(defaultLogo, 'base64'),
|
||||||
);
|
},
|
||||||
readFileStub
|
}).enable();
|
||||||
.withArgs('test/data/mnt/boot/splash/balena-logo-default.png')
|
|
||||||
.resolves(Buffer.from(defaultLogo, 'base64') as any);
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await deviceConfig.getBootConfig(splashImageBackend),
|
await deviceConfig.getBootConfig(splashImageBackend),
|
||||||
).to.deep.equal({
|
).to.deep.equal({
|
||||||
HOST_SPLASH_image: uri,
|
HOST_SPLASH_image: uri,
|
||||||
});
|
});
|
||||||
expect(readFileStub).to.be.calledWith(
|
await tTfs.restore();
|
||||||
'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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getRequiredSteps', () => {
|
describe('getRequiredSteps', () => {
|
||||||
const splash = path.join(bootMountPoint, 'splash/balena-logo.png');
|
const splash = path.join(bootMountPoint, 'splash/balena-logo.png');
|
||||||
|
|
||||||
// TODO: something like this could be done as a fixture instead of
|
const tFs = testfs({
|
||||||
// doing the file initialisation on 00-init.ts
|
// This is only needed so config.get doesn't fail
|
||||||
const mockFs = () => {
|
[configJson]: JSON.stringify({}),
|
||||||
mock({
|
[configTxt]: stripIndent`
|
||||||
// This is only needed so config.get doesn't fail
|
|
||||||
[configJson]: JSON.stringify({}),
|
|
||||||
[configTxt]: stripIndent`
|
|
||||||
enable_uart=true
|
enable_uart=true
|
||||||
`,
|
`,
|
||||||
[osRelease]: stripIndent`
|
[osRelease]: stripIndent`
|
||||||
PRETTY_NAME="balenaOS 2.88.5+rev1"
|
PRETTY_NAME="balenaOS 2.88.5+rev1"
|
||||||
META_BALENA_VERSION="2.88.5"
|
META_BALENA_VERSION="2.88.5"
|
||||||
VARIANT_ID="dev"
|
VARIANT_ID="dev"
|
||||||
`,
|
`,
|
||||||
[deviceTypeJson]: JSON.stringify({
|
[deviceTypeJson]: JSON.stringify({
|
||||||
slug: 'raspberrypi4-64',
|
slug: 'raspberrypi4-64',
|
||||||
arch: 'aarch64',
|
arch: 'aarch64',
|
||||||
}),
|
}),
|
||||||
[splash]: Buffer.from(
|
[splash]: Buffer.from(
|
||||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
|
||||||
'base64',
|
'base64',
|
||||||
),
|
),
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const unmockFs = () => {
|
|
||||||
mock.restore();
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockFs();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
beforeEach(async () => {
|
||||||
unmockFs();
|
await tFs.enable();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await tFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns required steps to config.json first if any', async () => {
|
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
|
shift
|
||||||
else
|
else
|
||||||
printf 'ERROR: "--timeout" requires a non-empty option argument.\n' >&2
|
printf 'ERROR: "--timeout" requires a non-empty option argument.\n' >&2
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
shift
|
||||||
break
|
break
|
||||||
;;
|
;;
|
||||||
--) # End of all options.
|
--) # End of all options.
|
||||||
@ -24,6 +26,7 @@ while :; do
|
|||||||
;;
|
;;
|
||||||
-?*)
|
-?*)
|
||||||
printf 'WARN: Unknown option (ignored): %s\n' "$1" >&2
|
printf 'WARN: Unknown option (ignored): %s\n' "$1" >&2
|
||||||
|
shift
|
||||||
;;
|
;;
|
||||||
*) # Default case: If no more options then break out of the loop.
|
*) # Default case: If no more options then break out of the loop.
|
||||||
break ;;
|
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