wardens
Version:
A framework for resource management
237 lines (197 loc) • 7.22 kB
text/typescript
import type ResourceControls from '../resource-controls';
import { create, destroy } from '../root-lifecycle';
import bindContext from '../bind-context';
describe('roots', () => {
describe('create', () => {
it('allocates the resource', async () => {
const config = { test: 'init-args' };
const Test = vi.fn(
async (_resource: ResourceControls, config: { test: string }) => ({
value: config,
}),
);
await expect(create(Test, config)).resolves.toEqual(config);
expect(Test).toHaveBeenCalledWith(expect.anything(), config);
});
describe('after initialization failure', () => {
it('destroys child resources in reverse order', async () => {
const spy = vi.fn();
const First = async () => ({ value: [], destroy: () => spy('1st') });
const Second = async () => ({ value: [], destroy: () => spy('2nd') });
const Parent = async (resource: ResourceControls) => {
await resource.create(First);
await resource.create(Second);
throw new Error('Testing resource initialization errors');
};
await expect(create(Parent)).rejects.toThrow();
expect(spy.mock.calls).toEqual([['2nd'], ['1st']]);
});
it('continues even if the children cannot be destroyed', async () => {
const parentError = new Error('Testing parent resource aborts');
const childError = new Error('Testing child resource aborts');
const Child = async () => ({
value: [],
destroy() {
throw childError;
},
});
const Parent = async (resource: ResourceControls) => {
await resource.create(Child);
await resource.create(Child);
throw parentError;
};
await expect(create(Parent)).rejects.toMatchObject({
cause: parentError,
failures: [childError, childError],
});
});
});
});
describe('destroy', () => {
it('deallocates the resource', async () => {
const spy = vi.fn<[], void>();
const Test = async () => ({
value: {},
destroy: spy,
});
const test = await create(Test);
expect(spy).not.toHaveBeenCalled();
await expect(destroy(test)).resolves.not.toThrow();
expect(spy).toHaveBeenCalled();
});
it('throws an error if the object is not a resource', async () => {
await expect(destroy({})).rejects.toThrow(
'Cannot destroy object. It is not a resource.',
);
});
it('survives if the resource is already deallocated', async () => {
const Test = async () => ({ value: [] });
const test = await create(Test);
await destroy(test);
await expect(destroy(test)).resolves.not.toThrow();
});
it('automatically unmounts all children', async () => {
const spy = vi.fn();
const Child = async () => ({ value: [], destroy: spy });
async function Parent(resource: ResourceControls) {
await resource.create(Child);
return { value: [] };
}
const parent = await create(Parent);
await destroy(parent);
expect(spy).toHaveBeenCalled();
});
it('throws an error if any of the children fail to close', async () => {
const error = new Error('Testing resource destruction errors');
const Child = async () => ({
value: [],
destroy() {
throw error;
},
});
const Parent = async (resource: ResourceControls) => {
await resource.create(Child);
return { value: [] };
};
const parent = await create(Parent);
await expect(destroy(parent)).rejects.toThrow(error);
});
it('destroys child resources in reverse order', async () => {
const spy = vi.fn();
const First = async () => ({ value: [], destroy: () => spy('1st') });
const Second = async () => ({ value: [], destroy: () => spy('2nd') });
const Parent = async (resource: ResourceControls) => {
await resource.create(First);
await resource.create(Second);
return { value: [] };
};
const parent = await create(Parent);
await destroy(parent);
expect(spy.mock.calls).toEqual([['2nd'], ['1st']]);
});
it('reports if multiple resources could not be destroyed', async () => {
const childError = new Error('Testing child resource aborts');
const Child = async () => ({
value: [],
destroy() {
throw childError;
},
});
const Parent = async (resource: ResourceControls) => {
await resource.create(Child);
await resource.create(Child);
return { value: [] };
};
const parent = await create(Parent);
await expect(destroy(parent)).rejects.toMatchObject({
failures: [childError, childError],
});
});
it('ensures child resources outlive their consumers', async () => {
const Child = async () => ({ value: [1] });
const Parent = async (resource: ResourceControls) => {
const child = await resource.create(Child);
return {
value: [],
destroy() {
// The `child` resource must still be usable here.
expect(child).toHaveLength(1);
},
};
};
const parent = await create(Parent);
await destroy(parent);
});
it('destroys child resources even if the parent fails to close', async () => {
const spy = vi.fn();
const Child = async () => ({ value: [], destroy: spy });
const Parent = async (resource: ResourceControls) => {
await resource.create(Child);
return {
value: [],
destroy() {
throw new Error('Testing parent teardown errors');
},
};
};
const parent = await create(Parent);
await expect(destroy(parent)).rejects.toThrow();
expect(spy).toHaveBeenCalled();
});
it('aggregates errors if the child resource fails to close', async () => {
const parentError = new Error('Testing parent resource aborts');
const childError = new Error('Testing child resource aborts');
const Child = async () => ({
value: [],
destroy() {
throw childError;
},
});
const Parent = async (resource: ResourceControls) => {
await resource.create(Child);
return {
value: [],
destroy() {
throw parentError;
},
};
};
const parent = await create(Parent);
await expect(destroy(parent)).rejects.toMatchObject({
failures: [parentError, childError],
});
});
it('guards against creating new resources after teardown', async () => {
const Child = async () => ({ value: [] });
const Parent = async (resource: ResourceControls) => ({
value: bindContext(resource),
});
const parent = await create(Parent);
const { create: resourceCreate } = parent;
await destroy(parent);
await expect(resourceCreate(Child)).rejects.toThrow(
/cannot create.*after teardown/i,
);
});
});
});