balena-supervisor/test/legacy/12-device-config.spec.ts

926 lines
25 KiB
TypeScript
Raw Normal View History

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 { 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 log from '~/lib/supervisor-console';
import { fnSchema } from '~/src/config/functions';
import prepare = require('~/test-lib/prepare');
import mock = require('mock-fs');
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 = 'test/data/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 () => {
// 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();
});
afterEach(() => {
// Restore stubs
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 mockFs = () => {
mock({
// 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',
}),
});
};
const unmockFs = () => {
mock.restore();
};
beforeEach(() => {
mockFs();
});
afterEach(() => {
// Reset the state of the fs after each test to
// prevent tests leaking into each other
unmockFs();
});
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 mockFs = () => {
mock({
// 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
`,
});
};
const unmockFs = () => {
mock.restore();
};
beforeEach(() => {
mockFs();
});
afterEach(() => {
// Reset the state of the fs after each test to
// prevent tests leaking into each other
unmockFs();
});
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');
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 mockFs = () => {
mock({
// 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: '',
},
},
});
};
const unmockFs = () => {
mock.restore();
};
beforeEach(() => {
mockFs();
});
afterEach(() => {
// Reset the state of the fs after each test to
// prevent tests leaking into each other
unmockFs();
});
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 () => {
stub(fsUtils, 'exec').resolves();
await configFsBackend.initialise();
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',
);
// 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 = () => {
mock({
// 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]: {
/* empty directory */
},
});
};
const unmockFs = () => {
mock.restore();
};
beforeEach(() => {
mockFs();
});
afterEach(() => {
unmockFs();
});
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'),
);
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;
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);
});
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 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;
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 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 () => {
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);
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();
});
});
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`
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',
),
});
};
const unmockFs = () => {
mock.restore();
};
beforeEach(() => {
mockFs();
});
afterEach(() => {
unmockFs();
});
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',
]);
});
});
});