Skip to content

Commit d2b9268

Browse files
committed
Refactor code for managing database/firestore emulators.
Add yarn commands for running database/firestore tests with their emulators.
1 parent 5509eaf commit d2b9268

File tree

10 files changed

+312
-150
lines changed

10 files changed

+312
-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: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,37 @@ 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();
47+
console.info(`Default Settings: ${JSON.stringify(DEFAULT_SETTINGS)}`);
3148

3249
function getDefaultSettings(): firestore.Settings {
3350
const karma = typeof __karma__ !== 'undefined' ? __karma__ : undefined;
3451
if (karma && karma.config.firestoreSettings) {
3552
return karma.config.firestoreSettings;
3653
} else {
37-
return {
38-
host: 'firestore.googleapis.com',
39-
ssl: true,
40-
timestampsInSnapshots: true
41-
};
54+
return USE_EMULATOR ? EMULATOR_FIRESTORE_SETTING : PROD_FIRESTORE_SETTING;
4255
}
4356
}
4457

45-
export const DEFAULT_PROJECT_ID = PROJECT_CONFIG.projectId;
58+
export const DEFAULT_PROJECT_ID = USE_EMULATOR
59+
? EMULATOR_PROJECT_ID
60+
: PROJECT_CONFIG.projectId;
4661
export const ALT_PROJECT_ID = 'test-db2';
4762

4863
function isIeOrEdge(): boolean {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
22+
async function runTest(port: number, namespace: string): Promise<any> {
23+
const options = {
24+
cwd: path.resolve(__dirname, '../../packages/database'),
25+
env: Object.assign({}, process.env, {
26+
RTDB_EMULATOR_PORT: port,
27+
RTDB_EMULATOR_NAMESPACE: namespace
28+
}),
29+
stdio: 'inherit'
30+
};
31+
return spawn('yarn', ['test'], options);
32+
}
33+
34+
async function run(): Promise<void> {
35+
const emulator = new DatabaseEmulator();
36+
try {
37+
await emulator.download();
38+
await emulator.setUp();
39+
await emulator.setPublicRules();
40+
await runTest(emulator.port, emulator.namespace);
41+
} finally {
42+
await emulator.tearDown();
43+
}
44+
}
45+
46+
run().catch(err => {
47+
console.error(err);
48+
process.exitCode = 1;
49+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
constructor(port = 8088, namespace = 'test-emulator') {
23+
super(port, namespace);
24+
this.binaryName = 'database-emulator.jar';
25+
this.binaryUrl =
26+
'https://storage.googleapis.com/firebase-preview-drop/emulator/firebase-database-emulator-v3.5.0.jar';
27+
}
28+
29+
async setPublicRules(): Promise<number> {
30+
console.log('Setting rule {".read": true, ".write": true} to emulator ...');
31+
return new Promise<number>((resolve, reject) => {
32+
request.put(
33+
{
34+
uri: `http://localhost:${this.port}/.settings/rules.json?ns=${
35+
this.namespace
36+
}`,
37+
headers: { Authorization: 'Bearer owner' },
38+
body: '{ "rules": { ".read": true, ".write": true } }'
39+
},
40+
(error, response, body) => {
41+
if (error) reject(error);
42+
console.log(`Done setting public rule to emulator: ${body}.`);
43+
resolve(response.statusCode);
44+
}
45+
);
46+
});
47+
}
48+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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 abstract class Emulator {
25+
binaryName: string;
26+
binaryUrl: string;
27+
binaryPath: string;
28+
29+
emulator: ChildProcess;
30+
port: number;
31+
namespace: string;
32+
33+
constructor(port: number, namespace: string) {
34+
this.port = port;
35+
this.namespace = namespace;
36+
}
37+
38+
async download(): Promise<void> {
39+
return new Promise<void>((resolve, reject) => {
40+
tmp.dir((err, dir) => {
41+
if (err) reject(err);
42+
43+
console.log(`Created temporary directory at [${dir}].`);
44+
const filepath: string = path.resolve(dir, this.binaryName);
45+
const writeStream: fs.WriteStream = fs.createWriteStream(filepath);
46+
47+
console.log(`Downloading emulator from [${this.binaryUrl}] ...`);
48+
request(this.binaryUrl)
49+
.pipe(writeStream)
50+
.on('finish', () => {
51+
console.log(`Saved emulator binary file to [${filepath}].`);
52+
this.binaryPath = filepath;
53+
resolve();
54+
})
55+
.on('error', reject);
56+
});
57+
});
58+
}
59+
60+
async setUp(): Promise<void> {
61+
return new Promise<void>((resolve, reject) => {
62+
const promise: any = spawn(
63+
'java',
64+
['-jar', path.basename(this.binaryPath), '--port', this.port],
65+
{
66+
cwd: path.dirname(this.binaryPath),
67+
stdio: 'inherit'
68+
}
69+
);
70+
promise.catch(reject);
71+
this.emulator = promise.childProcess;
72+
73+
console.log(`Waiting for emulator to start up ...`);
74+
const timeout = 10; // seconds
75+
const start: number = Date.now();
76+
77+
const wait = (resolve, reject) => {
78+
if (Date.now() - start > timeout * 1000) {
79+
reject(`Emulator not ready after ${timeout}s. Exiting ...`);
80+
} else {
81+
console.log(`Ping emulator at [http://localhost:${this.port}] ...`);
82+
request(`http://localhost:${this.port}`, (error, response) => {
83+
if (error && error.code === 'ECONNREFUSED') {
84+
setTimeout(wait, 1000, resolve, reject);
85+
} else if (response) {
86+
// More information on ways to interact with emulators:
87+
// https://firebase.google.com/docs/database/security/test-rules-emulator
88+
// https://firebase.google.com/docs/firestore/security/test-rules-emulator
89+
console.log('Emulator has started up successfully!');
90+
resolve();
91+
} else {
92+
// This should not happen.
93+
reject({ error, response });
94+
}
95+
});
96+
}
97+
};
98+
setTimeout(wait, 1000, resolve, reject);
99+
});
100+
}
101+
102+
async tearDown(): Promise<void> {
103+
if (this.emulator) {
104+
console.log(`Shutting down emulator, pid: [${this.emulator.pid}] ...`);
105+
this.emulator.kill();
106+
}
107+
}
108+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
constructor(port = 8087, namespace = 'test-emulator') {
21+
super(port, namespace);
22+
this.binaryName = 'firestore-emulator.jar';
23+
this.binaryUrl =
24+
'https://storage.googleapis.com/firebase-preview-drop/emulator/cloud-firestore-emulator-v1.2.1.jar';
25+
}
26+
}
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 { FirestoreEmulator } from './emulators/firestore-emulator';
21+
22+
async function runTest(port: number, namespace: string): Promise<any> {
23+
const options = {
24+
cwd: path.resolve(__dirname, '../../packages/firestore'),
25+
env: Object.assign({}, process.env, {
26+
FIRESTORE_EMULATOR_PORT: port,
27+
FIRESTORE_EMULATOR_PROJECT_ID: namespace
28+
}),
29+
stdio: 'inherit'
30+
};
31+
// Browser test can be enabled once we have WebChannel support
32+
// ready in firestore emulators.
33+
return spawn('yarn', ['test:node'], options);
34+
}
35+
36+
async function run(): Promise<void> {
37+
const emulator = new FirestoreEmulator();
38+
try {
39+
await emulator.download();
40+
await emulator.setUp();
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: 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)