mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-23 23:42:29 +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:
parent
0fb1de2a1a
commit
f19f70d690
@ -22,3 +22,4 @@ testfs:
|
|||||||
# when restoring the filesystem
|
# when restoring the filesystem
|
||||||
cleanup:
|
cleanup:
|
||||||
- /data/database.sqlite
|
- /data/database.sqlite
|
||||||
|
- /mnt/root/tmp/balena-supervisor/**/*.lock
|
||||||
|
@ -135,7 +135,7 @@ export async function lock<T extends unknown>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Resolve the function passed
|
// Resolve the function passed
|
||||||
return fn();
|
return await fn();
|
||||||
} finally {
|
} finally {
|
||||||
for (const [id, release] of releases.entries()) {
|
for (const [id, release] of releases.entries()) {
|
||||||
// Try to dispose all the locks
|
// Try to dispose all the locks
|
||||||
|
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',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,319 +0,0 @@
|
|||||||
import { expect } from 'chai';
|
|
||||||
import { SinonSpy, SinonStub, spy, stub } from 'sinon';
|
|
||||||
import * as path from 'path';
|
|
||||||
import { promises as fs } from 'fs';
|
|
||||||
import mockFs = require('mock-fs');
|
|
||||||
|
|
||||||
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';
|
|
||||||
import * as fsUtils from '~/lib/fs-utils';
|
|
||||||
|
|
||||||
describe('lib/update-lock', () => {
|
|
||||||
const appId = 1234567;
|
|
||||||
const serviceName = 'test';
|
|
||||||
|
|
||||||
const mockLockDir = ({
|
|
||||||
createLockfile = true,
|
|
||||||
}: {
|
|
||||||
createLockfile?: boolean;
|
|
||||||
}) => {
|
|
||||||
const lockDirFiles: any = {};
|
|
||||||
if (createLockfile) {
|
|
||||||
lockDirFiles['updates.lock'] = mockFs.file({
|
|
||||||
uid: updateLock.LOCKFILE_UID,
|
|
||||||
});
|
|
||||||
lockDirFiles['resin-updates.lock'] = mockFs.file({
|
|
||||||
uid: updateLock.LOCKFILE_UID,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
mockFs({
|
|
||||||
[path.join(
|
|
||||||
constants.rootMountPoint,
|
|
||||||
updateLock.lockPath(appId),
|
|
||||||
serviceName,
|
|
||||||
)]: lockDirFiles,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Remove these hooks when we don't need './test/data' as test process's rootMountPoint
|
|
||||||
before(() => {
|
|
||||||
// @ts-expect-error // Set rootMountPoint for mockFs
|
|
||||||
constants.rootMountPoint = '/mnt/root';
|
|
||||||
});
|
|
||||||
|
|
||||||
after(() => {
|
|
||||||
// @ts-expect-error
|
|
||||||
constants.rootMountPoint = process.env.ROOT_MOUNTPOINT;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('lockPath', () => {
|
|
||||||
it('should return path prefix of service lockfiles on host', () => {
|
|
||||||
expect(updateLock.lockPath(appId)).to.equal(
|
|
||||||
`/tmp/balena-supervisor/services/${appId}`,
|
|
||||||
);
|
|
||||||
expect(updateLock.lockPath(appId, serviceName)).to.equal(
|
|
||||||
`/tmp/balena-supervisor/services/${appId}/${serviceName}`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('abortIfHUPInProgress', () => {
|
|
||||||
const breadcrumbFiles = [
|
|
||||||
'rollback-health-breadcrumb',
|
|
||||||
'rollback-altboot-breadcrumb',
|
|
||||||
];
|
|
||||||
|
|
||||||
const mockBreadcrumbs = (breadcrumb?: string) => {
|
|
||||||
mockFs({
|
|
||||||
[path.join(
|
|
||||||
constants.rootMountPoint,
|
|
||||||
constants.stateMountPoint,
|
|
||||||
breadcrumb ? breadcrumb : '',
|
|
||||||
)]: '',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
afterEach(() => mockFs.restore());
|
|
||||||
|
|
||||||
it('should throw if any breadcrumbs exist on host', async () => {
|
|
||||||
for (const bc of breadcrumbFiles) {
|
|
||||||
mockBreadcrumbs(bc);
|
|
||||||
await expect(updateLock.abortIfHUPInProgress({ force: false }))
|
|
||||||
.to.eventually.be.rejectedWith('Waiting for Host OS update to finish')
|
|
||||||
.and.be.an.instanceOf(UpdatesLockedError);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should resolve to false if no breadcrumbs on host', async () => {
|
|
||||||
mockBreadcrumbs();
|
|
||||||
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) {
|
|
||||||
mockBreadcrumbs(bc);
|
|
||||||
await expect(
|
|
||||||
updateLock.abortIfHUPInProgress({ force: true }),
|
|
||||||
).to.eventually.equal(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Lock/dispose functionality', () => {
|
|
||||||
const getLockParentDir = (): string =>
|
|
||||||
`${constants.rootMountPoint}${updateLock.lockPath(appId, serviceName)}`;
|
|
||||||
|
|
||||||
const expectLocks = async (exists: boolean = true) => {
|
|
||||||
expect(await fs.readdir(getLockParentDir())).to.deep.equal(
|
|
||||||
exists ? ['resin-updates.lock', 'updates.lock'] : [],
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
let unlockSpy: SinonSpy;
|
|
||||||
let lockSpy: SinonSpy;
|
|
||||||
let execStub: SinonStub;
|
|
||||||
|
|
||||||
let configGetStub: SinonStub;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
unlockSpy = spy(lockfile, 'unlock');
|
|
||||||
lockSpy = spy(lockfile, 'lock');
|
|
||||||
// lockfile.lock calls exec to interface with the lockfile binary,
|
|
||||||
// so mock it here as we don't have access to the binary in the test env
|
|
||||||
// @ts-expect-error
|
|
||||||
execStub = stub(fsUtils, 'exec').callsFake(async (command, opts) => {
|
|
||||||
// Sanity check for the command call
|
|
||||||
expect(command.trim().startsWith('lockfile')).to.be.true;
|
|
||||||
|
|
||||||
// Remove any `lockfile` command options to leave just the command and the target filepath
|
|
||||||
const [, targetPath] = command
|
|
||||||
.replace(/-v|-nnn|-r\s+\d+|-l\s+\d+|-s\s+\d+|-!|-ml|-mu/g, '')
|
|
||||||
.split(/\s+/);
|
|
||||||
|
|
||||||
// Emulate the lockfile binary exec call
|
|
||||||
await fsUtils.touch(targetPath);
|
|
||||||
await fs.chown(targetPath, opts!.uid!, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// config.get is called in updateLock.lock to get `lockOverride` value,
|
|
||||||
// so mock it here to definitively avoid any side effects
|
|
||||||
configGetStub = stub(config, 'get').resolves(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
unlockSpy.restore();
|
|
||||||
lockSpy.restore();
|
|
||||||
execStub.restore();
|
|
||||||
|
|
||||||
configGetStub.restore();
|
|
||||||
|
|
||||||
// Even though mock-fs is restored, this is needed to delete any in-memory storage of locks
|
|
||||||
for (const lock of lockfile.getLocksTaken()) {
|
|
||||||
await lockfile.unlock(lock);
|
|
||||||
}
|
|
||||||
|
|
||||||
mockFs.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should take the lock, run the function, then dispose of locks', async () => {
|
|
||||||
// Set up fake filesystem for lockfiles
|
|
||||||
mockLockDir({ createLockfile: false });
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
updateLock.lock(appId, { force: false }, async () => {
|
|
||||||
// At this point the locks should be taken and not removed
|
|
||||||
// until this function has been resolved
|
|
||||||
await expectLocks(true);
|
|
||||||
return Promise.resolve();
|
|
||||||
}),
|
|
||||||
).to.eventually.be.fulfilled;
|
|
||||||
|
|
||||||
// Both `updates.lock` and `resin-updates.lock` should have been taken
|
|
||||||
expect(lockSpy.args).to.have.length(2);
|
|
||||||
|
|
||||||
// Everything that was locked should have been unlocked
|
|
||||||
expect(lockSpy.args.map(([lock]) => [lock])).to.deep.equal(
|
|
||||||
unlockSpy.args,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw UpdatesLockedError if lockfile exists', async () => {
|
|
||||||
// Set up fake filesystem for lockfiles
|
|
||||||
mockLockDir({ createLockfile: true });
|
|
||||||
|
|
||||||
const lockPath = `${getLockParentDir()}/updates.lock`;
|
|
||||||
|
|
||||||
execStub.throws(new lockfile.LockfileExistsError(lockPath));
|
|
||||||
|
|
||||||
try {
|
|
||||||
await updateLock.lock(appId, { force: false }, async () => {
|
|
||||||
await expectLocks(false);
|
|
||||||
return Promise.resolve();
|
|
||||||
});
|
|
||||||
expect.fail('updateLock.lock should throw an UpdatesLockedError');
|
|
||||||
} catch (err) {
|
|
||||||
expect(err).to.be.instanceOf(UpdatesLockedError);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should only have attempted to take `updates.lock`
|
|
||||||
expect(lockSpy.args.flat()).to.deep.equal([
|
|
||||||
lockPath,
|
|
||||||
updateLock.LOCKFILE_UID,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Since the lock-taking failed, there should be no locks to dispose of
|
|
||||||
expect(lockfile.getLocksTaken()).to.have.length(0);
|
|
||||||
|
|
||||||
// Since nothing was locked, nothing should be unlocked
|
|
||||||
expect(unlockSpy.args).to.have.length(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should dispose of taken locks on any other errors', async () => {
|
|
||||||
// Set up fake filesystem for lockfiles
|
|
||||||
mockLockDir({ createLockfile: false });
|
|
||||||
|
|
||||||
try {
|
|
||||||
await updateLock.lock(
|
|
||||||
appId,
|
|
||||||
{ 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
|
|
||||||
async () => {
|
|
||||||
await expectLocks();
|
|
||||||
return Promise.reject(new Error('Test error'));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
/* noop */
|
|
||||||
// This just catches the 'Test error' above
|
|
||||||
}
|
|
||||||
|
|
||||||
// Both `updates.lock` and `resin-updates.lock` should have been taken
|
|
||||||
expect(lockSpy.args).to.have.length(2);
|
|
||||||
|
|
||||||
// Everything that was locked should have been unlocked
|
|
||||||
expect(lockSpy.args.map(([lock]) => [lock])).to.deep.equal(
|
|
||||||
unlockSpy.args,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('locks all applications before resolving input function', async () => {
|
|
||||||
const appIds = [111, 222, 333];
|
|
||||||
// Set up fake filesystem for lockfiles
|
|
||||||
mockFs({
|
|
||||||
[path.join(
|
|
||||||
constants.rootMountPoint,
|
|
||||||
updateLock.lockPath(111),
|
|
||||||
serviceName,
|
|
||||||
)]: {},
|
|
||||||
[path.join(
|
|
||||||
constants.rootMountPoint,
|
|
||||||
updateLock.lockPath(222),
|
|
||||||
serviceName,
|
|
||||||
)]: {},
|
|
||||||
[path.join(
|
|
||||||
constants.rootMountPoint,
|
|
||||||
updateLock.lockPath(333),
|
|
||||||
serviceName,
|
|
||||||
)]: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
updateLock.lock(appIds, { force: false }, async () => {
|
|
||||||
// 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
|
|
||||||
expect(lockSpy.args).to.have.length(6);
|
|
||||||
// Make sure that no locks have been removed also
|
|
||||||
expect(unlockSpy).to.not.be.called;
|
|
||||||
return Promise.resolve();
|
|
||||||
}),
|
|
||||||
).to.eventually.be.fulfilled;
|
|
||||||
|
|
||||||
// Everything that was locked should have been unlocked after function resolves
|
|
||||||
expect(lockSpy.args.map(([lock]) => [lock])).to.deep.equal(
|
|
||||||
unlockSpy.args,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('resolves input function without locking when appId is null', async () => {
|
|
||||||
mockLockDir({ createLockfile: true });
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
updateLock.lock(null as any, { force: false }, stub().resolves()),
|
|
||||||
).to.be.fulfilled;
|
|
||||||
|
|
||||||
// Since appId is null, updateLock.lock should just run the function, so
|
|
||||||
// there should be no interfacing with the lockfile module
|
|
||||||
expect(unlockSpy).to.not.have.been.called;
|
|
||||||
expect(lockSpy).to.not.have.been.called;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('unlocks lockfile to resolve function if force option specified', async () => {
|
|
||||||
mockLockDir({ createLockfile: true });
|
|
||||||
|
|
||||||
await expect(updateLock.lock(1234567, { force: true }, stub().resolves()))
|
|
||||||
.to.be.fulfilled;
|
|
||||||
|
|
||||||
expect(unlockSpy).to.have.been.called;
|
|
||||||
expect(lockSpy).to.have.been.called;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('unlocks lockfile to resolve function if lockOverride option specified', async () => {
|
|
||||||
configGetStub.resolves(true);
|
|
||||||
mockLockDir({ createLockfile: true });
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
updateLock.lock(1234567, { force: false }, stub().resolves()),
|
|
||||||
).to.be.fulfilled;
|
|
||||||
|
|
||||||
expect(unlockSpy).to.have.been.called;
|
|
||||||
expect(lockSpy).to.have.been.called;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
17
test/unit/lib/update-lock.spec.ts
Normal file
17
test/unit/lib/update-lock.spec.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { expect } from 'chai';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
import * as updateLock from '~/lib/update-lock';
|
||||||
|
|
||||||
|
describe('lib/update-lock: unit tests', () => {
|
||||||
|
describe('lockPath', () => {
|
||||||
|
it('should return path prefix of service lockfiles on host', () => {
|
||||||
|
expect(updateLock.lockPath(123)).to.equal(
|
||||||
|
path.join(updateLock.BASE_LOCK_DIR, '123'),
|
||||||
|
);
|
||||||
|
expect(updateLock.lockPath(123, 'main')).to.equal(
|
||||||
|
path.join(updateLock.BASE_LOCK_DIR, '123', 'main'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user