mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-06-18 07:18:14 +00:00
Migrate update-lock tests as integration tests
Update-lock tests now use the actual filesystem for testing, instead of relying on stubs and spies. This commit also fixes a small bug with update-lock that would cause a `PromiseRejectionHandledWarning` when the lock callback would throw.
This commit is contained in:
290
test/integration/lib/update-lock.spec.ts
Normal file
290
test/integration/lib/update-lock.spec.ts
Normal file
@ -0,0 +1,290 @@
|
||||
import { expect } from 'chai';
|
||||
import * as path from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
import { testfs } from 'mocha-pod';
|
||||
|
||||
import * as updateLock from '~/lib/update-lock';
|
||||
import * as constants from '~/lib/constants';
|
||||
import { UpdatesLockedError } from '~/lib/errors';
|
||||
import * as config from '~/src/config';
|
||||
import * as lockfile from '~/lib/lockfile';
|
||||
|
||||
describe('lib/update-lock', () => {
|
||||
describe('abortIfHUPInProgress', () => {
|
||||
const breadcrumbFiles = [
|
||||
'rollback-health-breadcrumb',
|
||||
'rollback-altboot-breadcrumb',
|
||||
];
|
||||
|
||||
const breadcrumbsDir = path.join(
|
||||
constants.rootMountPoint,
|
||||
constants.stateMountPoint,
|
||||
);
|
||||
|
||||
const createBreadcrumb = (breadcrumb: string) =>
|
||||
testfs({
|
||||
[path.join(breadcrumbsDir, breadcrumb)]: '',
|
||||
}).enable();
|
||||
|
||||
before(async () => {
|
||||
// Ensure the directory exists for all tests
|
||||
await fs.mkdir(breadcrumbsDir, { recursive: true });
|
||||
});
|
||||
|
||||
it('should throw if any breadcrumbs exist on host', async () => {
|
||||
for (const bc of breadcrumbFiles) {
|
||||
const testFs = await createBreadcrumb(bc);
|
||||
await expect(updateLock.abortIfHUPInProgress({ force: false }))
|
||||
.to.eventually.be.rejectedWith('Waiting for Host OS update to finish')
|
||||
.and.be.an.instanceOf(UpdatesLockedError);
|
||||
await testFs.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('should resolve to false if no breadcrumbs on host', async () => {
|
||||
// check that there are no breadcrumbs already on the directory
|
||||
expect(await fs.readdir(breadcrumbsDir)).to.have.lengthOf(0);
|
||||
await expect(
|
||||
updateLock.abortIfHUPInProgress({ force: false }),
|
||||
).to.eventually.equal(false);
|
||||
});
|
||||
|
||||
it('should resolve to true if breadcrumbs are on host but force is passed', async () => {
|
||||
for (const bc of breadcrumbFiles) {
|
||||
const testFs = await createBreadcrumb(bc);
|
||||
await expect(
|
||||
updateLock.abortIfHUPInProgress({ force: true }),
|
||||
).to.eventually.equal(true);
|
||||
await testFs.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Lock/dispose functionality', () => {
|
||||
const testAppId = 1234567;
|
||||
const testServiceName = 'test';
|
||||
|
||||
const supportedLockfiles = ['resin-updates.lock', 'updates.lock'];
|
||||
|
||||
const takeLocks = () =>
|
||||
Promise.all(
|
||||
supportedLockfiles.map((lf) =>
|
||||
lockfile.lock(path.join(lockdir(testAppId, testServiceName), lf)),
|
||||
),
|
||||
);
|
||||
|
||||
const releaseLocks = async () => {
|
||||
await Promise.all(
|
||||
lockfile.getLocksTaken().map((lock) => lockfile.unlock(lock)),
|
||||
);
|
||||
|
||||
// Remove any other lockfiles created for the testAppId
|
||||
await Promise.all(
|
||||
supportedLockfiles.map((lf) =>
|
||||
lockfile.unlock(path.join(lockdir(testAppId, testServiceName), lf)),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const lockdir = (appId: number, serviceName: string): string =>
|
||||
path.join(
|
||||
constants.rootMountPoint,
|
||||
updateLock.lockPath(appId, serviceName),
|
||||
);
|
||||
|
||||
const expectLocks = async (
|
||||
exists: boolean,
|
||||
msg?: string,
|
||||
appId = testAppId,
|
||||
serviceName = testServiceName,
|
||||
) =>
|
||||
expect(
|
||||
fs.readdir(lockdir(appId, serviceName)),
|
||||
msg,
|
||||
).to.eventually.deep.equal(exists ? supportedLockfiles : []);
|
||||
|
||||
before(async () => {
|
||||
await config.initialized();
|
||||
await config.set({ lockOverride: false });
|
||||
|
||||
// Ensure the directory is available for all tests
|
||||
await fs.mkdir(lockdir(testAppId, testServiceName), {
|
||||
recursive: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Cleanup all locks between tests
|
||||
await releaseLocks();
|
||||
});
|
||||
|
||||
it('should take the lock, run the function, then dispose of locks', async () => {
|
||||
await expectLocks(
|
||||
false,
|
||||
'locks should not exist before the lock is taken',
|
||||
);
|
||||
|
||||
await expect(
|
||||
updateLock.lock(testAppId, { force: false }, () =>
|
||||
// At this point the locks should be taken and not removed
|
||||
// until this function has been resolved
|
||||
expectLocks(true, 'lockfiles should exist while the lock is active'),
|
||||
),
|
||||
).to.be.fulfilled;
|
||||
|
||||
await expectLocks(
|
||||
false,
|
||||
'locks should not exist after the lock is released',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw UpdatesLockedError if lockfiles exists', async () => {
|
||||
// Take the locks before testing
|
||||
await takeLocks();
|
||||
|
||||
await expectLocks(true, 'locks should exist before the lock is taken');
|
||||
|
||||
await updateLock
|
||||
.lock(testAppId, { force: false }, () =>
|
||||
Promise.reject(
|
||||
'the lock function should not invoke the callback if locks are taken',
|
||||
),
|
||||
)
|
||||
.catch((err) => expect(err).to.be.instanceOf(UpdatesLockedError));
|
||||
|
||||
// Since the lock-taking failed, there should be no locks to dispose of
|
||||
expect(lockfile.getLocksTaken()).to.have.length(0);
|
||||
|
||||
// Restore the locks that were taken at the beginning of the test
|
||||
await releaseLocks();
|
||||
});
|
||||
|
||||
it('should dispose of taken locks on any other errors', async () => {
|
||||
await expectLocks(false, 'locks should not exist before lock is called');
|
||||
await expect(
|
||||
updateLock.lock(
|
||||
testAppId,
|
||||
{ force: false },
|
||||
// At this point 2 lockfiles have been written, so this is testing
|
||||
// that even if the function rejects, lockfiles will be disposed of
|
||||
() =>
|
||||
expectLocks(
|
||||
true,
|
||||
'locks should be owned by the calling function',
|
||||
).then(() => Promise.reject('Test error')),
|
||||
),
|
||||
).to.be.rejectedWith('Test error');
|
||||
|
||||
await expectLocks(
|
||||
false,
|
||||
'locks should be removed if an error happens within the lock callback',
|
||||
);
|
||||
});
|
||||
|
||||
it('locks all applications before resolving input function', async () => {
|
||||
const appIds = [111, 222, 333];
|
||||
|
||||
// Set up necessary lock directories
|
||||
await Promise.all(
|
||||
appIds.map((id) =>
|
||||
fs.mkdir(lockdir(id, testServiceName), { recursive: true }),
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
updateLock.lock(appIds, { force: false }, () =>
|
||||
// At this point the locks should be taken and not removed
|
||||
// until this function has been resolved
|
||||
// Both `updates.lock` and `resin-updates.lock` should have been taken
|
||||
Promise.all(
|
||||
appIds.map((appId) =>
|
||||
expectLocks(
|
||||
true,
|
||||
`locks for app(${appId}) should exist`,
|
||||
appId,
|
||||
testServiceName,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
).to.eventually.be.fulfilled;
|
||||
|
||||
// Everything that was locked should have been unlocked after function resolves
|
||||
await Promise.all(
|
||||
appIds.map((appId) =>
|
||||
expectLocks(
|
||||
false,
|
||||
`locks for app(${appId}) should have been released`,
|
||||
appId,
|
||||
testServiceName,
|
||||
),
|
||||
),
|
||||
).finally(() =>
|
||||
// In case the above fails, we need to make sure to cleanup the lockdir
|
||||
Promise.all(
|
||||
appIds
|
||||
.map((appId) =>
|
||||
supportedLockfiles.map((lf) =>
|
||||
lockfile.unlock(path.join(lockdir(appId, testServiceName), lf)),
|
||||
),
|
||||
)
|
||||
.flat(),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves input function without locking when appId is null', async () => {
|
||||
await takeLocks();
|
||||
|
||||
await expect(
|
||||
updateLock.lock(null as any, { force: false }, () => Promise.resolve()),
|
||||
).to.be.fulfilled;
|
||||
|
||||
await expectLocks(
|
||||
true,
|
||||
'locks should not be touched by an unrelated lock() call',
|
||||
);
|
||||
|
||||
await releaseLocks();
|
||||
});
|
||||
|
||||
it('unlocks lockfile to resolve function if force option specified', async () => {
|
||||
await takeLocks();
|
||||
|
||||
await expect(
|
||||
updateLock.lock(testAppId, { force: true }, () =>
|
||||
expectLocks(
|
||||
true,
|
||||
'locks should be deleted and taken again by the lock() call',
|
||||
),
|
||||
),
|
||||
).to.be.fulfilled;
|
||||
|
||||
await expectLocks(
|
||||
false,
|
||||
'using force gave lock ownership to the callback, so they should now be deleted',
|
||||
);
|
||||
});
|
||||
|
||||
it('unlocks lockfile to resolve function if lockOverride option specified', async () => {
|
||||
await takeLocks();
|
||||
|
||||
// Change the configuration
|
||||
await config.set({ lockOverride: true });
|
||||
|
||||
await expect(
|
||||
updateLock.lock(testAppId, { force: false }, () =>
|
||||
expectLocks(
|
||||
true,
|
||||
'locks should be deleted and taken again by the lock() call because of the override',
|
||||
),
|
||||
),
|
||||
).to.be.fulfilled;
|
||||
|
||||
await expectLocks(
|
||||
false,
|
||||
'using lockOverride gave lock ownership to the callback, so they should now be deleted',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user