Skip to content

Commit 54df997

Browse files
authored
Add code for managing emulators and running database/firestore emulator tests via yarn commands. (#1435)
Refactor code for managing database/firestore emulators. Add yarn commands for running database/firestore tests with their emulators.
1 parent 088718e commit 54df997

File tree

10 files changed

+329
-150
lines changed

10 files changed

+329
-150
lines changed

.travis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
- script: yarn test:saucelabs --database-firestore-only
3838
# TODO(yifanyang): Once we verify the emulator tests are reliable, we
3939
# should make these tests blocking rather than allow failures.
40-
- script: node scripts/testing/database-emulator-test.js
40+
- script: yarn test:database:emulator
4141
include:
4242
- name: Node.js and Browser (Chrome) Test
4343
stage: test
@@ -53,7 +53,7 @@ jobs:
5353
if: type = push
5454
- name: Database Node.js and Browser (Chrome) Test with Emulator
5555
stage: test
56-
script: node scripts/testing/database-emulator-test.js
56+
script: yarn test:database:emulator
5757
- stage: deploy
5858
script: skip
5959
# NPM Canary Build Config

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
"test:coverage": "lcov-result-merger 'packages/**/lcov.info' | coveralls",
3030
"test:setup": "node tools/config.js",
3131
"pretest:saucelabs": "lerna run --parallel pretest",
32-
"test:saucelabs": "karma start config/karma.saucelabs.js --single-run"
32+
"test:saucelabs": "karma start config/karma.saucelabs.js --single-run",
33+
"test:database:emulator": "ts-node scripts/emulator-testing/database-test-runner.ts",
34+
"test:firestore:emulator": "ts-node scripts/emulator-testing/firestore-test-runner.ts"
3335
},
3436
"repository": {
3537
"type": "git",
@@ -61,6 +63,8 @@
6163
"prettier": "1.12.0",
6264
"semver": "5.5.0",
6365
"simple-git": "1.92.0",
66+
"ts-node": "5.0.1",
67+
"typescript": "2.8.1",
6468
"yargs": "11.0.0"
6569
}
6670
}

packages/firestore/test/integration/util/helpers.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,39 @@ declare const __karma__: any;
2727

2828
const PROJECT_CONFIG = require('../../../../../config/project.json');
2929

30+
const EMULATOR_PORT = process.env.FIRESTORE_EMULATOR_PORT;
31+
const EMULATOR_PROJECT_ID = process.env.FIRESTORE_EMULATOR_PROJECT_ID;
32+
export const USE_EMULATOR = !!EMULATOR_PORT;
33+
34+
const EMULATOR_FIRESTORE_SETTING = {
35+
host: `localhost:${EMULATOR_PORT}`,
36+
ssl: false,
37+
timestampsInSnapshots: true
38+
};
39+
40+
const PROD_FIRESTORE_SETTING = {
41+
host: 'firestore.googleapis.com',
42+
ssl: true,
43+
timestampsInSnapshots: true
44+
};
45+
3046
export const DEFAULT_SETTINGS = getDefaultSettings();
3147

48+
// tslint:disable-next-line:no-console
49+
console.log(`Default Settings: ${JSON.stringify(DEFAULT_SETTINGS)}`);
50+
3251
function getDefaultSettings(): firestore.Settings {
3352
const karma = typeof __karma__ !== 'undefined' ? __karma__ : undefined;
3453
if (karma && karma.config.firestoreSettings) {
3554
return karma.config.firestoreSettings;
3655
} else {
37-
return {
38-
host: 'firestore.googleapis.com',
39-
ssl: true,
40-
timestampsInSnapshots: true
41-
};
56+
return USE_EMULATOR ? EMULATOR_FIRESTORE_SETTING : PROD_FIRESTORE_SETTING;
4257
}
4358
}
4459

45-
export const DEFAULT_PROJECT_ID = PROJECT_CONFIG.projectId;
60+
export const DEFAULT_PROJECT_ID = USE_EMULATOR
61+
? EMULATOR_PROJECT_ID
62+
: PROJECT_CONFIG.projectId;
4663
export const ALT_PROJECT_ID = 'test-db2';
4764

