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