balena-supervisor/test/integration/device-config.spec.ts
pipex 827f892c13 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
2022-11-14 11:12:52 -03:00

852 lines
23 KiB
TypeScript

import { stripIndent } from 'common-tags';
import { promises as fs } from 'fs';
import * as path from 'path';
import { SinonStub, stub, spy, SinonSpy } from 'sinon';
import { expect } from 'chai';
import * as deviceConfig from '~/src/device-config';
import * as fsUtils from '~/lib/fs-utils';
import * as logger from '~/src/logger';
import { Extlinux } from '~/src/config/backends/extlinux';
import { ConfigTxt } from '~/src/config/backends/config-txt';
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 { testfs } from 'mocha-pod';
const extlinuxBackend = new Extlinux();
const configTxtBackend = new ConfigTxt();
const odmdataBackend = new Odmdata();
const configFsBackend = new ConfigFs();
const splashImageBackend = new SplashImage();
// TODO: Since the getBootConfig method is simple enough
// these tests could probably be removed if each backend has its own
// test and the src/config/utils module is properly tested.
describe('device-config', () => {
const bootMountPoint = path.join(
constants.rootMountPoint,
constants.bootMountPoint,
);
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');
const osRelease = path.join(constants.rootMountPoint, '/etc/os-release');
let logSpy: SinonSpy;
before(async () => {
logSpy = spy(logger, 'logSystemMessage');
});
after(() => {
logSpy.restore();
});
afterEach(() => {
logSpy.resetHistory();
});
describe('formatConfigKeys', () => {
it('accepts RESIN_ and BALENA_ variables', async () => {
return expect(
deviceConfig.formatConfigKeys({
FOO: 'bar', // should be removed
BAR: 'baz', // should be removed
RESIN_SUPERVISOR_LOCAL_MODE: 'false', // any device
BALENA_SUPERVISOR_OVERRIDE_LOCK: 'false', // any device
BALENA_SUPERVISOR_POLL_INTERVAL: '100', // any device
RESIN_HOST_CONFIG_dtparam: 'i2c_arm=on","spi=on","audio=on', // config.txt backend
RESIN_HOST_CONFIGFS_ssdt: 'spidev1.0', // configfs backend
BALENA_HOST_EXTLINUX_isolcpus: '1,2,3', // extlinux backend
}),
).to.deep.equal({
SUPERVISOR_LOCAL_MODE: 'false',
SUPERVISOR_OVERRIDE_LOCK: 'false',
SUPERVISOR_POLL_INTERVAL: '100',
HOST_CONFIG_dtparam: 'i2c_arm=on","spi=on","audio=on',
HOST_CONFIGFS_ssdt: 'spidev1.0',
HOST_EXTLINUX_isolcpus: '1,2,3',
});
});
});
describe('getDefaults', () => {
it('returns default configuration values', () => {
const conf = deviceConfig.getDefaults();
return expect(conf).to.deep.equal({
HOST_FIREWALL_MODE: 'off',
HOST_DISCOVERABILITY: 'true',
SUPERVISOR_VPN_CONTROL: 'true',
SUPERVISOR_POLL_INTERVAL: '900000',
SUPERVISOR_LOCAL_MODE: 'false',
SUPERVISOR_CONNECTIVITY_CHECK: 'true',
SUPERVISOR_LOG_CONTROL: 'true',
SUPERVISOR_DELTA: 'false',
SUPERVISOR_DELTA_REQUEST_TIMEOUT: '59000',
SUPERVISOR_DELTA_APPLY_TIMEOUT: '0',
SUPERVISOR_DELTA_RETRY_COUNT: '30',
SUPERVISOR_DELTA_RETRY_INTERVAL: '10000',
SUPERVISOR_DELTA_VERSION: '2',
SUPERVISOR_INSTANT_UPDATE_TRIGGER: 'true',
SUPERVISOR_OVERRIDE_LOCK: 'false',
SUPERVISOR_PERSISTENT_LOGGING: 'false',
SUPERVISOR_HARDWARE_METRICS: 'true',
});
});
});
describe('config.txt', () => {
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
disable_splash=1
avoid_warnings=1
dtparam=audio=on
gpu_mem=16`,
[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',
}),
});
beforeEach(async () => {
await tFs.enable();
});
afterEach(async () => {
// Reset the state of the fs after each test to
// prevent tests leaking into each other
await tFs.restore();
});
it('correctly parses a config.txt file', async () => {
// Will try to parse /test/data/mnt/boot/config.txt
await expect(
deviceConfig.getBootConfig(configTxtBackend),
).to.eventually.deep.equal({
HOST_CONFIG_dtparam: '"i2c_arm=on","spi=on","audio=on"',
HOST_CONFIG_enable_uart: '1',
HOST_CONFIG_disable_splash: '1',
HOST_CONFIG_avoid_warnings: '1',
HOST_CONFIG_gpu_mem: '16',
});
// Update config.txt to include initramfs and array variables
await fs.writeFile(
configTxt,
stripIndent`
initramfs initramf.gz 0x00800000
dtparam=i2c=on
dtparam=audio=on
dtoverlay=ads7846
dtoverlay=lirc-rpi,gpio_out_pin=17,gpio_in_pin=13
foobar=baz`,
);
await expect(
deviceConfig.getBootConfig(configTxtBackend),
).to.eventually.deep.equal({
HOST_CONFIG_initramfs: 'initramf.gz 0x00800000',
HOST_CONFIG_dtparam: '"i2c=on","audio=on"',
HOST_CONFIG_dtoverlay:
'"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"',
HOST_CONFIG_foobar: 'baz',
});
});
it('does not allow setting forbidden keys', async () => {
const current = {
HOST_CONFIG_initramfs: 'initramf.gz 0x00800000',
HOST_CONFIG_dtparam: '"i2c=on","audio=on"',
HOST_CONFIG_dtoverlay:
'"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"',
HOST_CONFIG_foobar: 'baz',
};
// Create another target with only change being initramfs which is blacklisted
const target = {
...current,
HOST_CONFIG_initramfs: 'initramf.gz 0x00810000',
};
expect(() =>
// @ts-expect-error accessing private value
deviceConfig.bootConfigChangeRequired(
configTxtBackend,
current,
target,
),
).to.throw('Attempt to change blacklisted config value initramfs');
// Check if logs were called
expect(logSpy).to.be.calledOnce;
expect(logSpy).to.be.calledWith(
'Attempt to change blacklisted config value initramfs',
{
error: 'Attempt to change blacklisted config value initramfs',
},
'Apply boot config error',
);
});
it('does not try to change config.txt if it should not change', async () => {
const current = {
HOST_CONFIG_initramfs: 'initramf.gz 0x00800000',
HOST_CONFIG_dtparam: '"i2c=on","audio=on"',
HOST_CONFIG_dtoverlay:
'"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"',
HOST_CONFIG_foobar: 'baz',
};
const target = {
HOST_CONFIG_initramfs: 'initramf.gz 0x00800000',
HOST_CONFIG_dtparam: '"i2c=on","audio=on"',
HOST_CONFIG_dtoverlay:
'"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"',
HOST_CONFIG_foobar: 'baz',
};
expect(
// @ts-expect-error accessing private value
deviceConfig.bootConfigChangeRequired(
configTxtBackend,
current,
target,
),
).to.equal(false);
expect(logSpy).to.not.be.called;
});
it('writes the target config.txt', async () => {
const current = {
HOST_CONFIG_initramfs: 'initramf.gz 0x00800000',
HOST_CONFIG_dtparam: '"i2c=on","audio=on"',
HOST_CONFIG_dtoverlay:
'"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"',
HOST_CONFIG_foobar: 'baz',
};
const target = {
HOST_CONFIG_initramfs: 'initramf.gz 0x00800000',
HOST_CONFIG_dtparam: '"i2c=on","audio=off"',
HOST_CONFIG_dtoverlay: '"lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"',
HOST_CONFIG_foobar: 'bat',
HOST_CONFIG_foobaz: 'bar',
};
expect(
deviceConfig.bootConfigChangeRequired(
configTxtBackend,
current,
target,
'fincm3',
),
).to.equal(true);
await deviceConfig.setBootConfig(configTxtBackend, target);
expect(logSpy).to.be.calledTwice;
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
expect(await fs.readFile(configTxt, 'utf-8')).to.equal(
stripIndent`
initramfs initramf.gz 0x00800000
dtparam=i2c=on
dtparam=audio=off
dtoverlay=lirc-rpi,gpio_out_pin=17,gpio_in_pin=13
dtoverlay=balena-fin
foobar=bat
foobaz=bar
` + '\n', // add newline because stripIndent trims last newline
);
});
it('ensures required fields are written to config.txt', async () => {
const current = {
HOST_CONFIG_initramfs: 'initramf.gz 0x00800000',
HOST_CONFIG_dtparam: '"i2c=on","audio=on"',
HOST_CONFIG_dtoverlay:
'"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"',
HOST_CONFIG_foobar: 'baz',
};
const target = {
HOST_CONFIG_initramfs: 'initramf.gz 0x00800000',
HOST_CONFIG_dtparam: '"i2c=on","audio=off"',
HOST_CONFIG_dtoverlay: '"lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"',
HOST_CONFIG_foobar: 'bat',
HOST_CONFIG_foobaz: 'bar',
};
expect(
// @ts-expect-error accessing private value
deviceConfig.bootConfigChangeRequired(
configTxtBackend,
current,
target,
),
).to.equal(true);
await deviceConfig.setBootConfig(configTxtBackend, target);
expect(logSpy).to.be.calledTwice;
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
expect(await fs.readFile(configTxt, 'utf-8')).to.equal(
stripIndent`
initramfs initramf.gz 0x00800000
dtparam=i2c=on
dtparam=audio=off
dtoverlay=lirc-rpi,gpio_out_pin=17,gpio_in_pin=13
dtoverlay=balena-fin
foobar=bat
foobaz=bar
` + '\n', // add newline because stripIndent trims last newline
);
});
});
describe('extlinux', () => {
const extlinuxConf = path.join(bootMountPoint, 'extlinux/extlinux.conf');
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`
DEFAULT primary
TIMEOUT 30
MENU TITLE Boot Options
LABEL primary
MENU LABEL primary Image
LINUX /Image
APPEND \${cbootargs} \${resin_kernel_root} ro rootwait
`,
});
beforeEach(async () => {
await tFs.enable();
});
afterEach(async () => {
// Reset the state of the fs after each test to
// prevent tests leaking into each other
await tFs.restore();
});
it('should correctly write to extlinux.conf files', async () => {
const target = {
HOST_EXTLINUX_isolcpus: '2',
HOST_EXTLINUX_fdt: '/boot/mycustomdtb.dtb',
};
await deviceConfig.setBootConfig(extlinuxBackend, target);
expect(logSpy).to.be.calledTwice;
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
expect(await fs.readFile(extlinuxConf, 'utf-8')).to.equal(
stripIndent`
DEFAULT primary
TIMEOUT 30
MENU TITLE Boot Options
LABEL primary
MENU LABEL primary Image
LINUX /Image
APPEND \${cbootargs} \${resin_kernel_root} ro rootwait isolcpus=2
FDT /boot/mycustomdtb.dtb
` + '\n', // add newline because stripIndent trims last newline
);
});
});
describe('Balena fin', () => {
it('should always add the balena-fin dtoverlay', () => {
expect(configTxtBackend.ensureRequiredConfig('fincm3', {})).to.deep.equal(
{
dtoverlay: ['balena-fin'],
},
);
expect(
configTxtBackend.ensureRequiredConfig('fincm3', {
test: '123',
test2: ['123'],
test3: ['123', '234'],
}),
).to.deep.equal({
test: '123',
test2: ['123'],
test3: ['123', '234'],
dtoverlay: ['balena-fin'],
});
expect(
configTxtBackend.ensureRequiredConfig('fincm3', {
dtoverlay: 'test',
}),
).to.deep.equal({ dtoverlay: ['test', 'balena-fin'] });
expect(
configTxtBackend.ensureRequiredConfig('fincm3', {
dtoverlay: ['test'],
}),
).to.deep.equal({ dtoverlay: ['test', 'balena-fin'] });
});
it('should not cause a config change when the cloud does not specify the balena-fin overlay', () => {
expect(
deviceConfig.bootConfigChangeRequired(
configTxtBackend,
{ HOST_CONFIG_dtoverlay: '"test","balena-fin"' },
{ HOST_CONFIG_dtoverlay: '"test"' },
'fincm3',
),
).to.equal(false);
expect(
deviceConfig.bootConfigChangeRequired(
configTxtBackend,
{ HOST_CONFIG_dtoverlay: '"test","balena-fin"' },
{ HOST_CONFIG_dtoverlay: 'test' },
'fincm3',
),
).to.equal(false);
expect(
deviceConfig.bootConfigChangeRequired(
configTxtBackend,
{ HOST_CONFIG_dtoverlay: '"test","test2","balena-fin"' },
{ HOST_CONFIG_dtoverlay: '"test","test2"' },
'fincm3',
),
).to.equal(false);
});
});
describe('ODMDATA', () => {
it('requires change when target is different', () => {
expect(
deviceConfig.bootConfigChangeRequired(
odmdataBackend,
{ HOST_ODMDATA_configuration: '2' },
{ HOST_ODMDATA_configuration: '5' },
'jetson-tx2',
),
).to.equal(true);
});
it('requires change when no target is set', () => {
expect(
deviceConfig.bootConfigChangeRequired(
odmdataBackend,
{ HOST_ODMDATA_configuration: '2' },
{},
'jetson-tx2',
),
).to.equal(false);
});
});
describe('config-fs', () => {
const acpiTables = path.join(constants.rootMountPoint, 'boot/acpi-tables');
const sysKernelAcpiTable = path.join(
constants.rootMountPoint,
'sys/kernel/config/acpi/table',
);
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'],
}),
[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: '',
},
},
});
beforeEach(async () => {
await tFs.enable();
});
afterEach(async () => {
// Reset the state of the fs after each test to
// prevent tests leaking into each other
await tFs.restore();
});
it('should correctly write to configfs.json files', async () => {
const current = {};
const target = {
HOST_CONFIGFS_ssdt: 'spidev1.0',
};
expect(
deviceConfig.bootConfigChangeRequired(
configFsBackend,
current,
target,
'up-board',
),
).to.equal(true);
await deviceConfig.setBootConfig(configFsBackend, target);
expect(logSpy).to.be.calledTwice;
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
expect(await fs.readFile(configFsJson, 'utf-8')).to.equal(
'{"ssdt":["spidev1.0"]}',
);
});
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 /mnt/root/boot/acpi-tables/spidev1.1.aml > /mnt/root/sys/kernel/config/acpi/table/spidev1.1/aml',
);
// Restore stubs
(fsUtils.exec as SinonStub).restore();
});
it('requires change when target is different', () => {
expect(
deviceConfig.bootConfigChangeRequired(
configFsBackend,
{ HOST_CONFIGFS_ssdt: '' },
{ HOST_CONFIGFS_ssdt: 'spidev1.0' },
'up-board',
),
).to.equal(true);
expect(
deviceConfig.bootConfigChangeRequired(
configFsBackend,
{ HOST_CONFIGFS_ssdt: '' },
{ HOST_CONFIGFS_ssdt: '"spidev1.0"' },
'up-board',
),
).to.equal(true);
expect(
deviceConfig.bootConfigChangeRequired(
configFsBackend,
{ HOST_CONFIGFS_ssdt: '"spidev1.0"' },
{ HOST_CONFIGFS_ssdt: '"spidev1.0","spidev1.1"' },
'up-board',
),
).to.equal(true);
});
it('should not report change when target is equal to current', () => {
expect(
deviceConfig.bootConfigChangeRequired(
configFsBackend,
{ HOST_CONFIGFS_ssdt: '' },
{ HOST_CONFIGFS_ssdt: '' },
'up-board',
),
).to.equal(false);
expect(
deviceConfig.bootConfigChangeRequired(
configFsBackend,
{ HOST_CONFIGFS_ssdt: 'spidev1.0' },
{ HOST_CONFIGFS_ssdt: 'spidev1.0' },
'up-board',
),
).to.equal(false);
expect(
deviceConfig.bootConfigChangeRequired(
configFsBackend,
{ HOST_CONFIGFS_ssdt: 'spidev1.0' },
{ HOST_CONFIGFS_ssdt: '"spidev1.0"' },
'up-board',
),
).to.equal(false);
expect(
deviceConfig.bootConfigChangeRequired(
configFsBackend,
{ HOST_CONFIGFS_ssdt: '"spidev1.0"' },
{ HOST_CONFIGFS_ssdt: 'spidev1.0' },
'up-board',
),
).to.equal(false);
});
});
describe('splash config', () => {
const defaultLogo =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/wQDLA+84AAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==';
const png =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=';
const uri = `data:image/png;base64,${png}`;
const splash = path.join(bootMountPoint, 'splash');
const mockFs = 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: 'raspberrypi4-64',
arch: 'aarch64',
}),
[splash]: {
dummy: '', // to ensure the directory is created
},
},
{ cleanup: [`${splash}/*.png`] },
);
beforeEach(async () => {
await mockFs.enable();
});
afterEach(async () => {
await mockFs.restore();
});
it('should correctly write to resin-logo.png', async () => {
// Devices with balenaOS < 2.51 use resin-logo.png
// Devices with balenaOS >= 2.51 use balena-logo.png
const tTfs = await testfs({
[splash]: {
'resin-logo.png': Buffer.from(png, 'base64'),
},
}).enable();
const target = {
HOST_SPLASH_image: png,
};
await deviceConfig.setBootConfig(splashImageBackend, target);
expect(logSpy).to.be.calledTwice;
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
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
const tTfs = await testfs({
[splash]: {
'balena-logo.png': Buffer.from(png, 'base64'),
},
}).enable();
const target = {
HOST_SPLASH_image: png,
};
await deviceConfig.setBootConfig(splashImageBackend, target);
expect(logSpy).to.be.calledTwice;
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
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 () => {
// Devices with balenaOS >= 2.51 use balena-logo.png
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,
'raspberrypi3',
),
).to.equal(true);
await deviceConfig.setBootConfig(splashImageBackend, target);
expect(logSpy).to.be.calledTwice;
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
expect(
await fs.readFile(path.join(splash, 'balena-logo.png'), 'base64'),
).to.equal(png);
});
it('should correctly read the splash logo if different from the default', async () => {
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,
});
await tTfs.restore();
});
});
describe('getRequiredSteps', () => {
const splash = path.join(bootMountPoint, 'splash/balena-logo.png');
const tFs = testfs({
// This is only needed so config.get doesn't fail
[configJson]: JSON.stringify({}),
[configTxt]: stripIndent`
enable_uart=true
`,
[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',
),
});
beforeEach(async () => {
await tFs.enable();
});
afterEach(async () => {
await tFs.restore();
});
it('returns required steps to config.json first if any', async () => {
const steps = await deviceConfig.getRequiredSteps(
{
local: {
config: {
SUPERVISOR_POLL_INTERVAL: 900000,
HOST_CONFIG_enable_uart: true,
},
},
} as any,
{
local: {
config: {
SUPERVISOR_POLL_INTERVAL: 600000,
HOST_CONFIG_enable_uart: false,
},
},
} as any,
);
expect(steps.map((s) => s.action)).to.have.members([
// No reboot is required by this config change
'changeConfig',
'noop', // The noop has to be here since there are also changes from config backends
]);
});
it('sets the rebooot breadcrumb for config steps that require a reboot', async () => {
const steps = await deviceConfig.getRequiredSteps(
{
local: {
config: {
SUPERVISOR_POLL_INTERVAL: 900000,
SUPERVISOR_PERSISTENT_LOGGING: false,
},
},
} as any,
{
local: {
config: {
SUPERVISOR_POLL_INTERVAL: 600000,
SUPERVISOR_PERSISTENT_LOGGING: true,
},
},
} as any,
);
expect(steps.map((s) => s.action)).to.have.members([
'setRebootBreadcrumb',
'changeConfig',
'noop',
]);
});
it('returns required steps for backends if no steps are required for config.json', async () => {
const steps = await deviceConfig.getRequiredSteps(
{
local: {
config: {
SUPERVISOR_POLL_INTERVAL: 900000,
SUPERVISOR_PERSISTENT_LOGGING: true,
HOST_CONFIG_enable_uart: true,
},
},
} as any,
{
local: {
config: {
SUPERVISOR_POLL_INTERVAL: 900000,
SUPERVISOR_PERSISTENT_LOGGING: true,
HOST_CONFIG_enable_uart: false,
},
},
} as any,
);
expect(steps.map((s) => s.action)).to.have.members([
'setRebootBreadcrumb',
'setBootConfig',
]);
});
});
});