1 | // Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved.
|
2 | // Node module: @loopback/testlab
|
3 | // This file is licensed under the MIT License.
|
4 | // License text available at https://opensource.org/licenses/MIT
|
5 |
|
6 | import {
|
7 | appendFile,
|
8 | copy,
|
9 | emptyDir,
|
10 | ensureDir,
|
11 | ensureDirSync,
|
12 | mkdtempSync,
|
13 | outputFile,
|
14 | outputJson,
|
15 | pathExists,
|
16 | readFile,
|
17 | remove,
|
18 | } from 'fs-extra';
|
19 | import {join, parse, resolve} from 'path';
|
20 |
|
21 | /**
|
22 | * Options for a test sandbox
|
23 | */
|
24 | export interface TestSandboxOptions {
|
25 | /**
|
26 | * The `subdir` controls if/how the sandbox creates a subdirectory under the
|
27 | * root path. It has one of the following values:
|
28 | *
|
29 | * - `true`: Creates a unique subdirectory. This will be the default behavior.
|
30 | * - `false`: Uses the root path as the target directory without creating a
|
31 | * subdirectory.
|
32 | * - a string such as `sub-dir-1`: creates a subdirectory with the given value.
|
33 | */
|
34 | subdir: boolean | string;
|
35 | }
|
36 |
|
37 | /**
|
38 | * TestSandbox class provides a convenient way to get a reference to a
|
39 | * sandbox folder in which you can perform operations for testing purposes.
|
40 | */
|
41 | export class TestSandbox {
|
42 | // Path of the TestSandbox
|
43 | private _path?: string;
|
44 |
|
45 | public get path(): string {
|
46 | if (!this._path) {
|
47 | throw new Error(
|
48 | `TestSandbox instance was deleted. Create a new instance.`,
|
49 | );
|
50 | }
|
51 | return this._path;
|
52 | }
|
53 |
|
54 | /**
|
55 | * Will create a directory if it doesn't already exist. If it exists, you
|
56 | * still get an instance of the TestSandbox.
|
57 | *
|
58 | * @example
|
59 | * ```ts
|
60 | * // Create a sandbox as a unique temporary subdirectory under the rootPath
|
61 | * const sandbox = new TestSandbox(rootPath);
|
62 | * const sandbox = new TestSandbox(rootPath, {subdir: true});
|
63 | *
|
64 | * // Create a sandbox in the root path directly
|
65 | * // This is same as the old behavior
|
66 | * const sandbox = new TestSandbox(rootPath, {subdir: false});
|
67 | *
|
68 | * // Create a sandbox in the `test1` subdirectory of the root path
|
69 | * const sandbox = new TestSandbox(rootPath, {subdir: 'test1'});
|
70 | * ```
|
71 | *
|
72 | * @param rootPath - Root path of the TestSandbox. If relative it will be
|
73 | * resolved against the current directory.
|
74 | * @param options - Options to control if/how the sandbox creates a
|
75 | * subdirectory for the sandbox. If not provided, the sandbox
|
76 | * will automatically creates a unique temporary subdirectory. This allows
|
77 | * sandboxes with the same root path can be used in parallel during testing.
|
78 | */
|
79 | constructor(rootPath: string, options?: TestSandboxOptions) {
|
80 | rootPath = resolve(rootPath);
|
81 | ensureDirSync(rootPath);
|
82 | options = {subdir: true, ...options};
|
83 | const subdir = typeof options.subdir === 'string' ? options.subdir : '.';
|
84 | if (options.subdir !== true) {
|
85 | this._path = resolve(rootPath, subdir);
|
86 | } else {
|
87 | // Create a unique temporary directory under the root path
|
88 | // See https://nodejs.org/api/fs.html#fs_fs_mkdtempsync_prefix_options
|
89 | this._path = mkdtempSync(join(rootPath, `/${process.pid}`));
|
90 | }
|
91 | }
|
92 |
|
93 | /**
|
94 | * Resets the TestSandbox. (Remove all files in it).
|
95 | */
|
96 | async reset(): Promise<void> {
|
97 | // Decache files from require's cache so future tests aren't affected incase
|
98 | // a file is recreated in sandbox with the same file name but different
|
99 | // contents after resetting the sandbox.
|
100 | for (const key in require.cache) {
|
101 | if (key.startsWith(this.path)) {
|
102 | delete require.cache[key];
|
103 | }
|
104 | }
|
105 |
|
106 | await emptyDir(this.path);
|
107 | }
|
108 |
|
109 | /**
|
110 | * Deletes the TestSandbox.
|
111 | */
|
112 | async delete(): Promise<void> {
|
113 | await remove(this.path);
|
114 | delete this._path;
|
115 | }
|
116 |
|
117 | /**
|
118 | * Makes a directory in the TestSandbox
|
119 | *
|
120 | * @param dir - Name of directory to create (relative to TestSandbox path)
|
121 | */
|
122 | async mkdir(dir: string): Promise<void> {
|
123 | await ensureDir(resolve(this.path, dir));
|
124 | }
|
125 |
|
126 | /**
|
127 | * Copies a file from src to the TestSandbox. If copying a `.js` file which
|
128 | * has an accompanying `.js.map` file in the src file location, the dest file
|
129 | * will have its sourceMappingURL updated to point to the original file as
|
130 | * an absolute path so you don't need to copy the map file.
|
131 | *
|
132 | * @param src - Absolute path of file to be copied to the TestSandbox
|
133 | * @param dest - Optional. Destination filename of the copy operation
|
134 | * (relative to TestSandbox). Original filename used if not specified.
|
135 | * @param transform - Optional. A function to transform the file content.
|
136 | */
|
137 | async copyFile(
|
138 | src: string,
|
139 | dest?: string,
|
140 | transform?: (content: string) => string,
|
141 | ): Promise<void> {
|
142 | dest = dest
|
143 | ? resolve(this.path, dest)
|
144 | : resolve(this.path, parse(src).base);
|
145 |
|
146 | if (transform == null) {
|
147 | await copy(src, dest);
|
148 | } else {
|
149 | let content = await readFile(src, 'utf-8');
|
150 | content = transform(content);
|
151 | await outputFile(dest, content, {encoding: 'utf-8'});
|
152 | }
|
153 |
|
154 | if (parse(src).ext === '.js' && (await pathExists(src + '.map'))) {
|
155 | const srcMap = src + '.map';
|
156 | await appendFile(dest, `\n//# sourceMappingURL=${srcMap}`);
|
157 | }
|
158 | }
|
159 |
|
160 | /**
|
161 | * Creates a new file and writes the given data serialized as JSON.
|
162 | *
|
163 | * @param dest - Destination filename, optionally including a relative path.
|
164 | * @param data - The data to write.
|
165 | */
|
166 | async writeJsonFile(dest: string, data: unknown): Promise<void> {
|
167 | dest = resolve(this.path, dest);
|
168 | return outputJson(dest, data, {spaces: 2});
|
169 | }
|
170 |
|
171 | /**
|
172 | * Creates a new file and writes the given data as a UTF-8-encoded text.
|
173 | *
|
174 | * @param dest - Destination filename, optionally including a relative path.
|
175 | * @param data - The text to write.
|
176 | */
|
177 | async writeTextFile(dest: string, data: string): Promise<void> {
|
178 | dest = resolve(this.path, dest);
|
179 | return outputFile(dest, data, 'utf-8');
|
180 | }
|
181 | }
|