4865
function isIeOrEdge(): boolean {
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Copyright 2018 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { spawn } from 'child-process-promise';
18+
import * as path from 'path';
19+
20+
import { DatabaseEmulator } from './emulators/database-emulator';
21+
import { ChildProcessPromise } from './emulators/emulator';
22+
23+
function runTest(port: number, namespace: string): ChildProcessPromise {
24+
const options = {
25+
cwd: path.resolve(__dirname, '../../packages/database'),
26+
env: Object.assign({}, process.env, {
27+
RTDB_EMULATOR_PORT: port,
28+
RTDB_EMULATOR_NAMESPACE: namespace
29+
}),
30+
stdio: 'inherit'
31+
};
32+
return spawn('yarn', ['test'], options);
33+
}
34+
35+
async function run(): Promise<void> {
36+
const emulator = new DatabaseEmulator();
37+
try {
38+
await emulator.download();
39+
await emulator.setUp();
40+
await emulator.setPublicRules();
41+
await runTest(emulator.port, emulator.namespace);
42+
} finally {
43+
await emulator.tearDown();
44+
}
45+
}
46+
47+
run().catch(err => {
48+
console.error(err);
49+
process.exitCode = 1;
50+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Copyright 2018 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as request from 'request';
18+
19+
import { Emulator } from './emulator';
20+
21+
export class DatabaseEmulator extends Emulator {
22+
namespace: string;
23+
24+
constructor(port = 8088, namespace = 'test-emulator') {
25+
super(port);
26+
this.namespace = namespace;
27+
this.binaryName = 'database-emulator.jar';
28+
// Use locked version of emulator for test to be deterministic.
29+
// The latest version can be found from database emulator doc:
30+
// https://firebase.google.com/docs/database/security/test-rules-emulator
31+
this.binaryUrl =
32+
'https://storage.googleapis.com/firebase-preview-drop/emulator/firebase-database-emulator-v3.5.0.jar';
33+
}
34+
35+
setPublicRules(): Promise<number> {
36+
console.log('Setting rule {".read": true, ".write": true} to emulator ...');
37+
return new Promise<number>((resolve, reject) => {
38+
request.put(
39+
{
40+
uri: `http://localhost:${this.port}/.settings/rules.json?ns=${
41+
this.namespace
42+
}`,
43+
headers: { Authorization: 'Bearer owner' },
44+
body: '{ "rules": { ".read": true, ".write": true } }'
45+
},
46+
(error, response, body) => {
47+
if (error) reject(error);
48+
console.log(`Done setting public rule to emulator: ${body}.`);
49+
resolve(response.statusCode);
50+
}
51+
);
52+
});
53+
}
54+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* Copyright 2018 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { spawn } from 'child-process-promise';
18+
import { ChildProcess } from 'child_process';
19+
import * as fs from 'fs';
20+
import * as path from 'path';
21+
import * as request from 'request';
22+
import * as tmp from 'tmp';
23+
24+
export interface ChildProcessPromise extends Promise<void> {
25+
childProcess: ChildProcess;
26+
}
27+
28+
export abstract class Emulator {
29+
binaryName: string;
30+
binaryUrl: string;
31+
binaryPath: string;
32+
33+
emulator: ChildProcess;
34+
port: number;
35+
36+
constructor(port: number) {
37+
this.port = port;
38+
}
39+
40+
download(): Promise<void> {
41+
return new Promise<void>((resolve, reject) => {
42+
tmp.dir((err, dir) => {
43+
if (err) reject(err);
44+
45+
console.log(`Created temporary directory at [${dir}].`);
46+
const filepath: string = path.resolve(dir, this.binaryName);
47+
const writeStream: fs.WriteStream = fs.createWriteStream(filepath);
48+
49+
console.log(`Downloading emulator from [${this.binaryUrl}] ...`);
50+
request(this.binaryUrl)
51+
.pipe(writeStream)
52+
.on('finish', () => {
53+
console.log(`Saved emulator binary file to [${filepath}].`);
54+
this.binaryPath = filepath;
55+
resolve();
56+
})
57+
.on('error', reject);
58+
});
59+
});
60+
}
61+
62+
setUp(): Promise<void> {
63+
return new Promise<void>((resolve, reject) => {
64+
const promise: ChildProcessPromise = spawn(
65+
'java',
66+
['-jar', path.basename(this.binaryPath), '--port', this.port],
67+
{
68+
cwd: path.dirname(this.binaryPath),
69+
stdio: 'inherit'
70+
}
71+
);
72+
promise.catch(reject);
73+
this.emulator = promise.childProcess;
74+
75+
console.log(`Waiting for emulator to start up ...`);
76+
const timeout = 10; // seconds
77+
const start: number = Date.now();
78+
79+
const wait = (resolve, reject) => {
80+
if (Date.now() - start > timeout * 1000) {
81+
reject(`Emulator not ready after ${timeout}s. Exiting ...`);
82+
} else {
83+
console.log(`Ping emulator at [http://localhost:${this.port}] ...`);
84+
request(`http://localhost:${this.port}`, (error, response) => {
85+
if (error && error.code === 'ECONNREFUSED') {
86+
setTimeout(wait, 1000, resolve, reject);
87+
} else if (response) {
88+
// Database and Firestore emulators will return 400 and 200 respectively.
89+
// As long as we get a response back, it means the emulator is ready.
90+
console.log('Emulator has started up successfully!');
91+
resolve();
92+
} else {
93+
// This should not happen.
94+
reject({ error, response });
95+
}
96+
});
97+
}
98+
};
99+
setTimeout(wait, 1000, resolve, reject);
100+
});
101+
}
102+
103+
tearDown(): void {
104+
if (this.emulator) {
105+
console.log(`Shutting down emulator, pid: [${this.emulator.pid}] ...`);
106+
this.emulator.kill();
107+
}
108+
}
109+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Copyright 2018 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Emulator } from './emulator';
18+
19+
export class FirestoreEmulator extends Emulator {
20+
projectId: string;
21+
22+
constructor(port = 8087, projectId = 'test-emulator') {
23+
super(port);
24+
this.projectId = projectId;
25+
this.binaryName = 'firestore-emulator.jar';
26+
// Use locked version of emulator for test to be deterministic.
27+
// The latest version can be found from firestore emulator doc:
28+
// https://firebase.google.com/docs/firestore/security/test-rules-emulator
29+
this.binaryUrl =
30+
'https://storage.googleapis.com/firebase-preview-drop/emulator/cloud-firestore-emulator-v1.2.1.jar';
31+
}
32+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* Copyright 2018 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { spawn } from 'child-process-promise';
18+
import * as path from 'path';
19+
20+
import { ChildProcessPromise } from './emulators/emulator';
21+
import { FirestoreEmulator } from './emulators/firestore-emulator';
22+
23+
function runTest(port: number, projectId: string): ChildProcessPromise {
24+
const options = {
25+
cwd: path.resolve(__dirname, '../../packages/firestore'),
26+
env: Object.assign({}, process.env, {
27+
FIRESTORE_EMULATOR_PORT: port,
28+
FIRESTORE_EMULATOR_PROJECT_ID: projectId
29+
}),
30+
stdio: 'inherit'
31+
};
32+
// TODO(b/113267261): Include browser test once WebChannel support is
33+
// ready in Firestore emulator.
34+
return spawn('yarn', ['test:node'], options);
35+
}
36+
37+
async function run(): Promise<void> {
38+
const emulator = new FirestoreEmulator();
39+
try {
40+
await emulator.download();
41+
await emulator.setUp();
42+
await runTest(emulator.port, emulator.projectId);
43+
} finally {
44+
await emulator.tearDown();
45+
}
46+
}
47+
48+
run().catch(err => {
49+
console.error(err);
50+
process.exitCode = 1;
51+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "../../config/tsconfig.base.json"
3+
}

0 commit comments

Comments
 (0)