mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
test(screenshot): enable screenshot tests w/ Firebase Functions and JWT (#3628)
Set up firebase functions to copy valid data to firebase storage/database Use JasonWebToken to validate data written to firebase Github Status change will be done in another PR Result at: https://material2-screenshots.firebaseapp.com/3628
This commit is contained in:
committed by
Jeremy Elbourn
parent
5f360f9a08
commit
23d18a80b3
+10
-1
@@ -4,6 +4,12 @@ sudo: false
|
||||
node_js:
|
||||
- '6.9.4'
|
||||
|
||||
addons:
|
||||
jwt:
|
||||
# SAUCE_ACCESS_KEY<=secret for FIREBASE_ACCESS_TOKEN to work around travis-ci/travis-ci#7223, unencrypted value in valentine as FIREBASE_ACCESS_TOKEN>
|
||||
# we alias FIREBASE_ACCESS_TOKEN to $SAUCE_ACCESS_KEY in env.sh and set the SAUCE_ACCESS_KEY there
|
||||
- secure: "PKts/IbxuJRWWOEeiGbl8Z9zds0M+hIdCH/g/E4WbQ9yzSvSbdwzfmRfFccQFjxjsrY7+SJMVjsURZy+xUyBpzqgWYHUItnSVqjZb8DlyAU2IXyg8TM9BVLkGGe6k5k4PIFVmfMMMzQwWMM0X0W9w3oYmfHL5egxwSHvf9HIqLolLNXg8sqamIdS5d5KoCXf1c+oRjN/IMBktzNBR6N4OFOZQXVoepXNiIvTWAcTtOPBvFWdKP2n7RVioHKdm4a85aCUpDJp+LYGaLqiQZoRzmzfVTnAhTAPdd4ao5w/+jojrfZIHV55bqYF9rLnQMTneKsiyVNVYJzOLuxmARa/EEKfZld+J3rX4/o4cogrU38YSZF+T7J9g/7CTsnIZ3F6W6m+8iJbIBh55nGOQi5PVe458Q/nGb3fgQd2Z4+6lK9k479H4Ssh/Y7hbVQbepqEVIXzZKqWX6/ZE4iWoR/Q2dm0hySFmmB/R2etixX5JxhnHvgobTYIQ+1liJVp/3YFW1ru64Yg6yz/V291Bhh9g31znmTROCJ/usAmZZaLUqW1TDKnLIMP+M74MF9XERqcWKywXRFwxP4E5uDnx/vAyN49gL+SDfrBUxUtXrTkKZAlglwo9SgA7cOYEPWrionvKcGm87gCBYHFUmXZNQVzh212fpuJYXb/vy0sPDj8La4="
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
@@ -12,11 +18,11 @@ env:
|
||||
global:
|
||||
- LOGS_DIR=/tmp/angular-material2-build/logs
|
||||
- SAUCE_USERNAME=angular-ci
|
||||
- SAUCE_ACCESS_KEY=9b988f434ff8-fbca-8aa4-4ae3-35442987
|
||||
- BROWSER_STACK_USERNAME=angularteam1
|
||||
- BROWSER_STACK_ACCESS_KEY=BWCd4SynLzdDcv8xtzsB
|
||||
- BROWSER_PROVIDER_READY_FILE=/tmp/angular-material2-build/readyfile
|
||||
- BROWSER_PROVIDER_ERROR_FILE=/tmp/angular-material2-build/errorfile
|
||||
|
||||
matrix:
|
||||
# Order: a slower build first, so that we don't occupy an idle travis worker waiting for others to complete.
|
||||
- MODE=lint
|
||||
@@ -32,6 +38,9 @@ matrix:
|
||||
- env: "MODE=saucelabs_optional"
|
||||
- env: "MODE=browserstack_optional"
|
||||
|
||||
before_install:
|
||||
- source ./scripts/ci/env.sh
|
||||
|
||||
install:
|
||||
- npm install
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"firebase": {
|
||||
"apiKey": "AIzaSyBekh5ZSi1vEhaE2qetH4RU91gHmUmpqgg",
|
||||
"authDomain": "material2-screenshots.firebaseapp.com",
|
||||
"databaseURL": "https://material2-screenshots.firebaseio.com",
|
||||
"storageBucket": "material2-screenshots.appspot.com",
|
||||
"messagingSenderId": "975527407245"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
'use strict';
|
||||
|
||||
const firebaseFunctions = require('firebase-functions');
|
||||
const firebaseAdmin = require('firebase-admin');
|
||||
const gcs = require('@google-cloud/storage')();
|
||||
const jwt = require('jsonwebtoken');
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* Data and images handling for Screenshot test.
|
||||
*
|
||||
* All users can post data to temporary folder. These Functions will check the data with JsonWebToken and
|
||||
* move the valid data out of temporary folder.
|
||||
*
|
||||
* For valid data posted to database /$temp/screenshot/reports/$prNumber/$secureToken, move it to
|
||||
* /screenshot/reports/$prNumber.
|
||||
* These are data for screenshot results (success or failure), GitHub PR/commit and TravisCI job information
|
||||
*
|
||||
* For valid image results written to database /$temp/screenshot/images/$prNumber/$secureToken/, save the image
|
||||
* data to image files and upload to google cloud storage under location /screenshots/$prNumber
|
||||
* These are screenshot test result images, and difference images generated from screenshot comparison.
|
||||
*
|
||||
* For golden images uploaded to /goldens, read the data from images files and write the data to Firebase database
|
||||
* under location /screenshot/goldens
|
||||
* Screenshot tests can only read restricted database data with no credentials, and they cannot access
|
||||
* Google Cloud Storage. Therefore we copy the image data to database to make it available to screenshot tests.
|
||||
*
|
||||
* The JWT is stored in the data path, so every write to database needs a valid JWT to be copied to database/storage.
|
||||
* All invalid data will be removed.
|
||||
* The JWT has 3 parts: header, payload and signature. These three parts are joint by '/' in path.
|
||||
*/
|
||||
|
||||
// Initailize the admin app
|
||||
firebaseAdmin.initializeApp(firebaseFunctions.config().firebase);
|
||||
|
||||
/** The valid data types database accepts */
|
||||
const dataTypes = ['filenames', 'commit', 'result', 'sha', 'travis'];
|
||||
|
||||
/** The repo slug. This is used to validate the JWT is sent from correct repo. */
|
||||
const repoSlug = firebaseFunctions.config().repo.slug;
|
||||
|
||||
/** The JWT secret. This is used to validate JWT. */
|
||||
const secret = firebaseFunctions.config().secret.key;
|
||||
|
||||
/** The storage bucket to store the images. The bucket is also used by Firebase Storage. */
|
||||
const bucket = gcs.bucket(firebaseFunctions.config().firebase.storageBucket);
|
||||
|
||||
/** The Json Web Token format. The token is stored in data path. */
|
||||
const jwtFormat = '{jwtHeader}/{jwtPayload}/{jwtSignature}';
|
||||
|
||||
/** The temporary folder name for screenshot data that needs to be validated via JWT. */
|
||||
const tempFolder = '/untrustedInbox';
|
||||
|
||||
/**
|
||||
* Copy valid data from /$temp/screenshot/reports/$prNumber/$secureToken/ to /screenshot/reports/$prNumber
|
||||
* Data copied: filenames(image results names), commit(github PR info),
|
||||
* sha (github PR info), result (true or false for all the tests), travis job number
|
||||
*/
|
||||
const copyDataPath = `${tempFolder}/screenshot/reports/{prNumber}/${jwtFormat}/{dataType}`;
|
||||
exports.copyData = firebaseFunctions.database.ref(copyDataPath).onWrite(event => {
|
||||
const dataType = event.params.dataType;
|
||||
if (dataTypes.includes(dataType)) {
|
||||
return verifyAndCopyScreenshotResult(event, dataType);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Copy valid data from /$temp/screenshot/reports/$prNumber/$secureToken/ to /screenshot/reports/$prNumber
|
||||
* Data copied: test result for each file/test with ${filename}. The value should be true or false.
|
||||
*/
|
||||
const copyDataResultPath = `${tempFolder}/screenshot/reports/{prNumber}/${jwtFormat}/results/{filename}`;
|
||||
exports.copyDataResult = firebaseFunctions.database.ref(copyDataResultPath).onWrite(event => {
|
||||
return verifyAndCopyScreenshotResult(event, `results/${event.params.filename}`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Copy valid data from database /$temp/screenshot/images/$prNumber/$secureToken/ to storage /screenshots/$prNumber
|
||||
* Data copied: test result images. Convert from data to image files in storage.
|
||||
*/
|
||||
const copyImagePath = `${tempFolder}/screenshot/images/{prNumber}/${jwtFormat}/{dataType}/{filename}`;
|
||||
exports.copyImage = firebaseFunctions.database.ref(copyImagePath).onWrite(event => {
|
||||
// Only edit data when it is first created. Exit when the data is deleted.
|
||||
if (event.data.previous.exists() || !event.data.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataType = event.params.dataType;
|
||||
const prNumber = event.params.prNumber;
|
||||
const secureToken = getSecureToken(event);
|
||||
const saveFilename = `${event.params.filename}.screenshot.png`;
|
||||
|
||||
if (dataType != 'diff' && dataType != 'test') {
|
||||
return;
|
||||
}
|
||||
|
||||
return verifySecureToken(secureToken, prNumber).then((payload) => {
|
||||
const tempPath = `/tmp/${dataType}-${saveFilename}`
|
||||
const filePath = `screenshots/${prNumber}/${dataType}/${saveFilename}`;
|
||||
const binaryData = new Buffer(event.data.val(), 'base64').toString('binary');
|
||||
fs.writeFile(tempPath, binaryData, 'binary');
|
||||
return bucket.upload(tempPath, {destination: filePath}).then(() => {
|
||||
// Clear the data in temporary folder after processed.
|
||||
return event.data.ref.parent.set(null);
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.error(`Invalid secure token ${secureToken} ${error}`);
|
||||
return event.data.ref.parent.set(null);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Copy valid goldens from storage /goldens/ to database /screenshot/goldens/
|
||||
* so we can read the goldens without credentials.
|
||||
*/
|
||||
exports.copyGoldens = firebaseFunctions.storage.bucket(firebaseFunctions.config().firebase.storageBucket)
|
||||
.object().onChange(event => {
|
||||
// The filePath should always l ook like "goldens/xxx.png"
|
||||
const filePath = event.data.name;
|
||||
|
||||
// Get the file name.
|
||||
const fileNames = filePath.split('/');
|
||||
if (fileNames.length != 2 && fileNames[0] != 'goldens') {
|
||||
return;
|
||||
}
|
||||
const filenameKey = fileNames[1].replace('.screenshot.png', '');
|
||||
|
||||
// When a gold image is deleted, also delete the corresponding record in the firebase database.
|
||||
if (event.data.resourceState === 'not_exists') {
|
||||
return firebaseAdmin.database().ref(`screenshot/goldens/${filenameKey}`).set(null);
|
||||
}
|
||||
|
||||
// Download file from bucket.
|
||||
const bucket = gcs.bucket(event.data.bucket);
|
||||
const tempFilePath = `/tmp/${fileNames[1]}`;
|
||||
return bucket.file(filePath).download({destination: tempFilePath}).then(() => {
|
||||
const data = fs.readFileSync(tempFilePath);
|
||||
return firebaseAdmin.database().ref(`screenshot/goldens/${filenameKey}`).set(data);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle data written to temporary folder. Validate the JWT and move the data out of
|
||||
* temporary folder if the token is valid.
|
||||
*/
|
||||
function verifyAndCopyScreenshotResult(event, path) {
|
||||
// Only edit data when it is first created. Exit when the data is deleted.
|
||||
if (event.data.previous.exists() || !event.data.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prNumber = event.params.prNumber;
|
||||
const secureToken = getSecureToken(event);
|
||||
const original = event.data.val();
|
||||
|
||||
return verifySecureToken(secureToken, prNumber).then((payload) => {
|
||||
return firebaseAdmin.database().ref().child('screenshot/reports')
|
||||
.child(prNumber).child(path).set(original).then(() => {
|
||||
// Clear the data in temporary folder after processed.
|
||||
return event.data.ref.parent.set(null);
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.error(`Invalid secure token ${secureToken} ${error}`);
|
||||
return event.data.ref.parent.set(null);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the Json Web Token from event params.
|
||||
* In screenshot gulp task the path we use is {jwtHeader}/{jwtPayload}/{jwtSignature}.
|
||||
* Replace '/' with '.' to get the token.
|
||||
*/
|
||||
function getSecureToken(event) {
|
||||
return `${event.params.jwtHeader}.${event.params.jwtPayload}.${event.params.jwtSignature}`;
|
||||
}
|
||||
|
||||
function verifySecureToken(token, prNumber) {
|
||||
return new Promise((resolve, reject) => {
|
||||
jwt.verify(token, secret, {issuer: 'Travis CI, GmbH'}, (err, payload) => {
|
||||
if (err) {
|
||||
reject(err.message || err);
|
||||
} else if (payload.slug !== repoSlug) {
|
||||
reject(`jwt slug invalid. expected: ${repoSlug}`);
|
||||
} else if (payload['pull-request'].toString() !== prNumber) {
|
||||
reject(`jwt pull-request invalid. expected: ${prNumber} actual: ${payload['pull-request']}`);
|
||||
} else {
|
||||
resolve(payload);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "angular-material2-functions",
|
||||
"description": "Angular Material2 screenshot test functions",
|
||||
"dependencies": {
|
||||
"@google-cloud/storage": "^0.8.0",
|
||||
"firebase-admin": "^4.1.3",
|
||||
"firebase-functions": "^0.5.2",
|
||||
"jsonwebtoken": "^7.3.0"
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,7 @@
|
||||
"dgeni-packages": "^0.16.5",
|
||||
"firebase-admin": "^4.1.2",
|
||||
"firebase-tools": "^2.2.1",
|
||||
"firebase": "^3.7.2",
|
||||
"fs-extra": "^2.0.0",
|
||||
"glob": "^7.1.1",
|
||||
"google-cloud": "^0.48.0",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
if [[ ${TRAVIS:-} ]]; then
|
||||
# If FIREBASE_ACCESS_TOKEN not set yet, export the FIREBASE_ACCESS_TOKEN using the JWT token that Travis generated and exported for SAUCE_ACCESS_KEY.
|
||||
# This is a workaround for travis-ci/travis-ci#7223
|
||||
# WARNING: FIREBASE_ACCESS_TOKEN should NOT be printed
|
||||
export FIREBASE_ACCESS_TOKEN=${FIREBASE_ACCESS_TOKEN:-$SAUCE_ACCESS_KEY}
|
||||
|
||||
# - we overwrite the value set by Travis JWT addon here to work around travis-ci/travis-ci#7223 for FIREBASE_ACCESS_TOKEN
|
||||
export SAUCE_ACCESS_KEY=9b988f434ff8-fbca-8aa4-4ae3-35442987
|
||||
fi
|
||||
@@ -57,4 +57,6 @@ task('e2e', sequenceTask(
|
||||
[':test:protractor:setup', 'serve:e2eapp'],
|
||||
':test:protractor',
|
||||
':serve:e2eapp:stop',
|
||||
'screenshots',
|
||||
));
|
||||
|
||||
|
||||
@@ -1,55 +1,68 @@
|
||||
import {task} from 'gulp';
|
||||
import {readdirSync, statSync, existsSync, mkdirp} from 'fs-extra';
|
||||
import {readdirSync, statSync, existsSync, mkdirp, readFileSync, writeFileSync} from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as admin from 'firebase-admin';
|
||||
import {openScreenshotsBucket, openFirebaseScreenshotsDatabase} from '../util/firebase';
|
||||
import * as firebase from 'firebase';
|
||||
import {
|
||||
openScreenshotsBucket,
|
||||
openFirebaseScreenshotsDatabase,
|
||||
connectFirebaseScreenshots} from '../util/firebase';
|
||||
import {setGithubStatus} from '../util/github';
|
||||
|
||||
const imageDiff = require('image-diff');
|
||||
|
||||
// Directory to which untrusted screenshot results are temporarily written
|
||||
// (without authentication required) before they are verified and copied to
|
||||
// the final storage location.
|
||||
const TEMP_FOLDER = 'untrustedInbox';
|
||||
const SCREENSHOT_DIR = './screenshots';
|
||||
const FIREBASE_REPORT = 'screenshot/reports';
|
||||
const FIREBASE_REPORT = `${TEMP_FOLDER}/screenshot/reports`;
|
||||
const FIREBASE_IMAGE = `${TEMP_FOLDER}/screenshot/images`;
|
||||
const FIREBASE_FILELIST = 'screenshot/filenames';
|
||||
|
||||
/** Task which upload screenshots generated from e2e test. */
|
||||
task('screenshots', () => {
|
||||
let prNumber = process.env['TRAVIS_PULL_REQUEST'];
|
||||
if (prNumber) {
|
||||
let database = openFirebaseScreenshotsDatabase();
|
||||
let firebaseApp = connectFirebaseScreenshots();
|
||||
let database = firebaseApp.database();
|
||||
|
||||
return getScreenshotFiles(database)
|
||||
.then((files: any[]) => downloadAllGoldsAndCompare(files, database, prNumber))
|
||||
.then(() => downloadAllGoldsAndCompare(database, prNumber))
|
||||
.then((results: boolean) => updateResult(database, prNumber, results))
|
||||
.then((result: boolean) => updateGithubStatus(prNumber, result))
|
||||
.then(() => uploadScreenshots('diff', prNumber))
|
||||
.then(() => uploadScreenshots('test', prNumber))
|
||||
.then(() => uploadScreenshotsData(database, 'diff', prNumber))
|
||||
.then(() => uploadScreenshotsData(database, 'test', prNumber))
|
||||
.then(() => updateTravis(database, prNumber))
|
||||
.then(() => setScreenFilenames(database, prNumber))
|
||||
.then(() => database.goOffline(), () => database.goOffline());
|
||||
} else if (process.env['TRAVIS']) {
|
||||
// Only update golds and filenames for build
|
||||
let database = openFirebaseScreenshotsDatabase();
|
||||
uploadScreenshots('gold')
|
||||
uploadScreenshots()
|
||||
.then(() => setScreenFilenames(database))
|
||||
.then(() => database.goOffline(), () => database.goOffline());
|
||||
}
|
||||
});
|
||||
|
||||
function updateFileResult(database: admin.database.Database, prNumber: string,
|
||||
function updateFileResult(database: firebase.database.Database, prNumber: string,
|
||||
filenameKey: string, result: boolean) {
|
||||
return getPullRequestRef(database, prNumber).child('results').child(filenameKey).set(result);
|
||||
}
|
||||
|
||||
function updateResult(database: admin.database.Database, prNumber: string, result: boolean) {
|
||||
function updateResult(database: firebase.database.Database, prNumber: string, result: boolean) {
|
||||
return getPullRequestRef(database, prNumber).child('result').set(result).then(() => result);
|
||||
}
|
||||
|
||||
function getPullRequestRef(database: admin.database.Database, prNumber: string) {
|
||||
return database.ref(FIREBASE_REPORT).child(prNumber);
|
||||
function getPullRequestRef(database: firebase.database.Database | admin.database.Database,
|
||||
prNumber: string) {
|
||||
let secureToken = getSecureToken();
|
||||
return database.ref(FIREBASE_REPORT).child(prNumber).child(secureToken);
|
||||
}
|
||||
|
||||
function updateTravis(database: admin.database.Database,
|
||||
function updateTravis(database: firebase.database.Database,
|
||||
prNumber: string) {
|
||||
return database.ref(FIREBASE_REPORT).child(prNumber).update({
|
||||
return getPullRequestRef(database, prNumber).update({
|
||||
commit: process.env['TRAVIS_COMMIT'],
|
||||
sha: process.env['TRAVIS_PULL_REQUEST_SHA'],
|
||||
travis: process.env['TRAVIS_JOB_ID'],
|
||||
@@ -57,11 +70,23 @@ function updateTravis(database: admin.database.Database,
|
||||
}
|
||||
|
||||
/** Get a list of filenames from firebase database. */
|
||||
function getScreenshotFiles(database: admin.database.Database) {
|
||||
let bucket = openScreenshotsBucket();
|
||||
return bucket.getFiles({ prefix: 'golds/' }).then(function(data: any) {
|
||||
return data[0].filter((file: any) => file.name.endsWith('.screenshot.png'));
|
||||
});
|
||||
function getScreenshotFiles(database: firebase.database.Database) {
|
||||
mkdirp(path.join(SCREENSHOT_DIR, `golds`));
|
||||
mkdirp(path.join(SCREENSHOT_DIR, `diff`));
|
||||
|
||||
return database.ref('screenshot/goldens').once('value')
|
||||
.then((snapshot: firebase.database.DataSnapshot) => {
|
||||
let counter = 0;
|
||||
snapshot.forEach((childSnapshot: firebase.database.DataSnapshot) => {
|
||||
let key = childSnapshot.key;
|
||||
let binaryData = new Buffer(childSnapshot.val(), 'base64').toString('binary');
|
||||
writeFileSync(`${SCREENSHOT_DIR}/golds/${key}.screenshot.png`, binaryData, 'binary');
|
||||
counter++;
|
||||
if (counter == snapshot.numChildren()) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}).catch((error: any) => console.log(error));
|
||||
}
|
||||
|
||||
function extractScreenshotName(fileName: string) {
|
||||
@@ -74,50 +99,53 @@ function getLocalScreenshotFiles(dir: string): string[] {
|
||||
.filter((fileName: string) => fileName.endsWith('.screenshot.png'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get processed secure token. The jwt token has 3 parts: header, payload, signature and has format
|
||||
* {jwtHeader}.{jwtPayload}.{jwtSignature}
|
||||
* The three parts is connected by '.', while '.' is not a valid path in firebase database.
|
||||
* Replace all '.' to '/' to make the path valid
|
||||
* Output is {jwtHeader}/{jwtPayload}/{jwtSignature}.
|
||||
* This secure token is used to validate the write access is from our TravisCI under our repo.
|
||||
* All data is written to /$path/$secureToken/$data and after validated the
|
||||
* secure token, the data is moved to /$path/$data in database.
|
||||
*/
|
||||
function getSecureToken() {
|
||||
return process.env['FIREBASE_ACCESS_TOKEN'].replace(/[.]/g, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload screenshots to google cloud storage.
|
||||
* @param prNumber - The key used in firebase. Here it is the PR number.
|
||||
* If there's no prNumber, we will upload images to 'golds/' folder
|
||||
* @param mode - Can be 'test' or 'diff' or 'gold'.
|
||||
* @param mode - Can be 'test' or 'diff' .
|
||||
* If the images are the test results, mode should be 'test'.
|
||||
* If the images are the diff images generated, mode should be 'diff'.
|
||||
* For golds mode should be 'gold'.
|
||||
*/
|
||||
function uploadScreenshots(mode?: 'test' | 'diff' | 'gold', prNumber?: string) {
|
||||
let bucket = openScreenshotsBucket();
|
||||
|
||||
let promises: any[] = [];
|
||||
function uploadScreenshotsData(database: firebase.database.Database,
|
||||
mode: 'test' | 'diff', prNumber: string) {
|
||||
let localDir = mode == 'diff' ? path.join(SCREENSHOT_DIR, 'diff') : SCREENSHOT_DIR;
|
||||
getLocalScreenshotFiles(localDir).forEach((file: string) => {
|
||||
let promises = getLocalScreenshotFiles(localDir).map((file: string) => {
|
||||
let fileName = path.join(localDir, file);
|
||||
let destination = (mode == 'gold' || !prNumber) ?
|
||||
`golds/${file}` : `screenshots/${prNumber}/${mode}/${file}`;
|
||||
promises.push(bucket.upload(fileName, { destination: destination }));
|
||||
let filenameKey = extractScreenshotName(fileName);
|
||||
let secureToken = getSecureToken();
|
||||
let data = readFileSync(fileName);
|
||||
return database.ref(FIREBASE_IMAGE).child(prNumber)
|
||||
.child(secureToken).child(mode).child(filenameKey).set(data);
|
||||
});
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
|
||||
/** Download golds screenshots. */
|
||||
function downloadAllGoldsAndCompare(
|
||||
files: any[], database: admin.database.Database,
|
||||
prNumber: string) {
|
||||
function downloadAllGoldsAndCompare(database: firebase.database.Database, prNumber: string) {
|
||||
|
||||
mkdirp(path.join(SCREENSHOT_DIR, `golds`));
|
||||
mkdirp(path.join(SCREENSHOT_DIR, `diff`));
|
||||
let filenames = getLocalScreenshotFiles(path.join(SCREENSHOT_DIR, `golds`));
|
||||
|
||||
return Promise.all(files.map((file: any) => {
|
||||
return downloadGold(file).then(() => diffScreenshot(file.name, database, prNumber));
|
||||
return Promise.all(filenames.map((filename: string) => {
|
||||
return diffScreenshot(filename, database, prNumber);
|
||||
})).then((results: boolean[]) => results.every((value: boolean) => value == true));
|
||||
}
|
||||
|
||||
/** Download one gold screenshot */
|
||||
function downloadGold(file: any) {
|
||||
return file.download({
|
||||
destination: path.join(SCREENSHOT_DIR, file.name)
|
||||
});
|
||||
}
|
||||
|
||||
function diffScreenshot(filename: string, database: admin.database.Database,
|
||||
function diffScreenshot(filename: string, database: firebase.database.Database,
|
||||
prNumber: string) {
|
||||
// TODO(tinayuangao): Run the downloads and diffs in parallel.
|
||||
filename = path.basename(filename);
|
||||
@@ -151,11 +179,11 @@ function diffScreenshot(filename: string, database: admin.database.Database,
|
||||
* Upload a list of filenames to firebase database as gold.
|
||||
* This is necessary for control panel since google-cloud is not available to client side.
|
||||
*/
|
||||
function setScreenFilenames(database: admin.database.Database,
|
||||
function setScreenFilenames(database: admin.database.Database | firebase.database.Database,
|
||||
prNumber?: string) {
|
||||
let filenames: string[] = getLocalScreenshotFiles(SCREENSHOT_DIR);
|
||||
let filelistDatabase = prNumber ?
|
||||
database.ref(FIREBASE_REPORT).child(prNumber).child('filenames') :
|
||||
getPullRequestRef(database, prNumber).child('filenames') :
|
||||
database.ref(FIREBASE_FILELIST);
|
||||
return filelistDatabase.set(filenames);
|
||||
}
|
||||
@@ -169,3 +197,14 @@ function updateGithubStatus(prNumber: number, result: boolean) {
|
||||
url: `http://material2-screenshots.firebaseapp.com/${prNumber}`
|
||||
});
|
||||
}
|
||||
|
||||
/** Upload screenshots to google cloud storage. */
|
||||
function uploadScreenshots() {
|
||||
let bucket = openScreenshotsBucket();
|
||||
let promises = getLocalScreenshotFiles(SCREENSHOT_DIR).map((file: string) => {
|
||||
let fileName = path.join(SCREENSHOT_DIR, file);
|
||||
let destination = `golds/${file}`;
|
||||
return bucket.upload(fileName, { destination: destination });
|
||||
});
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
const firebaseAdmin = require('firebase-admin');
|
||||
const firebase = require('firebase');
|
||||
const gcloud = require('google-cloud');
|
||||
|
||||
const config = require('../../../functions/config.json');
|
||||
|
||||
/** Opens a connection to the firebase realtime database. */
|
||||
export function openFirebaseDashboardDatabase() {
|
||||
// Initialize the Firebase application with admin credentials.
|
||||
// Initialize the Firebase application with firebaseAdmin credentials.
|
||||
// Credentials need to be for a Service Account, which can be created in the Firebase console.
|
||||
firebaseAdmin.initializeApp({
|
||||
credential: firebaseAdmin.credential.cert({
|
||||
@@ -11,7 +14,7 @@ export function openFirebaseDashboardDatabase() {
|
||||
client_email: 'firebase-adminsdk-ch1ob@material2-dashboard.iam.gserviceaccount.com',
|
||||
// In Travis CI the private key will be incorrect because the line-breaks are escaped.
|
||||
// The line-breaks need to persist in the service account private key.
|
||||
private_key: (process.env['MATERIAL2_FIREBASE_PRIVATE_KEY'] || '').replace(/\\n/g, '\n')
|
||||
private_key: decode(process.env['MATERIAL2_FIREBASE_PRIVATE_KEY'])
|
||||
}),
|
||||
databaseURL: 'https://material2-dashboard.firebaseio.com'
|
||||
});
|
||||
@@ -38,7 +41,7 @@ export function openScreenshotsBucket() {
|
||||
|
||||
/** Opens a connection to the firebase database for screenshots. */
|
||||
export function openFirebaseScreenshotsDatabase() {
|
||||
// Initialize the Firebase application with admin credentials.
|
||||
// Initialize the Firebase application with firebaseAdmin credentials.
|
||||
// Credentials need to be for a Service Account, which can be created in the Firebase console.
|
||||
let screenshotApp = firebaseAdmin.initializeApp({
|
||||
credential: firebaseAdmin.credential.cert({
|
||||
@@ -56,6 +59,14 @@ export function openFirebaseScreenshotsDatabase() {
|
||||
export function decode(str: string): string {
|
||||
// In Travis CI the private key will be incorrect because the line-breaks are escaped.
|
||||
// The line-breaks need to persist in the service account private key.
|
||||
return (str || '').split('\\n').reverse().join('\\n').replace(/\\n/g, '\n');
|
||||
return (str || '').replace(/\\n/g, '\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Open firebase connection for screenshot
|
||||
* This connection is client side connection with no credentials
|
||||
*/
|
||||
export function connectFirebaseScreenshots() {
|
||||
return firebase.initializeApp(config.firebase);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user