Skip to content

Commit 9526ba3

Browse files
authored
feat(testlab): create a test sandbox utility (#877)
* feat(testlab): create a test sandbox utility * style: add async suffix to promisified versions of functions * fix(testlab): apply feedback * fix(testlab): add node 6 polyfill for fs.copyFile * docs(testlab): add comment on node version for copyFile polyfill
1 parent 615882c commit 9526ba3

File tree

7 files changed

+299
-6
lines changed

7 files changed

+299
-6
lines changed

packages/cli/test/clone-example.test.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,29 @@
33
// This file is licensed under the MIT License.
44
// License text available at https://opensource.org/licenses/MIT
55

6-
'use strict';
6+
('use strict');
77

88
const promisify = require('../lib/utils').promisify;
99

1010
const cloneExampleFromGitHub = require('../generators/example/clone-example');
1111
const expect = require('@loopback/testlab').expect;
12+
const TestSandbox = require('@loopback/testlab').TestSandbox;
1213
const fs = require('fs');
1314
const glob = promisify(require('glob'));
1415
const path = require('path');
1516
const rimraf = promisify(require('rimraf'));
1617

1718
const VALID_EXAMPLE = 'codehub';
18-
const SANDBOX = path.resolve(__dirname, 'sandbox');
19+
const SANDBOX_PATH = path.resolve(__dirname, 'sandbox');
20+
let sandbox;
1921

2022
describe('cloneExampleFromGitHub', function() {
2123
this.timeout(10000);
22-
24+
before(createSandbox);
2325
beforeEach(resetSandbox);
2426

2527
it('extracts all project files', () => {
26-
return cloneExampleFromGitHub(VALID_EXAMPLE, SANDBOX)
28+
return cloneExampleFromGitHub(VALID_EXAMPLE, SANDBOX_PATH)
2729
.then(outDir => {
2830
return Promise.all([
2931
glob('**', {
@@ -42,7 +44,11 @@ describe('cloneExampleFromGitHub', function() {
4244
});
4345
});
4446

47+
function createSandbox() {
48+
sandbox = new TestSandbox(SANDBOX_PATH);
49+
}
50+
4551
function resetSandbox() {
46-
return rimraf(SANDBOX);
52+
sandbox.reset();
4753
}
4854
});

packages/testlab/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,18 @@
2121
"license": "MIT",
2222
"dependencies": {
2323
"@loopback/openapi-spec": "^4.0.0-alpha.20",
24+
"@types/rimraf": "^2.0.2",
2425
"@types/shot": "^3.4.0",
2526
"@types/sinon": "^2.3.7",
2627
"@types/supertest": "^2.0.0",
2728
"@types/swagger-parser": "^4.0.1",
29+
"rimraf": "^2.6.2",
2830
"shot": "^4.0.3",
2931
"should": "^13.1.3",
3032
"sinon": "^4.1.2",
3133
"supertest": "^3.0.0",
32-
"swagger-parser": "^4.0.1"
34+
"swagger-parser": "^4.0.1",
35+
"util.promisify": "^1.0.0"
3336
},
3437
"devDependencies": {
3538
"@loopback/build": "^4.0.0-alpha.9"
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright IBM Corp. 2017,2018. 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 {tmpdir} from 'os';
7+
import {createHash} from 'crypto';
8+
import {resolve, join, parse} from 'path';
9+
import * as util from 'util';
10+
import {mkdirSync, existsSync, mkdir, copyFile, readFile, writeFile} from 'fs';
11+
const promisify = util.promisify || require('util.promisify/implementation');
12+
const rimrafAsync = promisify(require('rimraf'));
13+
const mkdirAsync = promisify(mkdir);
14+
15+
// tslint:disable-next-line:no-any
16+
let copyFileAsync: any;
17+
if (copyFile) {
18+
copyFileAsync = promisify(copyFile);
19+
} else {
20+
/**
21+
* Taranveer: fs.copyFile wasn't made available till Node 8.5.0. As such this
22+
* polyfill is needed for versions of Node prior to that.
23+
*/
24+
copyFileAsync = async function(src: string, target: string) {
25+
const readFileAsync = promisify(readFile);
26+
const writeFileAsync = promisify(writeFile);
27+
const data = await readFileAsync(src);
28+
await writeFileAsync(target, data);
29+
};
30+
}
31+
32+
/**
33+
* TestSandbox class provides a convenient way to get a reference to a
34+
* sandbox folder in which you can perform operations for testing purposes.
35+
*/
36+
export class TestSandbox {
37+
// Path of the TestSandbox
38+
private path: string;
39+
40+
/**
41+
* Will create a directory if it doesn't already exist. If it exists, you
42+
* still get an instance of the TestSandbox.
43+
*
44+
* @param path Path of the TestSandbox. If relative (it will be resolved relative to cwd()).
45+
*/
46+
constructor(path: string) {
47+
// resolve ensures path is absolute / makes it absolute (relative to cwd())
48+
this.path = resolve(path);
49+
// Create directory if it doesn't already exist.
50+
if (!existsSync(this.path)) {
51+
this.create();
52+
}
53+
}
54+
55+
/**
56+
* Syncronously creates the TestSandbox directory. It is syncronous because
57+
* it is called by the constructor.
58+
*/
59+
private create(): void {
60+
mkdirSync(this.path);
61+
}
62+
63+
/**
64+
* This function ensures a valid instance is being used for operations.
65+
*/
66+
private validateInst() {
67+
if (!this.path) {
68+
throw new Error(
69+
`TestSandbox instance was deleted. Create a new instance.`,
70+
);
71+
}
72+
}
73+
74+
/**
75+
* Returns the path of the TestSandbox
76+
*/
77+
getPath(): string {
78+
this.validateInst();
79+
return this.path;
80+
}
81+
82+
/**
83+
* Resets the TestSandbox. (Remove all files in it).
84+
*/
85+
async reset(): Promise<void> {
86+
this.validateInst();
87+
await rimrafAsync(this.path);
88+
this.create();
89+
}
90+
91+
/**
92+
* Deletes the TestSandbox.
93+
*/
94+
async delete(): Promise<void> {
95+
this.validateInst();
96+
await rimrafAsync(this.path);
97+
delete this.path;
98+
}
99+
100+
/**
101+
* Makes a directory in the TestSandbox
102+
* @param dir Name of directory to create (relative to TestSandbox path)
103+
*/
104+
async mkdir(dir: string): Promise<void> {
105+
this.validateInst();
106+
await mkdirAsync(resolve(this.path, dir));
107+
}
108+
109+
/**
110+
* Copies a file from src to the TestSandbox.
111+
* @param src Absolute path of file to be copied to the TestSandbox
112+
* @param [dest] Optional. Destination filename of the copy operation
113+
* (relative to TestSandbox). Original filename used if not specified.
114+
*/
115+
async copy(src: string, dest?: string): Promise<void> {
116+
this.validateInst();
117+
dest = dest
118+
? resolve(this.path, dest)
119+
: resolve(this.path, parse(src).base);
120+
await copyFileAsync(src, dest);
121+
}
122+
}

packages/testlab/src/testlab.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ export {sinon, SinonSpy};
2020
export * from './client';
2121
export * from './shot';
2222
export * from './validate-api-spec';
23+
export * from './test-sandbox';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Hello World!
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Copyright IBM Corp. 2017,2018. 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 {TestSandbox, expect} from '../..';
7+
import {existsSync, readFile as readFile_} from 'fs';
8+
import {resolve} from 'path';
9+
import {createHash} from 'crypto';
10+
import * as util from 'util';
11+
const promisify = util.promisify || require('util.promisify/implementation');
12+
const rimraf = require('rimraf');
13+
const readFile = promisify(readFile_);
14+
15+
describe('TestSandbox integration tests', () => {
16+
let sandbox: TestSandbox;
17+
let path: string;
18+
const COPY_FILE = 'copy-me.txt';
19+
const COPY_FILE_PATH = resolve(
20+
__dirname,
21+
'../../../test/fixtures',
22+
COPY_FILE,
23+
);
24+
let fileContent: string;
25+
26+
before(getCopyFileContents);
27+
beforeEach(createSandbox);
28+
beforeEach(givenPath);
29+
afterEach(deleteSandbox);
30+
31+
it('returns path of sandbox and it exists', () => {
32+
expect(path).to.be.a.String();
33+
expect(existsSync(path)).to.be.True();
34+
});
35+
36+
it('creates a directory in the sandbox', async () => {
37+
const dir = 'controllers';
38+
await sandbox.mkdir(dir);
39+
expect(existsSync(resolve(path, dir))).to.be.True();
40+
});
41+
42+
it('copies a file to the sandbox', async () => {
43+
await sandbox.copy(COPY_FILE_PATH);
44+
expect(existsSync(resolve(path, COPY_FILE))).to.be.True();
45+
await compareFiles(resolve(path, COPY_FILE));
46+
});
47+
48+
it('copies and renames the file to the sandbox', async () => {
49+
const rename = 'copy.me.js';
50+
await sandbox.copy(COPY_FILE_PATH, rename);
51+
expect(existsSync(resolve(path, COPY_FILE))).to.be.False();
52+
expect(existsSync(resolve(path, rename))).to.be.True();
53+
await compareFiles(resolve(path, rename));
54+
});
55+
56+
it('copies file to a directory', async () => {
57+
const dir = 'test';
58+
await sandbox.mkdir(dir);
59+
const rename = `${dir}/${COPY_FILE}`;
60+
await sandbox.copy(COPY_FILE_PATH, rename);
61+
expect(existsSync(resolve(path, rename))).to.be.True();
62+
await compareFiles(resolve(path, rename));
63+
});
64+
65+
it('deletes the test sandbox', async () => {
66+
await sandbox.delete();
67+
expect(existsSync(path)).to.be.False();
68+
});
69+
70+
describe('after deleting sandbox', () => {
71+
const ERR: string =
72+
'TestSandbox instance was deleted. Create a new instance.';
73+
74+
beforeEach(callSandboxDelete);
75+
76+
it('throws an error when trying to call getPath()', () => {
77+
expect(() => sandbox.getPath()).to.throw(ERR);
78+
});
79+
80+
it('throws an error when trying to call mkdir()', async () => {
81+
await expect(sandbox.mkdir('test')).to.be.rejectedWith(ERR);
82+
});
83+
84+
it('throws an error when trying to call copy()', async () => {
85+
await expect(sandbox.copy(COPY_FILE_PATH)).to.be.rejectedWith(ERR);
86+
});
87+
88+
it('throws an error when trying to call reset()', async () => {
89+
await expect(sandbox.reset()).to.be.rejectedWith(ERR);
90+
});
91+
92+
it('throws an error when trying to call delete() again', async () => {
93+
await expect(sandbox.delete()).to.be.rejectedWith(ERR);
94+
});
95+
});
96+
97+
async function callSandboxDelete() {
98+
await sandbox.delete();
99+
}
100+
101+
async function compareFiles(path1: string) {
102+
const file = await readFile(path1, 'utf8');
103+
expect(file).to.equal(fileContent);
104+
}
105+
106+
function createSandbox() {
107+
sandbox = new TestSandbox(resolve(__dirname, 'sandbox'));
108+
}
109+
110+
function givenPath() {
111+
path = sandbox.getPath();
112+
}
113+
114+
function deleteSandbox() {
115+
if (!existsSync(path)) return;
116+
try {
117+
rimraf.sync(sandbox.getPath());
118+
} catch (err) {
119+
console.log(`Failed to delete sandbox because: ${err}`);
120+
}
121+
}
122+
123+
async function getCopyFileContents() {
124+
fileContent = await readFile(COPY_FILE_PATH, 'utf8');
125+
}
126+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright IBM Corp. 2017,2018. 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 {TestSandbox, expect, sinon} from '../..';
7+
import * as fs from 'fs';
8+
import {resolve} from 'path';
9+
10+
describe('TestSandbox unit tests', () => {
11+
const sandboxPath = resolve(__dirname, 'sandbox');
12+
let sandbox: TestSandbox;
13+
let mkdirSyncSpy: sinon.SinonSpy;
14+
15+
beforeEach(createSpies);
16+
beforeEach(createSandbox);
17+
afterEach(restoreSpies);
18+
19+
it('created a sandbox using mkdirSync', () => {
20+
sinon.assert.calledWith(mkdirSyncSpy, sandboxPath);
21+
});
22+
23+
function createSandbox() {
24+
sandbox = new TestSandbox(sandboxPath);
25+
}
26+
27+
function createSpies() {
28+
mkdirSyncSpy = sinon.spy(fs, 'mkdirSync');
29+
}
30+
31+
function restoreSpies() {
32+
mkdirSyncSpy.restore();
33+
}
34+
});

0 commit comments

Comments
 (0)