From c524f6dfd52e74eed3638d739ff11f210a59e32f Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Thu, 5 Jan 2017 15:23:50 -0800 Subject: [PATCH 01/16] Add google cloud storage --- tools/gulp/gulpfile.ts | 1 + tools/gulp/task_helpers.ts | 34 ++++++++ tools/gulp/tasks/screenshots.ts | 135 ++++++++++++++++++++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 tools/gulp/tasks/screenshots.ts diff --git a/tools/gulp/gulpfile.ts b/tools/gulp/gulpfile.ts index 02b2e1d0fbc4..d1800911cebc 100644 --- a/tools/gulp/gulpfile.ts +++ b/tools/gulp/gulpfile.ts @@ -12,3 +12,4 @@ import './tasks/unit-test'; import './tasks/docs'; import './tasks/aot'; import './tasks/payload'; +import './tasks/screenshots'; diff --git a/tools/gulp/task_helpers.ts b/tools/gulp/task_helpers.ts index ffeffc59af19..52b113c2f828 100644 --- a/tools/gulp/task_helpers.ts +++ b/tools/gulp/task_helpers.ts @@ -16,6 +16,7 @@ const gulpAutoprefixer = require('gulp-autoprefixer'); const gulpConnect = require('gulp-connect'); const resolveBin = require('resolve-bin'); const firebaseAdmin = require('firebase-admin'); +const gcloud = require('google-cloud'); /** If the string passed in is a glob, returns it, otherwise append '**\/*' to it. */ @@ -207,3 +208,36 @@ export function openFirebaseDatabase() { export function isTravisPushBuild() { return process.env['TRAVIS_PULL_REQUEST'] === 'false'; } + +/** Open Google Cloud Storage for screenshots */ +export function openScreenshotsCloudStorage() { + // Enable Storage + let gcs = gcloud.storage({ + projectId: 'material2-screenshots', + credentials: { + client_email: 'firebase-adminsdk-t4209@material2-screenshots.iam.gserviceaccount.com', + private_key: (process.env['MATERIAL2_SCREENSHOT_FIREBASE_KEY'] || '').replace(/\\n/g, '\n') + }, + }); + + // Reference an existing bucket. + return gcs.bucket('material2-screenshots.appspot.com'); +} + +/** Opens a connection to the firebase realtime database for screenshots. */ +export function openScreenshotsFirebaseDatabase() { + // Initialize the Firebase application with admin 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({ + project_id: 'material2-screenshots', + client_email: 'firebase-adminsdk-t4209@material2-screenshots.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_SCREENSHOT_FIREBASE_KEY'] || '').replace(/\\n/g, '\n') + }), + databaseURL: 'https://material2-screenshots.firebaseio.com' + }, 'material2-screenshots'); + + return screenshotApp.database(); +} diff --git a/tools/gulp/tasks/screenshots.ts b/tools/gulp/tasks/screenshots.ts new file mode 100644 index 000000000000..65adb0da02ef --- /dev/null +++ b/tools/gulp/tasks/screenshots.ts @@ -0,0 +1,135 @@ +import {task} from 'gulp'; +import {readdirSync, statSync, existsSync, mkdirSync} from 'fs'; +import {openScreenshotsCloudStorage, openScreenshotsFirebaseDatabase} from '../task_helpers'; +const imageDiff = require('image-diff'); + +const SCREENSHOT_DIR = './screenshots'; +const FIREBASE_FILELIST = 'screenshot/filenames'; +const FIREBASE_REPORT = 'screenshot/reports'; + +/** Task which upload screenshots generated from e2e test. */ +task('screenshots', () => { + let prNumber = process.env['TRAVIS_PULL_REQUEST']; + if (prNumber) { + let database = openScreenshotsFirebaseDatabase(); + return getFilenameList(database) + .then((filenames: string[]) => { + return downloadReferenceScreenshots(filenames, database) + .then((results: any) => { + return compareScreenshots(filenames, database, prNumber); + }); + }) + .then((results: boolean) => { + return database.ref(FIREBASE_REPORT).child(`${prNumber}/result`).set(results); + }) + .then(() => setFilenameList(database, prNumber)) + .then(() => uploadScreenshots(prNumber, 'diff')) + .then(() => uploadScreenshots(prNumber, 'test')) + .then(() => database.goOffline(), () => database.goOffline()); + } +}); + +/** Get a list of filenames from firebase database. */ +function getFilenameList(database: any) : Promise { + return database.ref(FIREBASE_FILELIST).once('value').then(function(snapshots: any) { + return snapshots.val(); + }); +} + +/** Upload a list of filenames to firebase database as reference. */ +function setFilenameList(database: any, + reportKey?: string): Promise { + let filenames: string[] = []; + readdirSync(SCREENSHOT_DIR).map(function(file) { + let fullName = SCREENSHOT_DIR + '/' + file; + let key = file.replace('.screenshot.png', ''); + if (!statSync(fullName).isDirectory() && key) { + filenames.push(file); + } + }); + let filelistDatabase = reportKey ? + database.ref(FIREBASE_REPORT).child(reportKey).child('filenames') : + database.ref(FIREBASE_FILELIST); + return filelistDatabase.set(filenames); +} + +/** Upload screenshots to google cloud storage. */ +function uploadScreenshots(reportKey?: string, mode?: 'test' | 'diff') { + let bucket = openScreenshotsCloudStorage(); + + let promises: Promise[] = []; + let localDir = mode == 'diff' ? `${SCREENSHOT_DIR}/diff` : SCREENSHOT_DIR; + readdirSync(localDir).map(function(file) { + let fileName = localDir + '/' + file; + let key = file.replace('.screenshot.png', ''); + let destination = (mode == null || !reportKey) ? + `references/${file}` : `screenshots/${reportKey}/${mode}/${file}`; + + if (!statSync(fileName).isDirectory() && key) { + promises.push(bucket.upload(fileName, { destination: destination })); + } + }); + return Promise.all(promises); +} + +/** Check whether the directory exists. If not then create one. */ +function _makeDir(dirName: string) { + if (!existsSync(dirName)) { + mkdirSync(dirName, '744'); + } +} + +/** Download references screenshots. */ +function downloadReferenceScreenshots( + filenames: string[], database: any): Promise { + _makeDir(`${SCREENSHOT_DIR}/references`); + + return Promise.all(filenames.map((filename: string) => { + return _downloadReferenceScreenshot(filename); + })); +} + +/** Download one reference screenshot */ +function _downloadReferenceScreenshot(filename: string): Promise { + let bucket = openScreenshotsCloudStorage(); + return bucket.file(`references/${filename}`).download({ + destination: `${SCREENSHOT_DIR}/references/${filename}` + }); +} + +/** Compare the test result and the reference. */ +function compareScreenshots(filenames: string[], database: any, reportKey: string): Promise { + return Promise.all(filenames.map((filename) => + _compareScreenshot(filename, database, reportKey))) + .then((results: any) => results.every((value: boolean) => value == true)); +} + +function _compareScreenshot(filename: string, database: any, + reportKey: string): Promise { + let expectedUrl = `${SCREENSHOT_DIR}/references/${filename}`; + let actualUrl = `${SCREENSHOT_DIR}/${filename}`; + let diffUrl = `${SCREENSHOT_DIR}/diff/${filename}`; + let filenameKey = filename.replace('.screenshot.png', ''); + + if (existsSync(expectedUrl) && existsSync(actualUrl)) { + return new Promise(function(resolve, reject) { + imageDiff({ + actualImage: actualUrl, + expectedImage: expectedUrl, + diffImage: diffUrl, + }, function (err: any, imagesAreSame: boolean) { + if (err) { + console.log(err); + imagesAreSame = false; + reject(err); + } + resolve(imagesAreSame); + return database.ref(FIREBASE_REPORT).child(`${reportKey}/results/${filenameKey}`) + .set(imagesAreSame); + }); + }); + } else { + return database.ref(FIREBASE_REPORT).child(`${reportKey}/results/${filenameKey}`) + .set(false).then(() => false); + } +} From ba80ecfc4b94d4c542987d5b3b2c722ff7d36cec Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Mon, 23 Jan 2017 15:37:04 -0800 Subject: [PATCH 02/16] Add screenshots to e2e test --- tools/gulp/tasks/e2e.ts | 1 + tools/gulp/tasks/screenshots.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/tools/gulp/tasks/e2e.ts b/tools/gulp/tasks/e2e.ts index b2a2f7af4dd7..46ee68e8415e 100644 --- a/tools/gulp/tasks/e2e.ts +++ b/tools/gulp/tasks/e2e.ts @@ -64,6 +64,7 @@ task('e2e', (done: (err?: string) => void) => { 'serve:e2eapp', ':test:protractor', ':serve:e2eapp:stop', + 'screenshots', (err: any) => done(err) ); }); diff --git a/tools/gulp/tasks/screenshots.ts b/tools/gulp/tasks/screenshots.ts index 65adb0da02ef..7c602811a907 100644 --- a/tools/gulp/tasks/screenshots.ts +++ b/tools/gulp/tasks/screenshots.ts @@ -22,6 +22,7 @@ task('screenshots', () => { .then((results: boolean) => { return database.ref(FIREBASE_REPORT).child(`${prNumber}/result`).set(results); }) + .then(() => database.ref(FIREBASE_REPORT).child(`${prNumber}/commit`).set(process.env['TRAVIS_COMMIT'])) .then(() => setFilenameList(database, prNumber)) .then(() => uploadScreenshots(prNumber, 'diff')) .then(() => uploadScreenshots(prNumber, 'test')) From cbb9dd1e4c49d43e68448b8c8b8fc5696e28151c Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Tue, 24 Jan 2017 17:20:11 -0800 Subject: [PATCH 03/16] Renamed functions. Add types. Remove firebase, use firebase-admin --- tools/gulp/task_helpers.ts | 14 ++-- tools/gulp/tasks/payload.ts | 4 +- tools/gulp/tasks/screenshots.ts | 129 ++++++++++++++++++-------------- 3 files changed, 82 insertions(+), 65 deletions(-) diff --git a/tools/gulp/task_helpers.ts b/tools/gulp/task_helpers.ts index 52b113c2f828..e19018b8cc01 100644 --- a/tools/gulp/task_helpers.ts +++ b/tools/gulp/task_helpers.ts @@ -2,7 +2,6 @@ import * as child_process from 'child_process'; import * as fs from 'fs'; import * as gulp from 'gulp'; import * as path from 'path'; - import {NPM_VENDOR_FILES, PROJECT_ROOT, DIST_ROOT, SASS_AUTOPREFIXER_OPTIONS} from './constants'; @@ -187,7 +186,7 @@ export function sequenceTask(...args: any[]) { } /** Opens a connection to the firebase realtime database. */ -export function openFirebaseDatabase() { +export function openFirebaseDashboardDatabase() { // Initialize the Firebase application with admin credentials. // Credentials need to be for a Service Account, which can be created in the Firebase console. firebaseAdmin.initializeApp({ @@ -209,6 +208,11 @@ export function isTravisPushBuild() { return process.env['TRAVIS_PULL_REQUEST'] === 'false'; } +/** Decode the token for Travis to use. */ +function decode(str: string): string { + return (str || '').split('\\n').reverse().join('\\n').replace(/\\n/g, '\n'); +} + /** Open Google Cloud Storage for screenshots */ export function openScreenshotsCloudStorage() { // Enable Storage @@ -216,7 +220,7 @@ export function openScreenshotsCloudStorage() { projectId: 'material2-screenshots', credentials: { client_email: 'firebase-adminsdk-t4209@material2-screenshots.iam.gserviceaccount.com', - private_key: (process.env['MATERIAL2_SCREENSHOT_FIREBASE_KEY'] || '').replace(/\\n/g, '\n') + private_key: decode(process.env['MATERIAL2_SCREENSHOT_FIREBASE_KEY']) }, }); @@ -225,7 +229,7 @@ export function openScreenshotsCloudStorage() { } /** Opens a connection to the firebase realtime database for screenshots. */ -export function openScreenshotsFirebaseDatabase() { +export function openFirebaseScreenshotsDatabase() { // Initialize the Firebase application with admin credentials. // Credentials need to be for a Service Account, which can be created in the Firebase console. let screenshotApp = firebaseAdmin.initializeApp({ @@ -234,7 +238,7 @@ export function openScreenshotsFirebaseDatabase() { client_email: 'firebase-adminsdk-t4209@material2-screenshots.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_SCREENSHOT_FIREBASE_KEY'] || '').replace(/\\n/g, '\n') + private_key: decode(process.env['MATERIAL2_SCREENSHOT_FIREBASE_KEY']) }), databaseURL: 'https://material2-screenshots.firebaseio.com' }, 'material2-screenshots'); diff --git a/tools/gulp/tasks/payload.ts b/tools/gulp/tasks/payload.ts index ec83a652f259..45ec8bb25238 100644 --- a/tools/gulp/tasks/payload.ts +++ b/tools/gulp/tasks/payload.ts @@ -2,7 +2,7 @@ import {task} from 'gulp'; import {join} from 'path'; import {statSync, readFileSync} from 'fs'; import {DIST_COMPONENTS_ROOT} from '../constants'; -import {openFirebaseDatabase, isTravisPushBuild} from '../task_helpers'; +import {openFirebaseDashboardDatabase, isTravisPushBuild} from '../task_helpers'; import {spawnSync} from 'child_process'; // Those imports lack types. @@ -48,7 +48,7 @@ function getUglifiedSize(filePath: string) { /** Publishes the given results to the firebase database. */ function publishResults(results: any) { let latestSha = spawnSync('git', ['rev-parse', 'HEAD']).stdout.toString().trim(); - let database = openFirebaseDatabase(); + let database = openFirebaseDashboardDatabase(); // Write the results to the payloads object with the latest Git SHA as key. return database.ref('payloads').child(latestSha).set(results) diff --git a/tools/gulp/tasks/screenshots.ts b/tools/gulp/tasks/screenshots.ts index 7c602811a907..9a600d7b016b 100644 --- a/tools/gulp/tasks/screenshots.ts +++ b/tools/gulp/tasks/screenshots.ts @@ -1,6 +1,8 @@ import {task} from 'gulp'; import {readdirSync, statSync, existsSync, mkdirSync} from 'fs'; -import {openScreenshotsCloudStorage, openScreenshotsFirebaseDatabase} from '../task_helpers'; +import {openScreenshotsCloudStorage, openFirebaseScreenshotsDatabase} from '../task_helpers'; +import * as path from 'path'; +import * as admin from 'firebase-admin'; const imageDiff = require('image-diff'); const SCREENSHOT_DIR = './screenshots'; @@ -11,38 +13,48 @@ const FIREBASE_REPORT = 'screenshot/reports'; task('screenshots', () => { let prNumber = process.env['TRAVIS_PULL_REQUEST']; if (prNumber) { - let database = openScreenshotsFirebaseDatabase(); - return getFilenameList(database) - .then((filenames: string[]) => { - return downloadReferenceScreenshots(filenames, database) - .then((results: any) => { - return compareScreenshots(filenames, database, prNumber); - }); - }) - .then((results: boolean) => { - return database.ref(FIREBASE_REPORT).child(`${prNumber}/result`).set(results); - }) - .then(() => database.ref(FIREBASE_REPORT).child(`${prNumber}/commit`).set(process.env['TRAVIS_COMMIT'])) - .then(() => setFilenameList(database, prNumber)) + let database = openFirebaseScreenshotsDatabase(); + return getScreenFilenames(database) + .then((filenames: string[]) => downloadAllGolds(filenames, database, prNumber)) + .then((results: boolean) => updateResult(database, prNumber, results)) + .then(() => setScreenFilenames(database, prNumber)) .then(() => uploadScreenshots(prNumber, 'diff')) .then(() => uploadScreenshots(prNumber, 'test')) + .then(() => updateCommit(database, prNumber)) .then(() => database.goOffline(), () => database.goOffline()); } }); +function updateFileResult(database: admin.database.Database, prNumber: string, + filenameKey: string, result: boolean): admin.Promise{ + return database.ref(FIREBASE_REPORT).child(`${prNumber}/results/${filenameKey}`).set(result); +} + +function updateResult(database: admin.database.Database, prNumber: string, + result: boolean): admin.Promise { + return database.ref(FIREBASE_REPORT).child(`${prNumber}/result`).set(result); +} + +function updateCommit(database: admin.database.Database, + prNumber: string): admin.Promise { + return database.ref(FIREBASE_REPORT).child(`${prNumber}/commit`) + .set(process.env['TRAVIS_COMMIT']); +} + /** Get a list of filenames from firebase database. */ -function getFilenameList(database: any) : Promise { - return database.ref(FIREBASE_FILELIST).once('value').then(function(snapshots: any) { +function getScreenFilenames(database: admin.database.Database): admin.Promise { + return database.ref(FIREBASE_FILELIST).once('value') + .then((snapshots: admin.database.DataSnapshot) => { return snapshots.val(); }); } -/** Upload a list of filenames to firebase database as reference. */ -function setFilenameList(database: any, - reportKey?: string): Promise { +/** Upload a list of filenames to firebase database as gold. */ +function setScreenFilenames(database: admin.database.Database, + reportKey?: string): admin.Promise { let filenames: string[] = []; - readdirSync(SCREENSHOT_DIR).map(function(file) { - let fullName = SCREENSHOT_DIR + '/' + file; + readdirSync(SCREENSHOT_DIR).map((file: string) => { + let fullName = path.join(SCREENSHOT_DIR, file); let key = file.replace('.screenshot.png', ''); if (!statSync(fullName).isDirectory() && key) { filenames.push(file); @@ -54,23 +66,31 @@ function setFilenameList(database: any, return filelistDatabase.set(filenames); } -/** Upload screenshots to google cloud storage. */ +/** + * Upload screenshots to google cloud storage. + * @param {string} reportKey - The key used in firebase. Here it is the PR number. + * If there's no reportKey, we will upload images to 'golds/' folder + * @param {string} mode - Can be 'test' or 'diff' or null. + * 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 null. + */ function uploadScreenshots(reportKey?: string, mode?: 'test' | 'diff') { let bucket = openScreenshotsCloudStorage(); - let promises: Promise[] = []; + let promises: admin.Promise[] = []; let localDir = mode == 'diff' ? `${SCREENSHOT_DIR}/diff` : SCREENSHOT_DIR; - readdirSync(localDir).map(function(file) { - let fileName = localDir + '/' + file; + readdirSync(localDir).map((file: string) => { + let fileName = path.join(localDir, file); let key = file.replace('.screenshot.png', ''); let destination = (mode == null || !reportKey) ? - `references/${file}` : `screenshots/${reportKey}/${mode}/${file}`; + `golds/${file}` : `screenshots/${reportKey}/${mode}/${file}`; if (!statSync(fileName).isDirectory() && key) { promises.push(bucket.upload(fileName, { destination: destination })); } }); - return Promise.all(promises); + return admin.Promise.all(promises); } /** Check whether the directory exists. If not then create one. */ @@ -80,57 +100,50 @@ function _makeDir(dirName: string) { } } -/** Download references screenshots. */ -function downloadReferenceScreenshots( - filenames: string[], database: any): Promise { - _makeDir(`${SCREENSHOT_DIR}/references`); +/** Download golds screenshots. */ +function downloadAllGolds( + filenames: string[], database: admin.database.Database, + reportKey: string): admin.Promise { + _makeDir(`${SCREENSHOT_DIR}/golds`); - return Promise.all(filenames.map((filename: string) => { - return _downloadReferenceScreenshot(filename); - })); + return admin.Promise.all(filenames.map((filename: string) => { + return downloadGold(filename).then(() => diffScreenshot(filename, database, reportKey)); + })).then((results: boolean[]) => results.every((value: boolean) => value == true)); } -/** Download one reference screenshot */ -function _downloadReferenceScreenshot(filename: string): Promise { +/** Download one gold screenshot */ +function downloadGold(filename: string): Promise { let bucket = openScreenshotsCloudStorage(); - return bucket.file(`references/${filename}`).download({ - destination: `${SCREENSHOT_DIR}/references/${filename}` + return bucket.file(`golds/${filename}`).download({ + destination: `${SCREENSHOT_DIR}/golds/${filename}` }); } -/** Compare the test result and the reference. */ -function compareScreenshots(filenames: string[], database: any, reportKey: string): Promise { - return Promise.all(filenames.map((filename) => - _compareScreenshot(filename, database, reportKey))) - .then((results: any) => results.every((value: boolean) => value == true)); -} - -function _compareScreenshot(filename: string, database: any, - reportKey: string): Promise { - let expectedUrl = `${SCREENSHOT_DIR}/references/${filename}`; - let actualUrl = `${SCREENSHOT_DIR}/${filename}`; +function diffScreenshot(filename: string, database: admin.database.Database, + reportKey: string): admin.Promise { + // TODO(tinayuangao): Run the downloads and diffs in parallel. + let goldUrl = `${SCREENSHOT_DIR}/golds/${filename}`; + let pullRequestUrl = `${SCREENSHOT_DIR}/${filename}`; let diffUrl = `${SCREENSHOT_DIR}/diff/${filename}`; let filenameKey = filename.replace('.screenshot.png', ''); - if (existsSync(expectedUrl) && existsSync(actualUrl)) { - return new Promise(function(resolve, reject) { + if (existsSync(goldUrl) && existsSync(pullRequestUrl)) { + return new admin.Promise((resolve: any, reject: any) => { imageDiff({ - actualImage: actualUrl, - expectedImage: expectedUrl, + actualImage: pullRequestUrl, + expectedImage: goldUrl, diffImage: diffUrl, - }, function (err: any, imagesAreSame: boolean) { + }, (err: any, imagesAreSame: boolean) => { if (err) { console.log(err); imagesAreSame = false; reject(err); } resolve(imagesAreSame); - return database.ref(FIREBASE_REPORT).child(`${reportKey}/results/${filenameKey}`) - .set(imagesAreSame); + return updateFileResult(database, reportKey, filenameKey, imagesAreSame); }); }); } else { - return database.ref(FIREBASE_REPORT).child(`${reportKey}/results/${filenameKey}`) - .set(false).then(() => false); + return updateFileResult(database, reportKey, filenameKey, false).then(() => false); } } From 1341c3e8ad4bcfb0c5b2703ff6062a49a24edc15 Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Fri, 27 Jan 2017 12:18:52 -0800 Subject: [PATCH 04/16] dependencies --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 427b8a5f5ff1..1592218ba2d8 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "firebase-tools": "^2.2.1", "fs-extra": "^2.0.0", "glob": "^7.1.1", + "google-cloud": "^0.45.1", "gulp": "^3.9.1", "gulp-autoprefixer": "^3.1.1", "gulp-better-rollup": "^1.0.2", @@ -77,6 +78,7 @@ "gulp-transform": "^1.1.0", "hammerjs": "^2.0.8", "highlight.js": "^9.9.0", + "image-diff": "^1.6.3", "jasmine-core": "^2.5.2", "karma": "^1.4.1", "karma-browserstack-launcher": "^1.2.0", From f42f55ae26618113d9cde8da70088afff72ed0bb Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Wed, 1 Feb 2017 14:10:29 -0800 Subject: [PATCH 05/16] Save pull request sha to firebase db --- tools/gulp/tasks/screenshots.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tools/gulp/tasks/screenshots.ts b/tools/gulp/tasks/screenshots.ts index 9a600d7b016b..8371945a2a47 100644 --- a/tools/gulp/tasks/screenshots.ts +++ b/tools/gulp/tasks/screenshots.ts @@ -20,7 +20,8 @@ task('screenshots', () => { .then(() => setScreenFilenames(database, prNumber)) .then(() => uploadScreenshots(prNumber, 'diff')) .then(() => uploadScreenshots(prNumber, 'test')) - .then(() => updateCommit(database, prNumber)) + .then(() => updateTravisCommit(database, prNumber)) + .then(() => updatePRSha(database, prNumber)) .then(() => database.goOffline(), () => database.goOffline()); } }); @@ -35,12 +36,18 @@ function updateResult(database: admin.database.Database, prNumber: string, return database.ref(FIREBASE_REPORT).child(`${prNumber}/result`).set(result); } -function updateCommit(database: admin.database.Database, +function updateTravisCommit(database: admin.database.Database, prNumber: string): admin.Promise { return database.ref(FIREBASE_REPORT).child(`${prNumber}/commit`) .set(process.env['TRAVIS_COMMIT']); } +function updatePRSha(database: admin.database.Database, + prNumber: string): admin.Promise { + return database.ref(FIREBASE_REPORT).child(`${prNumber}/sha`) + .set(process.env['TRAVIS_PULL_REQUEST_SHA']); +} + /** Get a list of filenames from firebase database. */ function getScreenFilenames(database: admin.database.Database): admin.Promise { return database.ref(FIREBASE_FILELIST).once('value') From 8a6f096416a46111bf20e9ef9f6beb11b5a08817 Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Wed, 1 Feb 2017 16:47:25 -0800 Subject: [PATCH 06/16] Save travis job id --- tools/gulp/tasks/screenshots.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/tools/gulp/tasks/screenshots.ts b/tools/gulp/tasks/screenshots.ts index 8371945a2a47..b73a72a56b00 100644 --- a/tools/gulp/tasks/screenshots.ts +++ b/tools/gulp/tasks/screenshots.ts @@ -20,8 +20,7 @@ task('screenshots', () => { .then(() => setScreenFilenames(database, prNumber)) .then(() => uploadScreenshots(prNumber, 'diff')) .then(() => uploadScreenshots(prNumber, 'test')) - .then(() => updateTravisCommit(database, prNumber)) - .then(() => updatePRSha(database, prNumber)) + .then(() => updateTravis(database, prNumber)) .then(() => database.goOffline(), () => database.goOffline()); } }); @@ -36,16 +35,13 @@ function updateResult(database: admin.database.Database, prNumber: string, return database.ref(FIREBASE_REPORT).child(`${prNumber}/result`).set(result); } -function updateTravisCommit(database: admin.database.Database, +function updateTravis(database: admin.database.Database, prNumber: string): admin.Promise { - return database.ref(FIREBASE_REPORT).child(`${prNumber}/commit`) - .set(process.env['TRAVIS_COMMIT']); -} - -function updatePRSha(database: admin.database.Database, - prNumber: string): admin.Promise { - return database.ref(FIREBASE_REPORT).child(`${prNumber}/sha`) - .set(process.env['TRAVIS_PULL_REQUEST_SHA']); + return database.ref(FIREBASE_REPORT).child(prNumber).update({ + 'commit': process.env['TRAVIS_COMMIT'], + 'sha': process.env['TRAVIS_PULL_REQUEST_SHA'], + 'travis': process.env['TRAVIS_JOB_ID'], + }); } /** Get a list of filenames from firebase database. */ From fddaf14b54a60c97e386744ea0af5177062cba4e Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Thu, 2 Feb 2017 11:56:01 -0800 Subject: [PATCH 07/16] Add function to update github commit status --- tools/gulp/tasks/screenshots.ts | 40 ++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/tools/gulp/tasks/screenshots.ts b/tools/gulp/tasks/screenshots.ts index b73a72a56b00..1f6170c51cd2 100644 --- a/tools/gulp/tasks/screenshots.ts +++ b/tools/gulp/tasks/screenshots.ts @@ -3,6 +3,7 @@ import {readdirSync, statSync, existsSync, mkdirSync} from 'fs'; import {openScreenshotsCloudStorage, openFirebaseScreenshotsDatabase} from '../task_helpers'; import * as path from 'path'; import * as admin from 'firebase-admin'; +const request = require('request'); const imageDiff = require('image-diff'); const SCREENSHOT_DIR = './screenshots'; @@ -17,6 +18,7 @@ task('screenshots', () => { return getScreenFilenames(database) .then((filenames: string[]) => downloadAllGolds(filenames, database, prNumber)) .then((results: boolean) => updateResult(database, prNumber, results)) + .then((result: boolean) => updateGithubStatus(result, prNumber)) .then(() => setScreenFilenames(database, prNumber)) .then(() => uploadScreenshots(prNumber, 'diff')) .then(() => uploadScreenshots(prNumber, 'test')) @@ -32,7 +34,7 @@ function updateFileResult(database: admin.database.Database, prNumber: string, function updateResult(database: admin.database.Database, prNumber: string, result: boolean): admin.Promise { - return database.ref(FIREBASE_REPORT).child(`${prNumber}/result`).set(result); + return database.ref(FIREBASE_REPORT).child(`${prNumber}/result`).set(result).then(() => result); } function updateTravis(database: admin.database.Database, @@ -150,3 +152,39 @@ function diffScreenshot(filename: string, database: admin.database.Database, return updateFileResult(database, reportKey, filenameKey, false).then(() => false); } } + +function decode(value: string): string { + return value.split('').reverse().join(''); +} + +function updateGithubStatus(result: boolean, prNumber: string) { + let state = result ? 'success' : 'failure'; + let sha = process.env['TRAVIS_PULL_REQUEST_SHA']; + let token = decode(process.env['MATERIAL2_GITHUB_STATUS_TOKEN']); + + let data = JSON.stringify({ + "state": state, + "target_url": `http://material2-screenshots.firebaseapp.com/${prNumber}`, + "context": "screenshot-diff", + "description": `Screenshot test ${state}` + }); + + let headers = { + 'Authorization': `token ${token}`, + 'User-Agent': 'ScreenshotDiff/1.0.0', + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(data) + }; + + return new admin.Promise((resolve, reject) => { + request({ + url: `https://api.github.com/repos/angular/material2/statuses/${sha}`, + method: 'POST', + form: data, + headers: headers + }, function (error: any, response: any, body: any){ + resolve(response.statusCode); + console.log(response.statusCode); + }); + }); +} From 14a09498111dc768b0fa7bb28f8a884079e717d0 Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Thu, 2 Feb 2017 15:28:05 -0800 Subject: [PATCH 08/16] remove update status code --- tools/gulp/tasks/screenshots.ts | 38 --------------------------------- 1 file changed, 38 deletions(-) diff --git a/tools/gulp/tasks/screenshots.ts b/tools/gulp/tasks/screenshots.ts index 1f6170c51cd2..b07ad54e9559 100644 --- a/tools/gulp/tasks/screenshots.ts +++ b/tools/gulp/tasks/screenshots.ts @@ -3,7 +3,6 @@ import {readdirSync, statSync, existsSync, mkdirSync} from 'fs'; import {openScreenshotsCloudStorage, openFirebaseScreenshotsDatabase} from '../task_helpers'; import * as path from 'path'; import * as admin from 'firebase-admin'; -const request = require('request'); const imageDiff = require('image-diff'); const SCREENSHOT_DIR = './screenshots'; @@ -18,7 +17,6 @@ task('screenshots', () => { return getScreenFilenames(database) .then((filenames: string[]) => downloadAllGolds(filenames, database, prNumber)) .then((results: boolean) => updateResult(database, prNumber, results)) - .then((result: boolean) => updateGithubStatus(result, prNumber)) .then(() => setScreenFilenames(database, prNumber)) .then(() => uploadScreenshots(prNumber, 'diff')) .then(() => uploadScreenshots(prNumber, 'test')) @@ -152,39 +150,3 @@ function diffScreenshot(filename: string, database: admin.database.Database, return updateFileResult(database, reportKey, filenameKey, false).then(() => false); } } - -function decode(value: string): string { - return value.split('').reverse().join(''); -} - -function updateGithubStatus(result: boolean, prNumber: string) { - let state = result ? 'success' : 'failure'; - let sha = process.env['TRAVIS_PULL_REQUEST_SHA']; - let token = decode(process.env['MATERIAL2_GITHUB_STATUS_TOKEN']); - - let data = JSON.stringify({ - "state": state, - "target_url": `http://material2-screenshots.firebaseapp.com/${prNumber}`, - "context": "screenshot-diff", - "description": `Screenshot test ${state}` - }); - - let headers = { - 'Authorization': `token ${token}`, - 'User-Agent': 'ScreenshotDiff/1.0.0', - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(data) - }; - - return new admin.Promise((resolve, reject) => { - request({ - url: `https://api.github.com/repos/angular/material2/statuses/${sha}`, - method: 'POST', - form: data, - headers: headers - }, function (error: any, response: any, body: any){ - resolve(response.statusCode); - console.log(response.statusCode); - }); - }); -} From ce85e7429b7f49f0fa257953a7e4cf7dbffbc9ce Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Fri, 3 Feb 2017 10:42:00 -0800 Subject: [PATCH 09/16] Add update status --- tools/gulp/tasks/screenshots.ts | 38 +++++++++++++++++++++++++++++++++ tools/gulp/tsconfig.json | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/tools/gulp/tasks/screenshots.ts b/tools/gulp/tasks/screenshots.ts index b07ad54e9559..1f6170c51cd2 100644 --- a/tools/gulp/tasks/screenshots.ts +++ b/tools/gulp/tasks/screenshots.ts @@ -3,6 +3,7 @@ import {readdirSync, statSync, existsSync, mkdirSync} from 'fs'; import {openScreenshotsCloudStorage, openFirebaseScreenshotsDatabase} from '../task_helpers'; import * as path from 'path'; import * as admin from 'firebase-admin'; +const request = require('request'); const imageDiff = require('image-diff'); const SCREENSHOT_DIR = './screenshots'; @@ -17,6 +18,7 @@ task('screenshots', () => { return getScreenFilenames(database) .then((filenames: string[]) => downloadAllGolds(filenames, database, prNumber)) .then((results: boolean) => updateResult(database, prNumber, results)) + .then((result: boolean) => updateGithubStatus(result, prNumber)) .then(() => setScreenFilenames(database, prNumber)) .then(() => uploadScreenshots(prNumber, 'diff')) .then(() => uploadScreenshots(prNumber, 'test')) @@ -150,3 +152,39 @@ function diffScreenshot(filename: string, database: admin.database.Database, return updateFileResult(database, reportKey, filenameKey, false).then(() => false); } } + +function decode(value: string): string { + return value.split('').reverse().join(''); +} + +function updateGithubStatus(result: boolean, prNumber: string) { + let state = result ? 'success' : 'failure'; + let sha = process.env['TRAVIS_PULL_REQUEST_SHA']; + let token = decode(process.env['MATERIAL2_GITHUB_STATUS_TOKEN']); + + let data = JSON.stringify({ + "state": state, + "target_url": `http://material2-screenshots.firebaseapp.com/${prNumber}`, + "context": "screenshot-diff", + "description": `Screenshot test ${state}` + }); + + let headers = { + 'Authorization': `token ${token}`, + 'User-Agent': 'ScreenshotDiff/1.0.0', + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(data) + }; + + return new admin.Promise((resolve, reject) => { + request({ + url: `https://api.github.com/repos/angular/material2/statuses/${sha}`, + method: 'POST', + form: data, + headers: headers + }, function (error: any, response: any, body: any){ + resolve(response.statusCode); + console.log(response.statusCode); + }); + }); +} diff --git a/tools/gulp/tsconfig.json b/tools/gulp/tsconfig.json index 5a09195713a2..db8ad5c60710 100644 --- a/tools/gulp/tsconfig.json +++ b/tools/gulp/tsconfig.json @@ -3,7 +3,7 @@ "declaration": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, - "lib": ["es6", "es2015"], + "lib": ["es6", "es2015", "dom"], "module": "commonjs", "moduleResolution": "node", "noEmitOnError": true, From 1c81b428ef81ffb96c418c7e044856e92da42930 Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Wed, 8 Feb 2017 11:59:36 -0800 Subject: [PATCH 10/16] Address comments --- tools/gulp/task_helpers.ts | 17 ++-- tools/gulp/tasks/screenshots.ts | 158 +++++++++++--------------------- tools/gulp/tsconfig.json | 2 +- tools/gulp/util-functions.ts | 38 ++++++++ 4 files changed, 100 insertions(+), 115 deletions(-) create mode 100644 tools/gulp/util-functions.ts diff --git a/tools/gulp/task_helpers.ts b/tools/gulp/task_helpers.ts index e19018b8cc01..b4af11325479 100644 --- a/tools/gulp/task_helpers.ts +++ b/tools/gulp/task_helpers.ts @@ -208,14 +208,8 @@ export function isTravisPushBuild() { return process.env['TRAVIS_PULL_REQUEST'] === 'false'; } -/** Decode the token for Travis to use. */ -function decode(str: string): string { - return (str || '').split('\\n').reverse().join('\\n').replace(/\\n/g, '\n'); -} - /** Open Google Cloud Storage for screenshots */ -export function openScreenshotsCloudStorage() { - // Enable Storage +export function openScreenshotsBucket() { let gcs = gcloud.storage({ projectId: 'material2-screenshots', credentials: { @@ -236,8 +230,6 @@ export function openFirebaseScreenshotsDatabase() { credential: firebaseAdmin.credential.cert({ project_id: 'material2-screenshots', client_email: 'firebase-adminsdk-t4209@material2-screenshots.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: decode(process.env['MATERIAL2_SCREENSHOT_FIREBASE_KEY']) }), databaseURL: 'https://material2-screenshots.firebaseio.com' @@ -245,3 +237,10 @@ export function openFirebaseScreenshotsDatabase() { return screenshotApp.database(); } + +/** Decode the token for Travis to use. */ +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'); +} diff --git a/tools/gulp/tasks/screenshots.ts b/tools/gulp/tasks/screenshots.ts index 1f6170c51cd2..d2762d550ddb 100644 --- a/tools/gulp/tasks/screenshots.ts +++ b/tools/gulp/tasks/screenshots.ts @@ -1,13 +1,14 @@ import {task} from 'gulp'; -import {readdirSync, statSync, existsSync, mkdirSync} from 'fs'; -import {openScreenshotsCloudStorage, openFirebaseScreenshotsDatabase} from '../task_helpers'; +import {readdirSync, statSync, existsSync, mkdirp} from 'fs-extra'; import * as path from 'path'; import * as admin from 'firebase-admin'; +import {openScreenshotsBucket, openFirebaseScreenshotsDatabase} from '../task_helpers'; +import {updateGithubStatus} from '../util-functions'; + const request = require('request'); const imageDiff = require('image-diff'); const SCREENSHOT_DIR = './screenshots'; -const FIREBASE_FILELIST = 'screenshot/filenames'; const FIREBASE_REPORT = 'screenshot/reports'; /** Task which upload screenshots generated from e2e test. */ @@ -15,11 +16,10 @@ task('screenshots', () => { let prNumber = process.env['TRAVIS_PULL_REQUEST']; if (prNumber) { let database = openFirebaseScreenshotsDatabase(); - return getScreenFilenames(database) - .then((filenames: string[]) => downloadAllGolds(filenames, database, prNumber)) + return getScreenshotFiles(database) + .then((files: any[]) => downloadAllGoldsAndCompare(files, database, prNumber)) .then((results: boolean) => updateResult(database, prNumber, results)) .then((result: boolean) => updateGithubStatus(result, prNumber)) - .then(() => setScreenFilenames(database, prNumber)) .then(() => uploadScreenshots(prNumber, 'diff')) .then(() => uploadScreenshots(prNumber, 'test')) .then(() => updateTravis(database, prNumber)) @@ -29,108 +29,92 @@ task('screenshots', () => { function updateFileResult(database: admin.database.Database, prNumber: string, filenameKey: string, result: boolean): admin.Promise{ - return database.ref(FIREBASE_REPORT).child(`${prNumber}/results/${filenameKey}`).set(result); + return database.ref(FIREBASE_REPORT).child(prNumber).child('results').child(filenameKey).set(result); } function updateResult(database: admin.database.Database, prNumber: string, - result: boolean): admin.Promise { - return database.ref(FIREBASE_REPORT).child(`${prNumber}/result`).set(result).then(() => result); + result: boolean) { + return database.ref(FIREBASE_REPORT).child(prNumber).child('result').set(result).then(() => result); } function updateTravis(database: admin.database.Database, prNumber: string): admin.Promise { return database.ref(FIREBASE_REPORT).child(prNumber).update({ - 'commit': process.env['TRAVIS_COMMIT'], - 'sha': process.env['TRAVIS_PULL_REQUEST_SHA'], - 'travis': process.env['TRAVIS_JOB_ID'], + commit: process.env['TRAVIS_COMMIT'], + sha: process.env['TRAVIS_PULL_REQUEST_SHA'], + travis: process.env['TRAVIS_JOB_ID'], }); } /** Get a list of filenames from firebase database. */ -function getScreenFilenames(database: admin.database.Database): admin.Promise { - return database.ref(FIREBASE_FILELIST).once('value') - .then((snapshots: admin.database.DataSnapshot) => { - return snapshots.val(); +function getScreenshotFiles(database: admin.database.Database): admin.Promise { + let bucket = openScreenshotsBucket(); + return bucket.getFiles({ prefix: 'golds/' }).then(function(data: any) { + return data[0].filter((file:any) => file.name.endsWith('.screenshot.png')); }); } -/** Upload a list of filenames to firebase database as gold. */ -function setScreenFilenames(database: admin.database.Database, - reportKey?: string): admin.Promise { - let filenames: string[] = []; - readdirSync(SCREENSHOT_DIR).map((file: string) => { - let fullName = path.join(SCREENSHOT_DIR, file); - let key = file.replace('.screenshot.png', ''); - if (!statSync(fullName).isDirectory() && key) { - filenames.push(file); - } - }); - let filelistDatabase = reportKey ? - database.ref(FIREBASE_REPORT).child(reportKey).child('filenames') : - database.ref(FIREBASE_FILELIST); - return filelistDatabase.set(filenames); +function extractScreenshotName(fileName: string) { + return path.basename(fileName, '.screenshot.png'); +} + +function getLocalScreenshotFiles(dir: string): string[] { + return readdirSync(dir) + .filter((fileName: string) => !statSync(path.join(SCREENSHOT_DIR, fileName)).isDirectory()) + .filter((fileName: string) => fileName.endsWith('.screenshot.png')); } /** * Upload screenshots to google cloud storage. - * @param {string} reportKey - The key used in firebase. Here it is the PR number. - * If there's no reportKey, we will upload images to 'golds/' folder - * @param {string} mode - Can be 'test' or 'diff' or null. + * @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 null. * 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 null. */ -function uploadScreenshots(reportKey?: string, mode?: 'test' | 'diff') { - let bucket = openScreenshotsCloudStorage(); +function uploadScreenshots(prNumber?: string, mode?: 'test' | 'diff') { + let bucket = openScreenshotsBucket(); let promises: admin.Promise[] = []; - let localDir = mode == 'diff' ? `${SCREENSHOT_DIR}/diff` : SCREENSHOT_DIR; - readdirSync(localDir).map((file: string) => { + let localDir = mode == 'diff' ? path.join(SCREENSHOT_DIR, 'diff') : SCREENSHOT_DIR; + getLocalScreenshotFiles(localDir).forEach((file: string) => { let fileName = path.join(localDir, file); - let key = file.replace('.screenshot.png', ''); - let destination = (mode == null || !reportKey) ? - `golds/${file}` : `screenshots/${reportKey}/${mode}/${file}`; - - if (!statSync(fileName).isDirectory() && key) { - promises.push(bucket.upload(fileName, { destination: destination })); - } + let destination = (mode == null || !prNumber) ? + `golds/${file}` : `screenshots/${prNumber}/${mode}/${file}`; + promises.push(bucket.upload(fileName, { destination: destination })); }); return admin.Promise.all(promises); } -/** Check whether the directory exists. If not then create one. */ -function _makeDir(dirName: string) { - if (!existsSync(dirName)) { - mkdirSync(dirName, '744'); - } -} - /** Download golds screenshots. */ -function downloadAllGolds( - filenames: string[], database: admin.database.Database, - reportKey: string): admin.Promise { - _makeDir(`${SCREENSHOT_DIR}/golds`); +function downloadAllGoldsAndCompare( + files: any[], database: admin.database.Database, + prNumber: string): admin.Promise { + + mkdirp(path.join(SCREENSHOT_DIR, `golds`)); + mkdirp(path.join(SCREENSHOT_DIR, `diff`)); - return admin.Promise.all(filenames.map((filename: string) => { - return downloadGold(filename).then(() => diffScreenshot(filename, database, reportKey)); + return admin.Promise.all(files.map((file: any) => { + return downloadGold(file).then(() => diffScreenshot(file.name, database, prNumber)); })).then((results: boolean[]) => results.every((value: boolean) => value == true)); } /** Download one gold screenshot */ -function downloadGold(filename: string): Promise { - let bucket = openScreenshotsCloudStorage(); - return bucket.file(`golds/${filename}`).download({ - destination: `${SCREENSHOT_DIR}/golds/${filename}` +function downloadGold(file: any): Promise { + return file.download({ + destination: path.join(SCREENSHOT_DIR, file.name) }); } function diffScreenshot(filename: string, database: admin.database.Database, - reportKey: string): admin.Promise { + prNumber: string): admin.Promise { // TODO(tinayuangao): Run the downloads and diffs in parallel. - let goldUrl = `${SCREENSHOT_DIR}/golds/${filename}`; - let pullRequestUrl = `${SCREENSHOT_DIR}/${filename}`; - let diffUrl = `${SCREENSHOT_DIR}/diff/${filename}`; - let filenameKey = filename.replace('.screenshot.png', ''); + filename = path.basename(filename); + let goldUrl = path.join(SCREENSHOT_DIR, `golds`, filename); + let pullRequestUrl = path.join(SCREENSHOT_DIR, filename); + let diffUrl = path.join(SCREENSHOT_DIR, `diff`, filename); + let filenameKey = extractScreenshotName(filename); if (existsSync(goldUrl) && existsSync(pullRequestUrl)) { return new admin.Promise((resolve: any, reject: any) => { @@ -145,46 +129,10 @@ function diffScreenshot(filename: string, database: admin.database.Database, reject(err); } resolve(imagesAreSame); - return updateFileResult(database, reportKey, filenameKey, imagesAreSame); + return updateFileResult(database, prNumber, filenameKey, imagesAreSame); }); }); } else { - return updateFileResult(database, reportKey, filenameKey, false).then(() => false); + return updateFileResult(database, prNumber, filenameKey, false).then(() => false); } } - -function decode(value: string): string { - return value.split('').reverse().join(''); -} - -function updateGithubStatus(result: boolean, prNumber: string) { - let state = result ? 'success' : 'failure'; - let sha = process.env['TRAVIS_PULL_REQUEST_SHA']; - let token = decode(process.env['MATERIAL2_GITHUB_STATUS_TOKEN']); - - let data = JSON.stringify({ - "state": state, - "target_url": `http://material2-screenshots.firebaseapp.com/${prNumber}`, - "context": "screenshot-diff", - "description": `Screenshot test ${state}` - }); - - let headers = { - 'Authorization': `token ${token}`, - 'User-Agent': 'ScreenshotDiff/1.0.0', - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(data) - }; - - return new admin.Promise((resolve, reject) => { - request({ - url: `https://api.github.com/repos/angular/material2/statuses/${sha}`, - method: 'POST', - form: data, - headers: headers - }, function (error: any, response: any, body: any){ - resolve(response.statusCode); - console.log(response.statusCode); - }); - }); -} diff --git a/tools/gulp/tsconfig.json b/tools/gulp/tsconfig.json index db8ad5c60710..5a09195713a2 100644 --- a/tools/gulp/tsconfig.json +++ b/tools/gulp/tsconfig.json @@ -3,7 +3,7 @@ "declaration": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, - "lib": ["es6", "es2015", "dom"], + "lib": ["es6", "es2015"], "module": "commonjs", "moduleResolution": "node", "noEmitOnError": true, diff --git a/tools/gulp/util-functions.ts b/tools/gulp/util-functions.ts new file mode 100644 index 000000000000..aa2fffe27bce --- /dev/null +++ b/tools/gulp/util-functions.ts @@ -0,0 +1,38 @@ +const request = require('request'); +import * as admin from 'firebase-admin'; + +/** Update github pr status to success/failure */ +export function updateGithubStatus(result: boolean, prNumber: string) { + let state = result ? 'success' : 'failure'; + let sha = process.env['TRAVIS_PULL_REQUEST_SHA']; + let token = decode(process.env['MATERIAL2_GITHUB_STATUS_TOKEN']); + + let data = JSON.stringify({ + state: state, + target_url: `http://material2-screenshots.firebaseapp.com/${prNumber}`, + context: "screenshot-diff", + description: `Screenshot test ${state}` + }); + + let headers = { + 'Authorization': `token ${token}`, + 'User-Agent': 'ScreenshotDiff/1.0.0', + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(data) + }; + + return new admin.Promise((resolve, reject) => { + request({ + url: `https://api.github.com/repos/angular/material2/statuses/${sha}`, + method: 'POST', + form: data, + headers: headers + }, function (error: any, response: any, body: any){ + resolve(response.statusCode); + }); + }); +} + +function decode(value: string): string { + return value.split('').reverse().join(''); +} From ae24c9ae897a5fcff63828dcece5b4307a98821b Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Wed, 8 Feb 2017 12:53:45 -0800 Subject: [PATCH 11/16] Add fs-extra --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 1592218ba2d8..302cdd0297bc 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@angular/platform-browser-dynamic": "^2.3.0", "@angular/platform-server": "^2.3.0", "@angular/router": "^3.3.0", + "@types/fs-extra": "0.0.37", "@types/glob": "^5.0.30", "@types/gulp": "^3.8.32", "@types/hammerjs": "^2.0.34", From 0fa76d54561971be1da516c496e4a8c9af8f9798 Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Wed, 8 Feb 2017 13:11:42 -0800 Subject: [PATCH 12/16] try using ES6 Promise --- tools/gulp/tasks/screenshots.ts | 18 +++++++++--------- tools/gulp/util-functions.ts | 3 +-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/tools/gulp/tasks/screenshots.ts b/tools/gulp/tasks/screenshots.ts index d2762d550ddb..49b3030cebcb 100644 --- a/tools/gulp/tasks/screenshots.ts +++ b/tools/gulp/tasks/screenshots.ts @@ -28,7 +28,7 @@ task('screenshots', () => { }); function updateFileResult(database: admin.database.Database, prNumber: string, - filenameKey: string, result: boolean): admin.Promise{ + filenameKey: string, result: boolean) { return database.ref(FIREBASE_REPORT).child(prNumber).child('results').child(filenameKey).set(result); } @@ -38,7 +38,7 @@ function updateResult(database: admin.database.Database, prNumber: string, } function updateTravis(database: admin.database.Database, - prNumber: string): admin.Promise { + prNumber: string) { return database.ref(FIREBASE_REPORT).child(prNumber).update({ commit: process.env['TRAVIS_COMMIT'], sha: process.env['TRAVIS_PULL_REQUEST_SHA'], @@ -47,7 +47,7 @@ function updateTravis(database: admin.database.Database, } /** Get a list of filenames from firebase database. */ -function getScreenshotFiles(database: admin.database.Database): admin.Promise { +function getScreenshotFiles(database: admin.database.Database): Promise { let bucket = openScreenshotsBucket(); return bucket.getFiles({ prefix: 'golds/' }).then(function(data: any) { return data[0].filter((file:any) => file.name.endsWith('.screenshot.png')); @@ -76,7 +76,7 @@ function getLocalScreenshotFiles(dir: string): string[] { function uploadScreenshots(prNumber?: string, mode?: 'test' | 'diff') { let bucket = openScreenshotsBucket(); - let promises: admin.Promise[] = []; + let promises: Promise[] = []; let localDir = mode == 'diff' ? path.join(SCREENSHOT_DIR, 'diff') : SCREENSHOT_DIR; getLocalScreenshotFiles(localDir).forEach((file: string) => { let fileName = path.join(localDir, file); @@ -84,18 +84,18 @@ function uploadScreenshots(prNumber?: string, mode?: 'test' | 'diff') { `golds/${file}` : `screenshots/${prNumber}/${mode}/${file}`; promises.push(bucket.upload(fileName, { destination: destination })); }); - return admin.Promise.all(promises); + return Promise.all(promises); } /** Download golds screenshots. */ function downloadAllGoldsAndCompare( files: any[], database: admin.database.Database, - prNumber: string): admin.Promise { + prNumber: string): Promise { mkdirp(path.join(SCREENSHOT_DIR, `golds`)); mkdirp(path.join(SCREENSHOT_DIR, `diff`)); - return admin.Promise.all(files.map((file: any) => { + return Promise.all(files.map((file: any) => { return downloadGold(file).then(() => diffScreenshot(file.name, database, prNumber)); })).then((results: boolean[]) => results.every((value: boolean) => value == true)); } @@ -108,7 +108,7 @@ function downloadGold(file: any): Promise { } function diffScreenshot(filename: string, database: admin.database.Database, - prNumber: string): admin.Promise { + prNumber: string) { // TODO(tinayuangao): Run the downloads and diffs in parallel. filename = path.basename(filename); let goldUrl = path.join(SCREENSHOT_DIR, `golds`, filename); @@ -117,7 +117,7 @@ function diffScreenshot(filename: string, database: admin.database.Database, let filenameKey = extractScreenshotName(filename); if (existsSync(goldUrl) && existsSync(pullRequestUrl)) { - return new admin.Promise((resolve: any, reject: any) => { + return new Promise((resolve: any, reject: any) => { imageDiff({ actualImage: pullRequestUrl, expectedImage: goldUrl, diff --git a/tools/gulp/util-functions.ts b/tools/gulp/util-functions.ts index aa2fffe27bce..e78f9b94b07d 100644 --- a/tools/gulp/util-functions.ts +++ b/tools/gulp/util-functions.ts @@ -1,5 +1,4 @@ const request = require('request'); -import * as admin from 'firebase-admin'; /** Update github pr status to success/failure */ export function updateGithubStatus(result: boolean, prNumber: string) { @@ -21,7 +20,7 @@ export function updateGithubStatus(result: boolean, prNumber: string) { 'Content-Length': Buffer.byteLength(data) }; - return new admin.Promise((resolve, reject) => { + return new Promise((resolve, reject) => { request({ url: `https://api.github.com/repos/angular/material2/statuses/${sha}`, method: 'POST', From e02d0dea804b21993832e0b4296d889019bfe292 Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Mon, 13 Feb 2017 11:11:57 -0800 Subject: [PATCH 13/16] . --- tools/gulp/tasks/screenshots.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/gulp/tasks/screenshots.ts b/tools/gulp/tasks/screenshots.ts index 49b3030cebcb..f6a08b693266 100644 --- a/tools/gulp/tasks/screenshots.ts +++ b/tools/gulp/tasks/screenshots.ts @@ -76,7 +76,7 @@ function getLocalScreenshotFiles(dir: string): string[] { function uploadScreenshots(prNumber?: string, mode?: 'test' | 'diff') { let bucket = openScreenshotsBucket(); - let promises: Promise[] = []; + let promises: admin.Promise[] = []; let localDir = mode == 'diff' ? path.join(SCREENSHOT_DIR, 'diff') : SCREENSHOT_DIR; getLocalScreenshotFiles(localDir).forEach((file: string) => { let fileName = path.join(localDir, file); @@ -90,12 +90,12 @@ function uploadScreenshots(prNumber?: string, mode?: 'test' | 'diff') { /** Download golds screenshots. */ function downloadAllGoldsAndCompare( files: any[], database: admin.database.Database, - prNumber: string): Promise { + prNumber: string) { mkdirp(path.join(SCREENSHOT_DIR, `golds`)); mkdirp(path.join(SCREENSHOT_DIR, `diff`)); - return Promise.all(files.map((file: any) => { + return admin.Promise.all(files.map((file: any) => { return downloadGold(file).then(() => diffScreenshot(file.name, database, prNumber)); })).then((results: boolean[]) => results.every((value: boolean) => value == true)); } From f87ad8693a8a41fd5a2ad924767d5a5bbe3954df Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Mon, 13 Feb 2017 12:16:54 -0800 Subject: [PATCH 14/16] Fix cannot find name promise --- tools/gulp/gulpfile.ts | 2 +- tools/gulp/tasks/screenshots.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/gulp/gulpfile.ts b/tools/gulp/gulpfile.ts index d1800911cebc..540c73aacfcf 100644 --- a/tools/gulp/gulpfile.ts +++ b/tools/gulp/gulpfile.ts @@ -7,9 +7,9 @@ import './tasks/docs'; import './tasks/e2e'; import './tasks/lint'; import './tasks/release'; +import './tasks/screenshots'; import './tasks/serve'; import './tasks/unit-test'; import './tasks/docs'; import './tasks/aot'; import './tasks/payload'; -import './tasks/screenshots'; diff --git a/tools/gulp/tasks/screenshots.ts b/tools/gulp/tasks/screenshots.ts index f6a08b693266..a528a8338d83 100644 --- a/tools/gulp/tasks/screenshots.ts +++ b/tools/gulp/tasks/screenshots.ts @@ -47,7 +47,7 @@ function updateTravis(database: admin.database.Database, } /** Get a list of filenames from firebase database. */ -function getScreenshotFiles(database: admin.database.Database): Promise { +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')); @@ -76,7 +76,7 @@ function getLocalScreenshotFiles(dir: string): string[] { function uploadScreenshots(prNumber?: string, mode?: 'test' | 'diff') { let bucket = openScreenshotsBucket(); - let promises: admin.Promise[] = []; + let promises: any[] = []; let localDir = mode == 'diff' ? path.join(SCREENSHOT_DIR, 'diff') : SCREENSHOT_DIR; getLocalScreenshotFiles(localDir).forEach((file: string) => { let fileName = path.join(localDir, file); @@ -95,13 +95,13 @@ function downloadAllGoldsAndCompare( mkdirp(path.join(SCREENSHOT_DIR, `golds`)); mkdirp(path.join(SCREENSHOT_DIR, `diff`)); - return admin.Promise.all(files.map((file: any) => { + return Promise.all(files.map((file: any) => { return downloadGold(file).then(() => diffScreenshot(file.name, database, prNumber)); })).then((results: boolean[]) => results.every((value: boolean) => value == true)); } /** Download one gold screenshot */ -function downloadGold(file: any): Promise { +function downloadGold(file: any) { return file.download({ destination: path.join(SCREENSHOT_DIR, file.name) }); From 8482fed42ee29cc5415004398483785fe9624f57 Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Mon, 13 Feb 2017 14:14:24 -0800 Subject: [PATCH 15/16] Add back function to save filenames to firebase --- tools/gulp/tasks/screenshots.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tools/gulp/tasks/screenshots.ts b/tools/gulp/tasks/screenshots.ts index a528a8338d83..539a70493d7a 100644 --- a/tools/gulp/tasks/screenshots.ts +++ b/tools/gulp/tasks/screenshots.ts @@ -10,6 +10,7 @@ const imageDiff = require('image-diff'); const SCREENSHOT_DIR = './screenshots'; const FIREBASE_REPORT = 'screenshot/reports'; +const FIREBASE_FILELIST = 'screenshot/filenames'; /** Task which upload screenshots generated from e2e test. */ task('screenshots', () => { @@ -23,6 +24,7 @@ task('screenshots', () => { .then(() => uploadScreenshots(prNumber, 'diff')) .then(() => uploadScreenshots(prNumber, 'test')) .then(() => updateTravis(database, prNumber)) + .then(() => setScreenFilenames(database, prNumber)) .then(() => database.goOffline(), () => database.goOffline()); } }); @@ -136,3 +138,13 @@ function diffScreenshot(filename: string, database: admin.database.Database, return updateFileResult(database, prNumber, filenameKey, false).then(() => false); } } + +/** Upload a list of filenames to firebase database as gold. */ +function setScreenFilenames(database: admin.database.Database, + prNumber?: string) { + let filenames: string[] = getLocalScreenshotFiles(SCREENSHOT_DIR); + let filelistDatabase = prNumber ? + database.ref(FIREBASE_REPORT).child(prNumber).child('filenames') : + database.ref(FIREBASE_FILELIST); + return filelistDatabase.set(filenames); +} From b82a064f4643fa9a3687c5cc9f4baa0548e6e041 Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Tue, 14 Feb 2017 13:23:33 -0800 Subject: [PATCH 16/16] Update comments --- tools/gulp/task_helpers.ts | 5 ++++- tools/gulp/tasks/screenshots.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/gulp/task_helpers.ts b/tools/gulp/task_helpers.ts index b4af11325479..68f9f8e24b79 100644 --- a/tools/gulp/task_helpers.ts +++ b/tools/gulp/task_helpers.ts @@ -208,7 +208,10 @@ export function isTravisPushBuild() { return process.env['TRAVIS_PULL_REQUEST'] === 'false'; } -/** Open Google Cloud Storage for screenshots */ +/** + * Open Google Cloud Storage for screenshots. + * The files uploaded to google cloud are also available to firebase storage. + */ export function openScreenshotsBucket() { let gcs = gcloud.storage({ projectId: 'material2-screenshots', diff --git a/tools/gulp/tasks/screenshots.ts b/tools/gulp/tasks/screenshots.ts index 539a70493d7a..05143c823d8e 100644 --- a/tools/gulp/tasks/screenshots.ts +++ b/tools/gulp/tasks/screenshots.ts @@ -139,7 +139,10 @@ function diffScreenshot(filename: string, database: admin.database.Database, } } -/** Upload a list of filenames to firebase database as gold. */ +/** + * 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, prNumber?: string) { let filenames: string[] = getLocalScreenshotFiles(SCREENSHOT_DIR);