/** * Runs the AMP visual diff tests. */ async function performVisualTests() { if (!argv.percy_disabled && (!process.env.PERCY_PROJECT || !process.env.PERCY_TOKEN)) { log('fatal', 'Could not find', colors.cyan('PERCY_PROJECT'), 'and', colors.cyan('PERCY_TOKEN'), 'environment variables'); } setDebuggingLevel(); // Launch a local web server. try { await launchWebServer(); } catch (reason) { log('fatal', `Failed to start a web server: ${reason}`); } if (argv.empty) { await createEmptyBuild(); } else { // Load and parse the config. Use JSON5 due to JSON comments in file. const visualTestsConfig = JSON5.parse( fs.readFileSync( path.resolve(__dirname, '../../../test/visual-diff/visual-tests'), 'utf8')); await runVisualTests( visualTestsConfig.asset_globs, visualTestsConfig.webpages); } }
function updateEnvironment(sets, version) { var eb = sets.eb , opts = sets.opts , envSettings = sets.envSettings , versionLabel = version.ApplicationVersion.VersionLabel , deferred = Q.defer() , envParameter = {}; logger('Start to update enviroment version %s to %s', color.cyan(opts.environmentName), color.cyan(versionLabel)); envParameter.EnvironmentName = opts.environmentName; envParameter.VersionLabel = versionLabel; if (envSettings) { envParameter.OptionSettings = envSettings; } eb.updateEnvironment(envParameter, function(error, result){ if(error) { logger('Fail to update enviroment %s version to %s', color.red(opts.environmentName), color.red(versionLabel)); deferred.reject(error); } else { logger('Deploying enviroment %s version to %s success', color.cyan(opts.environmentName), color.cyan(versionLabel)); deferred.resolve(result); } }); return deferred.promise; }
function createApplicationVersion(sets) { var eb = sets.eb , opts = sets.opts , bucketParam = sets.bucketParam , deferred = Q.defer(); logger('Start to create application version %s to %s', color.cyan(opts.applicationName), color.cyan(opts.versionLabel)); eb.createApplicationVersion({ ApplicationName: opts.applicationName, VersionLabel: opts.versionLabel, Description: opts.description, SourceBundle: { S3Bucket: bucketParam.Bucket, S3Key: bucketParam.Key } }, function(error, version){ if(error) { logger('Fail to create application %s version to %s', color.red(opts.applicationName), color.red(opts.versionLabel)); deferred.reject(error); } else { logger('Create application %s version to %s success', color.cyan(opts.applicationName), color.cyan(opts.versionLabel)); deferred.resolve(version); } }); return deferred.promise; }
/** * Runs the visual tests. * * @param {!Array<string>} assetGlobs an array of glob strings to load assets * from. * @param {!Array<JsonObject>} webpages an array of JSON objects containing * details about the pages to snapshot. */ async function runVisualTests(assetGlobs, webpages) { // Create a Percy client and start a build. const percy = createPercyPuppeteerController(assetGlobs); await percy.startBuild(); const {buildId} = percy; fs.writeFileSync('PERCY_BUILD_ID', buildId); log('info', 'Started Percy build', colors.cyan(buildId)); if (process.env['PERCY_TARGET_COMMIT']) { log('info', 'The Percy build is baselined on top of commit', colors.cyan(shortSha(process.env['PERCY_TARGET_COMMIT']))); } try { // Take the snapshots. await generateSnapshots(percy, webpages); } finally { // Tell Percy we're finished taking snapshots. await percy.finalizeBuild(); } // check if the build failed early. const status = await getBuildStatus(buildId); if (status.state == 'failed') { log('fatal', 'Build', colors.cyan(buildId), 'failed!'); } else { log('info', 'Build', colors.cyan(buildId), 'is now being processed by Percy.'); } }
.then(body => { log(green('INFO:'), 'reported', cyan(`${type}/${action}`), 'to the test-status GitHub App'); if (body.length > 0) { log(green('INFO:'), 'response from test-status was', cyan(body.substr(0, 100))); } }).catch(error => {
/** * @param {boolean} condition * @param {string} field * @param {string} message * @param {string} name * @param {string} found */ function verifyBundle_(condition, field, message, name, found) { if (!condition) { log(colors.red('ERROR:'), colors.cyan(field), message, colors.cyan(name), '\n' + found); process.exit(1); } }
verifyVisualDiffTests: function() { if (!process.env.PERCY_PROJECT || !process.env.PERCY_TOKEN) { console.log( '\n' + fileLogPrefix, 'Could not find environment variables', colors.cyan('PERCY_PROJECT'), 'and', colors.cyan('PERCY_TOKEN') + '. Skipping verification of visual diff tests.'); return; } timedExec('gulp visual-diff --verify_status'); },
function startRollback(sets) { var deferred = Q.defer() , opts = sets.opts; logger('Start to rollback application version %s to %s', color.cyan(opts.applicationName), color.cyan(opts.versionLabel)); deferred.resolve({ApplicationVersion: {VersionLabel: opts.versionLabel}}); return deferred.promise; }
eb.updateEnvironment(envParameter, function(error, result){ if(error) { logger('Fail to update enviroment %s version to %s', color.red(opts.environmentName), color.red(versionLabel)); deferred.reject(error); } else { logger('Deploying enviroment %s version to %s success', color.cyan(opts.environmentName), color.cyan(versionLabel)); deferred.resolve(result); } });
}, function(err, result){ if(err) { deferred.reject(err); return; } logger('Upload success -> %s/%s ', color.cyan(bucketParam.Bucket), color.cyan(bucketParam.Key)); deferred.resolve(result); });
}, function(error, version){ if(error) { logger('Fail to create application %s version to %s', color.red(opts.applicationName), color.red(opts.versionLabel)); deferred.reject(error); } else { logger('Create application %s version to %s success', color.cyan(opts.applicationName), color.cyan(opts.versionLabel)); deferred.resolve(version); } });
async function ensureOrBuildAmpRuntimeInTestMode_() { if (argv.nobuild) { const isInTestMode = /AMP_CONFIG=\{(?:.+,)?"test":true\b/.test( fs.readFileSync('dist/amp.js', 'utf8')); if (!isInTestMode) { log('fatal', 'The AMP runtime was not built in test mode. Run', colors.cyan('gulp build --fortesting'), 'or remove the', colors.cyan('--nobuild'), 'option from this command'); } } else { execOrDie('gulp build --fortesting'); } }
/** * Makes sure that yarn.lock was properly updated. */ function runYarnLockfileCheck() { const localChanges = gitDiffColor(); if (localChanges.includes('yarn.lock')) { console.error(fileLogPrefix, colors.red('ERROR:'), 'This PR did not properly update', colors.cyan('yarn.lock') + '.'); console.error(fileLogPrefix, colors.yellow('NOTE:'), 'To fix this, sync your branch to', colors.cyan('upstream/master') + ', run', colors.cyan('gulp update-packages') + ', and push a new commit containing the changes.'); console.error(fileLogPrefix, 'Expected changes:'); console.log(localChanges); process.exit(1); } }
/** * Sets the AMP config, launches a server, and generates Percy snapshots for a * set of given webpages. * * @param {!Percy} percy a Percy-Puppeteer controller. * @param {!Array<JsonObject>} webpages an array of JSON objects containing * details about the pages to snapshot. */ async function generateSnapshots(percy, webpages) { const numUnfilteredPages = webpages.length; webpages = webpages.filter(webpage => !webpage.flaky); if (numUnfilteredPages != webpages.length) { log('info', 'Skipping', colors.cyan(numUnfilteredPages - webpages.length), 'flaky pages'); } if (argv.grep) { webpages = webpages.filter(webpage => argv.grep.test(webpage.name)); log('info', colors.cyan(`--grep ${argv.grep}`), 'matched', colors.cyan(webpages.length), 'pages'); } // Expand all the interactive tests. Every test should have a base test with // no interactions, and each test that has in interactive tests file should // load those tests here. for (const webpage of webpages) { webpage.tests_ = { '': async() => {}, }; if (webpage.interactive_tests) { Object.assign(webpage.tests_, require(path.resolve(ROOT_DIR, webpage.interactive_tests))); } } const totalTests = webpages.reduce( (numTests, webpage) => numTests + Object.keys(webpage.tests_).length, 0); if (!totalTests) { log('fatal', 'No pages left to test!'); } else { log('info', 'Executing', colors.cyan(totalTests), 'visual diff tests on', colors.cyan(webpages.length), 'pages'); } const browser = await launchBrowser(); if (argv.master) { const page = await newPage(browser); await page.goto( `http://${HOST}:${PORT}/examples/visual-tests/blank-page/blank.html`); await percy.snapshot('Blank page', page, SNAPSHOT_EMPTY_BUILD_OPTIONS); } log('verbose', 'Generating snapshots...'); if (!(await snapshotWebpages(percy, browser, webpages))) { log('fatal', 'Some tests have failed locally.'); } }
/** * Stops connection to Sauce Labs * @param {string} functionName */ function stopSauceConnect(functionName) { const stopScCmd = 'build-system/sauce_connect/stop_sauce_connect.sh'; const fileLogPrefix = colors.bold(colors.yellow(`${functionName}:`)); console.log('\n' + fileLogPrefix, 'Stopping Sauce Connect Proxy:', colors.cyan(stopScCmd)); execOrDie(stopScCmd); }
webServerProcess_.on('close', code => { code = code || 0; if (code != 0) { log('fatal', colors.cyan("'serve'"), `errored with code ${code}. Cannot continue with visual diff tests`); } });
/** * Starts a timer to measure the execution time of the given function. * @param {string} functionName * @param {string} fileName * @return {DOMHighResTimeStamp} */ function startTimer(functionName, fileName) { const startTime = Date.now(); const fileLogPrefix = colors.bold(colors.yellow(`${fileName}:`)); console.log( '\n' + fileLogPrefix, 'Running', colors.cyan(functionName) + '...'); return startTime; }
}, function(error, environment) { if(error) { return deferred.reject(error); } waitState.current = environment; waitState.current.color = color[environment.Color.toLowerCase()] || color.gray; if(waitState.prev) { var _p = waitState.prev.color; var _c = waitState.current.color; logger('Enviroment %s health has transitioned from %s(%s) to %s(%s)', color.cyan(opts.environmentName), _p(waitState.prev.HealthStatus), _p(waitState.prev.Status), _c(environment.HealthStatus), _c(environment.Status) ); } waitState.prev = waitState.current; if(environment.Status === 'Ready') { deferred.resolve(environment); return; } setTimeout( () => { deferred.resolve( waitdeploy(sets, interval) ); }, interval); });
.pipe(eslint.results(function(results) { // TODO(#15255, #14761): Remove log folding after warnings are fixed. if (collapseLintResults) { console./* OK*/log('travis_fold:end:lint_results'); } if (results.errorCount == 0 && results.warningCount == 0) { if (!process.env.TRAVIS) { logOnSameLine(colors.green('SUCCESS: ') + 'No linter warnings or errors.'); } } else { const prefix = results.errorCount == 0 ? colors.yellow('WARNING: ') : colors.red('ERROR: '); logOnSameLine(prefix + 'Found ' + results.errorCount + ' error(s) and ' + results.warningCount + ' warning(s).'); if (!options.fix) { log(colors.yellow('NOTE 1:'), 'You may be able to automatically fix some of these warnings ' + '/ errors by running', colors.cyan('gulp lint --local-changes --fix'), 'from your local branch.'); log(colors.yellow('NOTE 2:'), 'Since this is a destructive operation (that edits your files', 'in-place), make sure you commit before running the command.'); } } }))
/** * Takes all of the nodes in the dependency graph and transfers them * to a temporary directory where we can run babel transformations. * * @param {!Object} graph * @param {!Object} config */ function transformPathsToTempDir(graph, config) { if (!isTravisBuild()) { log('Writing transforms to', colors.cyan(graph.tmp)); } // `sorted` will always have the files that we need. graph.sorted.forEach(f => { // For now, just copy node_module files instead of transforming them. The // exceptions are common JS modules that need to be transformed to ESM // because we now no longer use the process_common_js_modules flag for // closure compiler. if (f.startsWith('node_modules/') && !isCommonJsModule(f)) { fs.copySync(f, `${graph.tmp}/${f}`); } else { const {code} = babel.transformFileSync(f, { plugins: conf.plugins({ isEsmBuild: config.define.indexOf('ESM_BUILD=true') !== -1, isCommonJsModule: isCommonJsModule(f), isForTesting: config.define.indexOf('FORTESTING=true') !== -1, }), retainLines: true, }); fs.outputFileSync(`${graph.tmp}/${f}`, code); } }); }
function installPercy_() { log('info', 'Running', colors.cyan('yarn'), 'to install Percy...'); execOrDie('npx yarn --cwd build-system/tasks/visual-diff', {'stdio': 'ignore'}); puppeteer = require('puppeteer'); Percy = require('@percy/puppeteer').Percy; }
.then(async() => { log('verbose', 'Navigation to page', colors.yellow(name), 'is done, verifying page'); await page.bringToFront(); await verifyCssElements(page, name, webpage.forbidden_css, webpage.loading_incomplete_css, webpage.loading_complete_css); if (webpage.loading_complete_delay_ms) { log('verbose', 'Waiting', colors.cyan(`${webpage.loading_complete_delay_ms}ms`), 'for loading to complete'); await sleep(webpage.loading_complete_delay_ms); } await testFunction(page, name); const snapshotOptions = Object.assign({}, DEFAULT_SNAPSHOT_OPTIONS); if (webpage.enable_percy_javascript) { snapshotOptions.enableJavaScript = true; // Remove all scripts that have an external source, leaving only // those scripts that are inlined in the page inside a <script> // tag. await page.evaluate( 'document.head.querySelectorAll("script[src]").forEach(' + 'node => node./*OK*/remove())'); } if (viewport) { snapshotOptions.widths = [viewport.width]; log('verbose', 'Wrapping viewport-constrained page in an iframe'); await page.evaluate(WRAP_IN_IFRAME_SCRIPT .replace(/__WIDTH__/g, viewport.width) .replace(/__HEIGHT__/g, viewport.height)); await page.setViewport({ width: VIEWPORT_WIDTH, height: VIEWPORT_HEIGHT, }); } await percy.snapshot(name, page, snapshotOptions); log('travis', colors.cyan('●')); })
function startSauceConnect() { process.env['SAUCE_USERNAME'] = '******'; process.env['SAUCE_ACCESS_KEY'] = getStdout('curl --silent ' + 'https://amphtml-sauce-token-dealer.appspot.com/getJwtToken').trim(); const startScCmd = 'build-system/sauce_connect/start_sauce_connect.sh'; console.log('\n' + fileLogPrefix, 'Starting Sauce Connect Proxy:', colors.cyan(startScCmd)); execOrDie(startScCmd); }
/** * Stops the timer for the given function and prints the execution time. * @param {string} functionName * @param {DOMHighResTimeStamp} startTime * @return {number} */ function stopTimer(functionName, startTime) { const endTime = Date.now(); const executionTime = new Date(endTime - startTime); const mins = executionTime.getMinutes(); const secs = executionTime.getSeconds(); console.log( fileLogPrefix, 'Done running', colors.cyan(functionName), 'Total time:', colors.green(mins + 'm ' + secs + 's')); }
/** * Returns a list of files in the commit range within this pull request (PR) * after filtering out commits to master from other PRs. * @return {!Array<string>} */ function filesInPr() { const files = gitDiffNameOnlyMaster(); const changeSummary = gitDiffStatMaster(); console.log(fileLogPrefix, 'Testing the following changes at commit', colors.cyan(process.env.TRAVIS_PULL_REQUEST_SHA)); console.log(changeSummary); return files; }
watcher.on('change', ({path}) => { log('Detected a change in', cyan(path)); log('Running tests...'); // clear file from node require cache if running test again delete require.cache[path]; const mocha = createMocha_(); mocha.files = [path]; mocha.run(); });
gulp.task('task:checkConfig', done => { if(!CONF.TAG){ printError( 'Missing TAG version. Add param ' + colors.cyan('--tag'), '$ npm run gulp default -- --tag="v1.2.4"'); process.exit(0); } done(); });
/** * Verifies that a Percy build succeeded and didn't contain any visual diffs. * @param {!JsonObject} status The eventual status of the Percy build. * @param {string} buildId ID of the Percy build. */ function verifyBuildStatus(status, buildId) { switch (status.state) { case 'finished': if (status.total_comparisons_diff > 0) { if (MASTER_BRANCHES_REGEXP.test(status.branch)) { // If there are visual diffs on master or a release branch, fail // Travis. For master, print instructions for how to approve new // visual changes. if (status.branch == 'master') { log('error', 'Found visual diffs. If the changes are intentional,', 'you must approve the build at', colors.cyan(`${PERCY_BUILD_URL}/${buildId}`), 'in order to update the baseline snapshots.'); } else { log('error', `Found visual diffs on branch ${status.branch}`); } } else { // For PR branches, just print a warning since the diff may be into // intentional, with instructions for how to approve the new snapshots // so they are used as the baseline for future visual diff builds. log('warning', 'Percy build', colors.cyan(buildId), 'contains visual diffs.'); log('warning', 'If they are intentional, you must first approve the', 'build at', colors.cyan(`${PERCY_BUILD_URL}/${buildId}`), 'to allow your PR to be merged.'); } } else { log('info', 'Percy build', colors.cyan(buildId), 'contains no visual diffs.'); } break; case 'pending': case 'processing': log('error', 'Percy build not processed after', `${BUILD_PROCESSING_TIMEOUT_MS}ms`); break; case 'failed': default: log('error', `Percy build failed: ${status.failure_reason}`); break; } }
ftps.raw(`mirror -p --reverse --delete --verbose --ignore-time ${src} ${path}`).exec((err, { error, data }) => { if (error) { reject(error); } else { if (data.length === 0) { log(`${cyan('[remote]')} Nothing to sync`); } resolve(); } }).stdout.on('data', (res) => {
/** * Does a yarn check on node_modules, and if it is outdated, runs yarn. * Follows it up with a call to patch web-animations-js if necessary. */ function updatePackages() { const integrityCmd = 'yarn check --integrity'; if (getStderr(integrityCmd).trim() != '') { log(colors.yellow('WARNING:'), 'The packages in', colors.cyan('node_modules'), 'do not match', colors.cyan('package.json.')); const verifyTreeCmd = 'yarn check --verify-tree'; exec(verifyTreeCmd); log('Running', colors.cyan('yarn'), 'to update packages...'); const yarnCmd = 'yarn'; exec(yarnCmd); } else { if (!process.env.TRAVIS) { log(colors.green('All packages in'), colors.cyan('node_modules'), colors.green('are up to date.')); } } patchWebAnimations(); }