-
Notifications
You must be signed in to change notification settings - Fork 940
Add code for managing emulators and running database/firestore emulator tests via yarn commands. #1435
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add code for managing emulators and running database/firestore emulator tests via yarn commands. #1435
Changes from 1 commit
df00b9f
c6a2fd8
ba584ac
8cc5e62
c10402b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
/** | ||
* Copyright 2018 Google Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import { spawn } from 'child-process-promise'; | ||
import * as path from 'path'; | ||
|
||
import { DatabaseEmulator } from './emulators/database-emulator'; | ||
|
||
async function runTest(port: number, namespace: string): Promise<any> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. async is not needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
const options = { | ||
cwd: path.resolve(__dirname, '../../packages/database'), | ||
env: Object.assign({}, process.env, { | ||
RTDB_EMULATOR_PORT: port, | ||
RTDB_EMULATOR_NAMESPACE: namespace | ||
}), | ||
stdio: 'inherit' | ||
}; | ||
return spawn('yarn', ['test'], options); | ||
} | ||
|
||
async function run(): Promise<void> { | ||
const emulator = new DatabaseEmulator(); | ||
try { | ||
await emulator.download(); | ||
await emulator.setUp(); | ||
await emulator.setPublicRules(); | ||
await runTest(emulator.port, emulator.namespace); | ||
} finally { | ||
await emulator.tearDown(); | ||
} | ||
} | ||
|
||
run().catch(err => { | ||
console.error(err); | ||
process.exitCode = 1; | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
/** | ||
* Copyright 2018 Google Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import * as request from 'request'; | ||
|
||
import { Emulator } from './emulator'; | ||
|
||
export class DatabaseEmulator extends Emulator { | ||
constructor(port = 8088, namespace = 'test-emulator') { | ||
super(port, namespace); | ||
this.binaryName = 'database-emulator.jar'; | ||
this.binaryUrl = | ||
'https://storage.googleapis.com/firebase-preview-drop/emulator/firebase-database-emulator-v3.5.0.jar'; | ||
} | ||
|
||
async setPublicRules(): Promise<number> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (possibly a question for @ryanpbrewster) Why don't we have similar rules for Firestore? Do the emulators have different default behavior for rules? Is that intentional? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They do have different behavior, though not for any particularly principled reason. I'm considering having the RTDB emulator start open-by-default. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. async is not needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
console.log('Setting rule {".read": true, ".write": true} to emulator ...'); | ||
return new Promise<number>((resolve, reject) => { | ||
request.put( | ||
{ | ||
uri: `http://localhost:${this.port}/.settings/rules.json?ns=${ | ||
this.namespace | ||
}`, | ||
headers: { Authorization: 'Bearer owner' }, | ||
body: '{ "rules": { ".read": true, ".write": true } }' | ||
}, | ||
(error, response, body) => { | ||
if (error) reject(error); | ||
console.log(`Done setting public rule to emulator: ${body}.`); | ||
resolve(response.statusCode); | ||
} | ||
); | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
/** | ||
* Copyright 2018 Google Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import { spawn } from 'child-process-promise'; | ||
import { ChildProcess } from 'child_process'; | ||
import * as fs from 'fs'; | ||
import * as path from 'path'; | ||
import * as request from 'request'; | ||
import * as tmp from 'tmp'; | ||
|
||
export abstract class Emulator { | ||
binaryName: string; | ||
binaryUrl: string; | ||
binaryPath: string; | ||
|
||
emulator: ChildProcess; | ||
port: number; | ||
namespace: string; | ||
|
||
constructor(port: number, namespace: string) { | ||
this.port = port; | ||
this.namespace = namespace; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. namespace isn't used in this base class, and I think it only makes sense for database, so it should probably live in DatabaseEmulator. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
} | ||
|
||
async download(): Promise<void> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. async is not needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
return new Promise<void>((resolve, reject) => { | ||
tmp.dir((err, dir) => { | ||
if (err) reject(err); | ||
|
||
console.log(`Created temporary directory at [${dir}].`); | ||
const filepath: string = path.resolve(dir, this.binaryName); | ||
const writeStream: fs.WriteStream = fs.createWriteStream(filepath); | ||
|
||
console.log(`Downloading emulator from [${this.binaryUrl}] ...`); | ||
request(this.binaryUrl) | ||
.pipe(writeStream) | ||
.on('finish', () => { | ||
console.log(`Saved emulator binary file to [${filepath}].`); | ||
this.binaryPath = filepath; | ||
resolve(); | ||
}) | ||
.on('error', reject); | ||
}); | ||
}); | ||
} | ||
|
||
async setUp(): Promise<void> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. async is not needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
return new Promise<void>((resolve, reject) => { | ||
const promise: any = spawn( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Generally speaking, any is not allowed. Since typing is not available for interface ChildProcessPromise extends Promise<void> {
childProcess: number;
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
'java', | ||
['-jar', path.basename(this.binaryPath), '--port', this.port], | ||
{ | ||
cwd: path.dirname(this.binaryPath), | ||
stdio: 'inherit' | ||
} | ||
); | ||
promise.catch(reject); | ||
this.emulator = promise.childProcess; | ||
|
||
console.log(`Waiting for emulator to start up ...`); | ||
const timeout = 10; // seconds | ||
const start: number = Date.now(); | ||
|
||
const wait = (resolve, reject) => { | ||
if (Date.now() - start > timeout * 1000) { | ||
reject(`Emulator not ready after ${timeout}s. Exiting ...`); | ||
} else { | ||
console.log(`Ping emulator at [http://localhost:${this.port}] ...`); | ||
request(`http://localhost:${this.port}`, (error, response) => { | ||
if (error && error.code === 'ECONNREFUSED') { | ||
setTimeout(wait, 1000, resolve, reject); | ||
} else if (response) { | ||
// Database and Firestore emulators will return 400 and 200 respectively. | ||
// As long as we get a response back, it means the emulator is ready. | ||
console.log('Emulator has started up successfully!'); | ||
resolve(); | ||
} else { | ||
// This should not happen. | ||
reject({ error, response }); | ||
} | ||
}); | ||
} | ||
}; | ||
setTimeout(wait, 1000, resolve, reject); | ||
}); | ||
} | ||
|
||
async tearDown(): Promise<void> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. async is not needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
if (this.emulator) { | ||
console.log(`Shutting down emulator, pid: [${this.emulator.pid}] ...`); | ||
this.emulator.kill(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
/** | ||
* Copyright 2018 Google Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import { Emulator } from './emulator'; | ||
|
||
export class FirestoreEmulator extends Emulator { | ||
constructor(port = 8087, namespace = 'test-emulator') { | ||
super(port, namespace); | ||
this.binaryName = 'firestore-emulator.jar'; | ||
this.binaryUrl = | ||
'https://storage.googleapis.com/firebase-preview-drop/emulator/cloud-firestore-emulator-v1.2.1.jar'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a little concerned about the hard coded url. If we forgot to update the version, we will eventually become using an out dated emulator binary. @ryanpbrewster Is there any way to always get the latest binary? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was some discussion before on specifying emulator versions. The conclusion seemed to be that it's probably better to have a locked version and update the version manually, than to always get the latest version and have risk that test may fail surprisingly with the new version. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay. That sounds fine to me. Can you please add a comment about it and include a pointer to where the latest emulator can be found? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
/** | ||
* Copyright 2018 Google Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import { spawn } from 'child-process-promise'; | ||
import * as path from 'path'; | ||
|
||
import { FirestoreEmulator } from './emulators/firestore-emulator'; | ||
|
||
async function runTest(port: number, namespace: string): Promise<any> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. namespace is really a database-only term. This should just be projectId. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
const options = { | ||
cwd: path.resolve(__dirname, '../../packages/firestore'), | ||
env: Object.assign({}, process.env, { | ||
FIRESTORE_EMULATOR_PORT: port, | ||
FIRESTORE_EMULATOR_PROJECT_ID: namespace | ||
}), | ||
stdio: 'inherit' | ||
}; | ||
// TODO(b/113267261): Include browser test once WebChannel support is | ||
// ready in Firestore emulator. | ||
return spawn('yarn', ['test:node'], options); | ||
} | ||
|
||
async function run(): Promise<void> { | ||
const emulator = new FirestoreEmulator(); | ||
try { | ||
await emulator.download(); | ||
await emulator.setUp(); | ||
await runTest(emulator.port, emulator.namespace); | ||
} finally { | ||
await emulator.tearDown(); | ||
} | ||
} | ||
|
||
run().catch(err => { | ||
console.error(err); | ||
process.exitCode = 1; | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"extends": "../../config/tsconfig.base.json" | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see from tslint config that we don't want
console.log
. Do we allowconsole.info
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We prevent console.log / console.info because: 1) In the SDK, all logging should go through our official logging mechanism, 2) often developers accidentally leave debug console log statements in their PRs and this makes sure they don't get checked in.
So it's okay to use console logging in tests as long as it's used sparingly and it's intentional.
So in this case, I would just add a tslint suppression comment above it:
// tslint:disable-next-line:ban
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.