backend-test-utils: refactor MockDirectory to be synchronous

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-09-22 11:48:12 +02:00
parent 95dab5827e
commit 7d275f4587
3 changed files with 108 additions and 128 deletions
+5 -5
View File
@@ -36,17 +36,17 @@ export function isDockerDisabledForTests(): boolean;
// @public
export class MockDirectory {
addContent(root: MockDirectoryContent): Promise<void>;
clear: () => Promise<void>;
addContent(root: MockDirectoryContent): void;
clear: () => void;
content(
options?: MockDirectoryContentOptions,
): Promise<MockDirectoryContent | undefined>;
): MockDirectoryContent | undefined;
static create(options?: MockDirectoryCreateOptions): MockDirectory;
static mockOsTmpDir(): MockDirectory;
get path(): string;
remove: () => Promise<void>;
remove: () => void;
resolve(...paths: string[]): string;
setContent(root: MockDirectoryContent): Promise<void>;
setContent(root: MockDirectoryContent): void;
}
// @public
@@ -38,19 +38,19 @@ describe('MockDirectory', () => {
it('should remove itself', async () => {
await expect(fs.pathExists(mockDir.path)).resolves.toBe(true);
await mockDir.remove();
mockDir.remove();
await expect(fs.pathExists(mockDir.path)).resolves.toBe(false);
});
it('should populate a directory with text files', async () => {
await mockDir.setContent({
it('should populate a directory with text files', () => {
mockDir.setContent({
'a.txt': 'a',
'a/b.txt': 'b',
'a/b/c.txt': 'c',
'a/b/d.txt': 'd',
});
await expect(mockDir.content()).resolves.toEqual({
expect(mockDir.content()).toEqual({
'a.txt': 'a',
a: {
'b.txt': 'b',
@@ -62,15 +62,15 @@ describe('MockDirectory', () => {
});
});
it('should mix text and binary files', async () => {
await mockDir.setContent({
it('should mix text and binary files', () => {
mockDir.setContent({
'a.txt': 'a',
'a/b.txt': 'b',
'a/b/c.bin': Buffer.from([0xc]),
'a/b/d.bin': Buffer.from([0xd]),
});
await expect(mockDir.content()).resolves.toEqual({
expect(mockDir.content()).toEqual({
'a.txt': 'a',
a: {
'b.txt': 'b',
@@ -82,25 +82,25 @@ describe('MockDirectory', () => {
});
});
it('should be able to add content', async () => {
await mockDir.setContent({
it('should be able to add content', () => {
mockDir.setContent({
'a.txt': 'a',
b: {},
});
await expect(mockDir.content()).resolves.toEqual({
expect(mockDir.content()).toEqual({
'a.txt': 'a',
b: {},
});
await mockDir.addContent({
mockDir.addContent({
'b.txt': 'b',
b: {
'c.txt': 'c',
},
});
await expect(mockDir.content()).resolves.toEqual({
expect(mockDir.content()).toEqual({
'a.txt': 'a',
'b.txt': 'b',
b: {
@@ -109,22 +109,22 @@ describe('MockDirectory', () => {
});
});
it('should replace existing files', async () => {
await mockDir.setContent({
it('should replace existing files', () => {
mockDir.setContent({
'a.txt': 'a',
});
await mockDir.addContent({
mockDir.addContent({
'a.txt': 'a2',
});
await expect(mockDir.content()).resolves.toEqual({
expect(mockDir.content()).toEqual({
'a.txt': 'a2',
});
});
it('should read content from sub dirs', async () => {
await mockDir.setContent({
it('should read content from sub dirs', () => {
mockDir.setContent({
'a.txt': 'a',
'b/b.txt': 'b',
'b/c/c.txt': 'c',
@@ -140,77 +140,65 @@ describe('MockDirectory', () => {
},
};
await expect(mockDir.content()).resolves.toEqual(expected);
await expect(mockDir.content({ path: mockDir.path })).resolves.toEqual(
expected,
);
await expect(
mockDir.content({ path: mockDir.resolve('.') }),
).resolves.toEqual(expected);
await expect(mockDir.content({ path: 'b' })).resolves.toEqual(expected.b);
await expect(mockDir.content({ path: './b' })).resolves.toEqual(expected.b);
await expect(
mockDir.content({ path: mockDir.resolve('b') }),
).resolves.toEqual(expected.b);
await expect(mockDir.content({ path: 'b/c' })).resolves.toEqual(
expect(mockDir.content()).toEqual(expected);
expect(mockDir.content({ path: mockDir.path })).toEqual(expected);
expect(mockDir.content({ path: mockDir.resolve('.') })).toEqual(expected);
expect(mockDir.content({ path: 'b' })).toEqual(expected.b);
expect(mockDir.content({ path: './b' })).toEqual(expected.b);
expect(mockDir.content({ path: mockDir.resolve('b') })).toEqual(expected.b);
expect(mockDir.content({ path: 'b/c' })).toEqual(expected.b.c);
expect(mockDir.content({ path: './b/c' })).toEqual(expected.b.c);
expect(mockDir.content({ path: mockDir.resolve('b/c') })).toEqual(
expected.b.c,
);
await expect(mockDir.content({ path: './b/c' })).resolves.toEqual(
expect(mockDir.content({ path: mockDir.resolve('b', 'c') })).toEqual(
expected.b.c,
);
await expect(
mockDir.content({ path: mockDir.resolve('b/c') }),
).resolves.toEqual(expected.b.c);
await expect(
mockDir.content({ path: mockDir.resolve('b', 'c') }),
).resolves.toEqual(expected.b.c);
});
it('should allow text reading to be configured', async () => {
it('should allow text reading to be configured', () => {
const text = 'a';
const binary = Buffer.from('a', 'utf8');
await mockDir.setContent({
mockDir.setContent({
a: binary,
'a.txt': text,
'a.bin': binary,
});
await expect(mockDir.content()).resolves.toEqual({
expect(mockDir.content()).toEqual({
a: binary,
'a.txt': text,
'a.bin': binary,
});
await expect(mockDir.content({ shouldReadAsText: false })).resolves.toEqual(
{
a: binary,
'a.txt': binary,
'a.bin': binary,
},
);
expect(mockDir.content({ shouldReadAsText: false })).toEqual({
a: binary,
'a.txt': binary,
'a.bin': binary,
});
await expect(mockDir.content({ shouldReadAsText: true })).resolves.toEqual({
expect(mockDir.content({ shouldReadAsText: true })).toEqual({
a: text,
'a.txt': text,
'a.bin': text,
});
await expect(
expect(
mockDir.content({ shouldReadAsText: path => path.length > 3 }),
).resolves.toEqual({
).toEqual({
a: binary,
'a.txt': text,
'a.bin': text,
});
});
it('should provide a posix path to shouldReadAsText', async () => {
it('should provide a posix path to shouldReadAsText', () => {
const shouldReadAsText = jest.fn().mockReturnValue(true);
await mockDir.setContent({ 'a/b/c': 'c' });
mockDir.setContent({ 'a/b/c': 'c' });
await expect(mockDir.content({ shouldReadAsText })).resolves.toEqual({
expect(mockDir.content({ shouldReadAsText })).toEqual({
a: { b: { c: 'c' } },
});
expect(shouldReadAsText).toHaveBeenCalledWith(
@@ -219,27 +207,27 @@ describe('MockDirectory', () => {
);
});
it('should not override directories', async () => {
await mockDir.setContent({
it('should not override directories', () => {
mockDir.setContent({
'a.txt': 'a',
b: {},
});
await expect(
expect(() =>
mockDir.addContent({
'a.txt': {},
}),
).rejects.toThrow('EEXIST');
).toThrow('EEXIST');
await expect(
expect(() =>
mockDir.addContent({
b: 'b',
}),
).rejects.toThrow('EISDIR');
).toThrow('EISDIR');
});
it('examples should work', async () => {
await mockDir.setContent({
it('examples should work', () => {
mockDir.setContent({
'test.txt': 'content',
'sub-dir': {
'file.txt': 'content',
@@ -249,7 +237,7 @@ describe('MockDirectory', () => {
'binary-file': Buffer.from([0, 1, 2]),
});
await mockDir.addContent({
mockDir.addContent({
'test.txt': 'content',
'sub-dir': {
'file.txt': 'content',
@@ -259,7 +247,7 @@ describe('MockDirectory', () => {
'binary-file': Buffer.from([0, 1, 2]),
});
await expect(mockDir.content()).resolves.toEqual({
expect(mockDir.content()).toEqual({
'test.txt': 'content',
'sub-dir': {
'file.txt': 'content',
@@ -272,15 +260,15 @@ describe('MockDirectory', () => {
});
});
it('should reject non-child paths', async () => {
it('should reject non-child paths', () => {
const path = resolvePath('/root/a.txt');
await expect(mockDir.setContent({ '/root/a.txt': 'a' })).rejects.toThrow(
expect(() => mockDir.setContent({ '/root/a.txt': 'a' })).toThrow(
`Provided path must resolve to a child path of the mock directory, got '${path}'`,
);
await expect(mockDir.addContent({ '/root/a.txt': 'a' })).rejects.toThrow(
expect(() => mockDir.addContent({ '/root/a.txt': 'a' })).toThrow(
`Provided path must resolve to a child path of the mock directory, got '${path}'`,
);
await expect(mockDir.content({ path: '/root/a.txt' })).rejects.toThrow(
expect(() => mockDir.content({ path: '/root/a.txt' })).toThrow(
`Provided path must resolve to a child path of the mock directory, got '${path}'`,
);
});
@@ -291,19 +279,19 @@ describe('MockDirectory', () => {
describe('inner', () => {
cleanupMockDir = MockDirectory.create();
it('should populate a directory', async () => {
await cleanupMockDir.setContent({
it('should populate a directory', () => {
cleanupMockDir.setContent({
'a.txt': 'a',
});
await expect(cleanupMockDir.content()).resolves.toEqual({
expect(cleanupMockDir.content()).toEqual({
'a.txt': 'a',
});
});
});
it('should clean up after itself automatically', async () => {
await expect(cleanupMockDir.content()).resolves.toBeUndefined();
it('should clean up after itself automatically', () => {
expect(cleanupMockDir.content()).toBeUndefined();
});
});
@@ -313,12 +301,12 @@ describe('MockDirectory', () => {
describe('inner', () => {
tmpDirMock = MockDirectory.mockOsTmpDir();
it('should mock os.tmpdir()', async () => {
it('should mock os.tmpdir()', () => {
expect(os.tmpdir()).toBe(tmpDirMock.path);
});
});
it('should restore os.tmpdir()', async () => {
it('should restore os.tmpdir()', () => {
expect(os.tmpdir()).not.toBe(tmpDirMock.path);
});
});
@@ -329,15 +317,15 @@ describe('MockDirectory', () => {
describe('inner', () => {
existingMockDir = MockDirectory.create({ root: __dirname }); // hardcore mode
it('should read existing directory', async () => {
await expect(existingMockDir.content()).resolves.toMatchObject({
it('should read existing directory', () => {
expect(existingMockDir.content()).toMatchObject({
'index.ts': expect.any(String),
});
});
});
it('should remove existing directory', async () => {
await expect(fs.pathExists(__dirname)).resolves.toBe(true);
it('should remove existing directory', () => {
expect(fs.pathExistsSync(__dirname)).toBe(true);
});
});
});
@@ -124,9 +124,9 @@ export class MockDirectory {
* describe('MySubject', () => {
* const mockDir = MockDirectory.create();
*
* beforeEach(() => mockDir.clear());
* beforeEach(mockDir.clear);
*
* it('should work', async () => {
* it('should work', () => {
* // ... use mockDir
* })
* })
@@ -141,7 +141,7 @@ export class MockDirectory {
const shouldCleanup = !options?.root || !fs.pathExistsSync(options.root);
if (shouldCleanup) {
process.on('beforeExit', mocker.#removeSync);
process.on('beforeExit', mocker.remove);
try {
afterAll(mocker.remove);
@@ -199,7 +199,7 @@ export class MockDirectory {
*
* @example
* ```ts
* await mockDir.setContent({
* mockDir.setContent({
* 'test.txt': 'content',
* 'sub-dir': {
* 'file.txt': 'content',
@@ -210,8 +210,8 @@ export class MockDirectory {
* });
* ```
*/
async setContent(root: MockDirectoryContent): Promise<void> {
await this.remove();
setContent(root: MockDirectoryContent): void {
this.remove();
return this.addContent(root);
}
@@ -221,7 +221,7 @@ export class MockDirectory {
*
* @example
* ```ts
* await mockDir.addContent({
* mockDir.addContent({
* 'test.txt': 'content',
* 'sub-dir': {
* 'file.txt': 'content',
@@ -232,7 +232,7 @@ export class MockDirectory {
* });
* ```
*/
async addContent(root: MockDirectoryContent): Promise<void> {
addContent(root: MockDirectoryContent): void {
const entries = this.#transformInput(root);
for (const entry of entries) {
@@ -244,10 +244,10 @@ export class MockDirectory {
}
if (entry.type === 'dir') {
await fs.ensureDir(fullPath);
fs.ensureDirSync(fullPath);
} else if (entry.type === 'file') {
await fs.ensureDir(dirname(fullPath));
await fs.writeFile(fullPath, entry.content);
fs.ensureDirSync(dirname(fullPath));
fs.writeFileSync(fullPath, entry.content);
}
}
}
@@ -262,7 +262,7 @@ export class MockDirectory {
*
* @example
* ```ts
* await expect(mockDir.content()).resolves.toEqual({
* expect(mockDir.content()).toEqual({
* 'test.txt': 'content',
* 'sub-dir': {
* 'file.txt': 'content',
@@ -275,9 +275,9 @@ export class MockDirectory {
* });
* ```
*/
async content(
content(
options?: MockDirectoryContentOptions,
): Promise<MockDirectoryContent | undefined> {
): MockDirectoryContent | undefined {
const shouldReadAsText =
(typeof options?.shouldReadAsText === 'boolean'
? () => options?.shouldReadAsText
@@ -291,33 +291,29 @@ export class MockDirectory {
);
}
async function read(
path: string,
): Promise<MockDirectoryContent | undefined> {
if (!(await fs.pathExists(path))) {
function read(path: string): MockDirectoryContent | undefined {
if (!fs.pathExistsSync(path)) {
return undefined;
}
const entries = await fs.readdir(path, { withFileTypes: true });
const entries = fs.readdirSync(path, { withFileTypes: true });
return Object.fromEntries(
await Promise.all(
entries.map(async entry => {
const fullPath = resolvePath(path, entry.name);
entries.map(entry => {
const fullPath = resolvePath(path, entry.name);
if (entry.isDirectory()) {
return [entry.name, await read(fullPath)];
}
const content = await fs.readFile(fullPath);
const relativePosixPath = relativePath(root, fullPath)
.split(win32.sep)
.join(posix.sep);
if (entry.isDirectory()) {
return [entry.name, read(fullPath)];
}
const content = fs.readFileSync(fullPath);
const relativePosixPath = relativePath(root, fullPath)
.split(win32.sep)
.join(posix.sep);
if (shouldReadAsText(relativePosixPath, content)) {
return [entry.name, content.toString('utf8')];
}
return [entry.name, content];
}),
),
if (shouldReadAsText(relativePosixPath, content)) {
return [entry.name, content.toString('utf8')];
}
return [entry.name, content];
}),
);
}
@@ -327,20 +323,20 @@ export class MockDirectory {
/**
* Clears the content of the mock directory, ensuring that the directory itself exists.
*/
clear = async (): Promise<void> => {
await this.setContent({});
clear = (): void => {
this.setContent({});
};
/**
* Removes the mock directory and all its contents.
*/
remove = async (): Promise<void> => {
remove = (): void => {
try {
await fs.rm(this.#root, { recursive: true, force: true });
fs.rmSync(this.#root, { recursive: true, force: true });
} catch (error: unknown) {
if (isError(error) && error.code === 'ENOTEMPTY') {
// Windows can be a bit flaky, give it another go
await fs.rm(this.#root, { recursive: true, force: true });
fs.rmSync(this.#root, { recursive: true, force: true });
} else {
throw error;
}
@@ -372,8 +368,4 @@ export class MockDirectory {
return entries;
}
#removeSync = () => {
fs.rmSync(this.#root, { recursive: true, force: true });
};
}