From eec8248844ac865c9f11ce1ad4bfffc5a41ad270 Mon Sep 17 00:00:00 2001 From: Oussama Ben Brahim Date: Sun, 7 Jan 2018 15:27:14 +0100 Subject: [PATCH] Output JUnit XML metadata for CircleCi Fixes #11949 --- .circleci/config.yml | 11 ++ package.json | 2 + scripts/circleci/build.sh | 15 +- scripts/circleci/check_license.sh | 10 +- scripts/circleci/check_modules.sh | 12 +- scripts/circleci/common.sh | 31 +++ scripts/circleci/test_coverage.sh | 2 +- scripts/circleci/test_entry_point.sh | 10 +- scripts/circleci/upload_build.sh | 11 +- scripts/circleci/write_junit_report.sh | 7 + scripts/eslint/index.js | 30 ++- scripts/facts-tracker/index.js | 87 ++++++--- scripts/prettier/index.js | 46 +++-- scripts/rollup/build.js | 8 + scripts/rollup/validate/index.js | 13 +- scripts/shared/__tests__/reporting-test.js | 55 ++++++ scripts/shared/reporting.js | 216 +++++++++++++++++++++ scripts/tasks/flow.js | 40 +++- scripts/tasks/junit.js | 16 ++ scripts/tasks/version-check.js | 20 +- yarn.lock | 34 ++++ 21 files changed, 602 insertions(+), 74 deletions(-) create mode 100644 scripts/circleci/common.sh create mode 100755 scripts/circleci/write_junit_report.sh create mode 100644 scripts/shared/__tests__/reporting-test.js create mode 100644 scripts/shared/reporting.js create mode 100644 scripts/tasks/junit.js diff --git a/.circleci/config.yml b/.circleci/config.yml index 86fd8c7412554..7e9adad89560b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,6 +8,8 @@ jobs: environment: TZ: /usr/share/zoneinfo/America/Los_Angeles TRAVIS_REPO_SLUG: facebook/react + REPORT_FORMATTER: junit + REPORT_DIR: reports/junit parallelism: 4 @@ -34,6 +36,15 @@ jobs: - run: name: Test Packages command: ./scripts/circleci/test_entry_point.sh + environment: + JEST_JUNIT_OUTPUT: "reports/junit/jest.xml" + JEST_PROCESSOR: "jest-junit" + + - store_test_results: + path: reports/junit + + - store_artifacts: + path: reports/junit - save_cache: name: Save node_modules cache diff --git a/package.json b/package.json index b17d950e443e5..8fc12c5df30ec 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,8 @@ "gzip-size": "^3.0.0", "jasmine-check": "^1.0.0-rc.0", "jest": "^22.0.4", + "jest-junit": "^3.4.1", + "junit-merge": "^1.2.3", "merge-stream": "^1.0.0", "minimatch": "^3.0.4", "minimist": "^1.2.0", diff --git a/scripts/circleci/build.sh b/scripts/circleci/build.sh index f186f7674f483..332e2d507138a 100755 --- a/scripts/circleci/build.sh +++ b/scripts/circleci/build.sh @@ -1,17 +1,24 @@ -#!/bin/bash - -set -e +#!/bin/bash + +TEMPORARY_LOG_FILE="local_size_measurements-errors.log" + +. ./scripts/circleci/common.sh # Update the local size measurements to the master version # so that the size diff printed at the end of the build is # accurate. -curl -o scripts/rollup/results.json http://react.zpao.com/builds/master/latest/results.json +process_curl "build" "$REPORT_FORMATTER" "$TEMPORARY_LOG_FILE" curl \ + -sS -o scripts/rollup/results.json http://react.zpao.com/builds/master/latest/results.json + +set -e yarn build --extract-errors + # Note: since we run the full build including extracting error codes, # it is important that we *don't* reset the change to `scripts/error-codes/codes.json`. # When production bundle tests run later, it needs to be available. # See https://github.com/facebook/react/pull/11655. # Do a sanity check on bundles + yarn lint-build diff --git a/scripts/circleci/check_license.sh b/scripts/circleci/check_license.sh index 68b41d6ac441b..82f1895d33a1f 100755 --- a/scripts/circleci/check_license.sh +++ b/scripts/circleci/check_license.sh @@ -6,8 +6,14 @@ set -e EXPECTED='scripts/circleci/check_license.sh' ACTUAL=$(git grep -l PATENTS) +ANNOUNCEMENT=$(echo "PATENTS crept into some new files?") +DIFF=$(diff -u <(echo "$EXPECTED") <(echo "$ACTUAL") || true) +DATA=$ANNOUNCEMENT$DIFF + if [ "$EXPECTED" != "$ACTUAL" ]; then - echo "PATENTS crept into some new files?" - diff -u <(echo "$EXPECTED") <(echo "$ACTUAL") || true + echo "$DATA" + if [ "$REPORT_FORMATTER" = "junit" ]; then + ./scripts/circleci/write_junit_report.sh "check_license" "$DATA" false + fi exit 1 fi diff --git a/scripts/circleci/check_modules.sh b/scripts/circleci/check_modules.sh index 9f900beb027f3..9710f1a598aa4 100755 --- a/scripts/circleci/check_modules.sh +++ b/scripts/circleci/check_modules.sh @@ -10,12 +10,18 @@ packages/shared/ReactTypes.js scripts/rollup/wrappers.js' ACTUAL=$(git grep -l @providesModule -- './*.js' ':!scripts/rollup/shims/*.js') -# Colors +# Colors^ red=$'\e[1;31m' end=$'\e[0m' +ANNOUNCEMENT=$(printf "%s\n" "${red}ERROR: @providesModule crept into some new files?${end}") +DIFF=$(diff -u <(echo "$EXPECTED") <(echo "$ACTUAL") || true) +DATA=$ANNOUNCEMENT$DIFF + if [ "$EXPECTED" != "$ACTUAL" ]; then - printf "%s\n" "${red}ERROR: @providesModule crept into some new files?${end}" - diff -u <(echo "$EXPECTED") <(echo "$ACTUAL") || true + echo "$DATA" + if [ "$REPORT_FORMATTER" = "junit" ]; then + ./scripts/circleci/write_junit_report.sh "check_modules" "$DATA" false + fi exit 1 fi diff --git a/scripts/circleci/common.sh b/scripts/circleci/common.sh new file mode 100644 index 0000000000000..797c4db9c700d --- /dev/null +++ b/scripts/circleci/common.sh @@ -0,0 +1,31 @@ + +redirect_stderr() { + REPORT_FORMATTER=$1 + TEMPORARY_LOG_FILE=$2 + + if [ "$REPORT_FORMATTER" = "junit" ]; then + "${@:3}" 2> "$TEMPORARY_LOG_FILE" + else + "${@:3}" + fi +} + +process_curl() { + BUILD_STEP=$1 + REPORT_FORMATTER=$2 + TEMPORARY_LOG_FILE=$3 + + redirect_stderr "$REPORT_FORMATTER" "$TEMPORARY_LOG_FILE" "${@:4}" + + if [ "$REPORT_FORMATTER" = "junit" ]; then + ERROR=$(cat "$TEMPORARY_LOG_FILE") + if [ "$ERROR" != "" ];then + echo $ERROR + ./scripts/circleci/write_junit_report.sh "$BUILD_STEP" "$ERROR" false + fi + rm -f $TEMPORARY_LOG_FILE + if [ "$ERROR" != "" ];then + exit 1 + fi + fi +} diff --git a/scripts/circleci/test_coverage.sh b/scripts/circleci/test_coverage.sh index c6730a50b0342..764e672317fcf 100755 --- a/scripts/circleci/test_coverage.sh +++ b/scripts/circleci/test_coverage.sh @@ -2,7 +2,7 @@ set -e -yarn test --coverage --runInBand +yarn test --coverage --runInBand --testResultsProcessor=${JEST_PROCESSOR} if [ -z $CI_PULL_REQUEST ]; then cat ./coverage/lcov.info | ./node_modules/.bin/coveralls fi diff --git a/scripts/circleci/test_entry_point.sh b/scripts/circleci/test_entry_point.sh index f16a2a78e652d..bfd98836fd499 100755 --- a/scripts/circleci/test_entry_point.sh +++ b/scripts/circleci/test_entry_point.sh @@ -7,10 +7,10 @@ set -e COMMANDS_TO_RUN=() if [ $((0 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then - COMMANDS_TO_RUN+=('node ./scripts/prettier/index') + COMMANDS_TO_RUN+=('node ./scripts/prettier/index check') COMMANDS_TO_RUN+=('node ./scripts/tasks/flow') COMMANDS_TO_RUN+=('node ./scripts/tasks/eslint') - COMMANDS_TO_RUN+=('yarn test --runInBand') + COMMANDS_TO_RUN+=("yarn test --runInBand --testResultsProcessor=${JEST_PROCESSOR}") COMMANDS_TO_RUN+=('./scripts/circleci/check_license.sh') COMMANDS_TO_RUN+=('./scripts/circleci/check_modules.sh') COMMANDS_TO_RUN+=('./scripts/circleci/test_print_warnings.sh') @@ -18,13 +18,13 @@ if [ $((0 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then fi if [ $((1 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then - COMMANDS_TO_RUN+=('yarn test-prod --runInBand') + COMMANDS_TO_RUN+=("yarn test-prod --runInBand --testResultsProcessor=${JEST_PROCESSOR}") fi if [ $((2 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then COMMANDS_TO_RUN+=('./scripts/circleci/build.sh') - COMMANDS_TO_RUN+=('yarn test-build --runInBand') - COMMANDS_TO_RUN+=('yarn test-build-prod --runInBand') + COMMANDS_TO_RUN+=("yarn test-build --runInBand --testResultsProcessor=${JEST_PROCESSOR}") + COMMANDS_TO_RUN+=("yarn test-build-prod --runInBand --testResultsProcessor=${JEST_PROCESSOR}") COMMANDS_TO_RUN+=('./scripts/circleci/upload_build.sh') fi diff --git a/scripts/circleci/upload_build.sh b/scripts/circleci/upload_build.sh index 26ae3e9bed331..e129aa8513a5c 100755 --- a/scripts/circleci/upload_build.sh +++ b/scripts/circleci/upload_build.sh @@ -1,9 +1,11 @@ #!/bin/bash -set -e +. ./scripts/circleci/common.sh + +TEMPORARY_LOG_FILE="upload_build-errors.log" if [ -z $CI_PULL_REQUEST ] && [ -n "$BUILD_SERVER_ENDPOINT" ]; then - curl \ + process_curl "upload_build" "$REPORT_FORMATTER" "$TEMPORARY_LOG_FILE" curl \ -F "react.development=@build/dist/react.development.js" \ -F "react.production.min=@build/dist/react.production.min.js" \ -F "react-dom.development=@build/dist/react-dom.development.js" \ @@ -18,3 +20,8 @@ if [ -z $CI_PULL_REQUEST ] && [ -n "$BUILD_SERVER_ENDPOINT" ]; then -F "branch=$CIRCLE_BRANCH" \ $BUILD_SERVER_ENDPOINT fi + + + + + diff --git a/scripts/circleci/write_junit_report.sh b/scripts/circleci/write_junit_report.sh new file mode 100755 index 0000000000000..3e00931a43924 --- /dev/null +++ b/scripts/circleci/write_junit_report.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -e + +node ./scripts/tasks/junit "$1" "$2" "$3" + + diff --git a/scripts/eslint/index.js b/scripts/eslint/index.js index 2bc4cab84df12..a5a46d68fe5c0 100644 --- a/scripts/eslint/index.js +++ b/scripts/eslint/index.js @@ -11,14 +11,20 @@ const minimatch = require('minimatch'); const CLIEngine = require('eslint').CLIEngine; const listChangedFiles = require('../shared/listChangedFiles'); const {es5Paths, esNextPaths} = require('../shared/pathsByLanguageVersion'); - -const allPaths = ['**/*.js']; +const { + isJunitEnabled, + writePartialJunitReport, + mergePartialJunitReports, +} = require('../shared/reporting'); let changedFiles = null; +const allPaths = ['**/*.js']; + function runESLintOnFilesWithOptions(filePatterns, onlyChanged, options) { const cli = new CLIEngine(options); - const formatter = cli.getFormatter(); + // stylish is the default ESLint formatter. We switch to JUnit formatter in the scope of circleci + const formatter = cli.getFormatter(isJunitEnabled() ? 'junit' : 'stylish'); if (onlyChanged && changedFiles === null) { // Calculate lazily. @@ -65,6 +71,8 @@ function runESLint({onlyChanged}) { if (typeof onlyChanged !== 'boolean') { throw new Error('Pass options.onlyChanged as a boolean.'); } + const mutableESLintTemporaryFiles = []; + let errorCount = 0; let warningCount = 0; let output = ''; @@ -79,12 +87,26 @@ function runESLint({onlyChanged}) { runESLintOnFilesWithOptions(es5Paths, onlyChanged, { configFile: `${__dirname}/eslintrc.es5.js`, }), - ].forEach(result => { + ].forEach((result, index) => { errorCount += result.errorCount; warningCount += result.warningCount; output += result.output; + + if (isJunitEnabled()) { + // we create a JUnit file per run and then we merge them into one using junit-merge. + writePartialJunitReport( + 'eslint', + result.output, + index, + mutableESLintTemporaryFiles + ); + } }); + // Whether we store lint results in a file or not, we also log the results in the console console.log(output); + if (isJunitEnabled()) { + mergePartialJunitReports('eslint', mutableESLintTemporaryFiles); + } return errorCount === 0 && warningCount === 0; } diff --git a/scripts/facts-tracker/index.js b/scripts/facts-tracker/index.js index 78bd66cc71ab2..a1fcf4b01d466 100644 --- a/scripts/facts-tracker/index.js +++ b/scripts/facts-tracker/index.js @@ -13,6 +13,8 @@ const fs = require('fs'); const path = require('path'); const execFileSync = require('child_process').execFileSync; +const {isJunitEnabled, writeJunitReport} = require('../shared/reporting'); + let cwd = null; function exec(command, args) { console.error('>', [command].concat(args)); @@ -33,38 +35,51 @@ if (isCI) { !!process.env.CI_PULL_REQUEST; if (branch !== 'master') { - console.error('facts-tracker: Branch is not master, exiting...'); + const masterBranchMessage = + 'facts-tracker: Branch is not master, exiting...'; + console.error(masterBranchMessage); + if (isJunitEnabled()) { + writeJunitReport('track_stats', masterBranchMessage, true); + } process.exit(0); } if (isPullRequest) { - console.error('facts-tracker: This is a pull request, exiting...'); + const pullRequestMessage = + 'facts-tracker: This is a pull request, exiting...'; + console.error(pullRequestMessage); + if (isJunitEnabled()) { + writeJunitReport('track_stats', pullRequestMessage, true); + } process.exit(0); } if (!process.env.GITHUB_USER) { - console.error( + const githubMessage = 'In order to use facts-tracker, you need to configure a ' + - 'few environment variables in order to be able to commit to the ' + - 'repository. Follow those steps to get you setup:\n' + - '\n' + - 'Go to https://github.com/settings/tokens/new\n' + - ' - Fill "Token description" with "facts-tracker for ' + - process.env.TRAVIS_REPO_SLUG + - '"\n' + - ' - Check "public_repo"\n' + - ' - Press "Generate Token"\n' + - '\n' + - 'In a different tab, go to https://travis-ci.org/' + - process.env.TRAVIS_REPO_SLUG + - '/settings\n' + - ' - Make sure "Build only if .travis.yml is present" is ON\n' + - ' - Fill "Name" with "GITHUB_USER" and "Value" with the name of the ' + - 'account you generated the token with. Press "Add"\n' + - '\n' + - 'Once this is done, commit anything to the repository to restart ' + - 'Travis and it should work :)' - ); + 'few environment variables in order to be able to commit to the ' + + 'repository. Follow those steps to get you setup:\n' + + '\n' + + 'Go to https://github.com/settings/tokens/new\n' + + ' - Fill "Token description" with "facts-tracker for ' + + process.env.TRAVIS_REPO_SLUG + + '"\n' + + ' - Check "public_repo"\n' + + ' - Press "Generate Token"\n' + + '\n' + + 'In a different tab, go to https://travis-ci.org/' + + process.env.TRAVIS_REPO_SLUG + + '/settings\n' + + ' - Make sure "Build only if .travis.yml is present" is ON\n' + + ' - Fill "Name" with "GITHUB_USER" and "Value" with the name of the ' + + 'account you generated the token with. Press "Add"\n' + + '\n' + + 'Once this is done, commit anything to the repository to restart ' + + 'Travis and it should work :)'; + console.error(githubMessage); + if (isJunitEnabled()) { + writeJunitReport('track_stats', githubMessage, false); + } process.exit(1); } @@ -83,7 +98,12 @@ if (isCI) { } if (process.argv.length <= 2) { - console.error('Usage: facts-tracker ...'); + const usageMessage = + 'Usage: facts-tracker ...'; + console.error(usageMessage); + if (isJunitEnabled()) { + writeJunitReport('track_stats', usageMessage, false); + } process.exit(1); } @@ -99,8 +119,11 @@ function getRepoSlug() { return match[1]; } } - - console.error('Cannot find repository slug, sorry.'); + const repoMessage = 'Cannot find repository slug, sorry.'; + console.error(repoMessage); + if (isJunitEnabled()) { + writeJunitReport('track_stats', repoMessage, false); + } process.exit(1); } @@ -139,7 +162,11 @@ function checkoutFactsFolder() { cwd = path.resolve(factsFolder); exec('git', ['fetch']); if (exec('git', ['status', '--porcelain'])) { - console.error('facts-tracker: `git status` is not clean, aborting.'); + const statusMessage = 'facts-tracker: `git status` is not clean, aborting.'; + console.error(statusMessage); + if (isJunitEnabled()) { + writeJunitReport('track_stats', statusMessage, false); + } process.exit(1); } exec('git', ['rebase', 'origin/facts']); @@ -186,6 +213,10 @@ if (exec('git', ['status', '--porcelain'])) { exec('git', ['commit', '-m', 'Adding facts for ' + currentCommitHash]); exec('git', ['push', 'origin', 'facts']); } else { - console.error('facts-tracker: nothing to update'); + const nothingToUpdateMessage = 'facts-tracker: nothing to update'; + console.error(nothingToUpdateMessage); + if (isJunitEnabled()) { + writeJunitReport('track_stats', nothingToUpdateMessage, true); + } } cwd = null; diff --git a/scripts/prettier/index.js b/scripts/prettier/index.js index 4de60a981d95c..a79880bfb4f85 100644 --- a/scripts/prettier/index.js +++ b/scripts/prettier/index.js @@ -16,7 +16,10 @@ const fs = require('fs'); const listChangedFiles = require('../shared/listChangedFiles'); const prettierConfigPath = require.resolve('../../.prettierrc'); +const {isJunitEnabled, writeJunitReport} = require('../shared/reporting'); + const mode = process.argv[2] || 'check'; + const shouldWrite = mode === 'write' || mode === 'write-changed'; const onlyChanged = mode === 'check-changed' || mode === 'write-changed'; @@ -28,6 +31,14 @@ const files = glob .sync('**/*.js', {ignore: '**/node_modules/**'}) .filter(f => !onlyChanged || changedFiles.has(f)); +const writeReport = (data, hasSucceeded) => { + if (isJunitEnabled()) { + writeJunitReport('prettier', data, hasSucceeded); + } +}; + +let junitData = ''; + if (!files.length) { return; } @@ -46,30 +57,43 @@ files.forEach(file => { } else { if (!prettier.check(input, options)) { if (!didWarn) { - console.log( + const announcement = '\n' + - chalk.red( - ` This project uses prettier to format all JavaScript code.\n` - ) + - chalk.dim(` Please run `) + - chalk.reset('yarn prettier-all') + - chalk.dim( - ` and add changes to files listed below to your commit:` - ) + - `\n\n` - ); + chalk.red( + ` This project uses prettier to format all JavaScript code.\n` + ) + + chalk.dim(` Please run `) + + chalk.reset('yarn prettier-all') + + chalk.dim( + ` and add changes to files listed below to your commit:` + ) + + `\n\n`; + console.log(announcement); + if (isJunitEnabled()) { + junitData += announcement; + } didWarn = true; } console.log(file); + if (isJunitEnabled()) { + junitData += file + '\n'; + } } } } catch (error) { didError = true; console.log('\n\n' + error.message); console.log(file); + if (isJunitEnabled()) { + junitData += '\n\n' + error.message; + junitData += file + '\n'; + } } }); if (didWarn || didError) { + writeReport(junitData, false); process.exit(1); +} else { + writeReport(junitData, true); } diff --git a/scripts/rollup/build.js b/scripts/rollup/build.js index 1b3c0d34bbde3..05d9b3a1b4d58 100644 --- a/scripts/rollup/build.js +++ b/scripts/rollup/build.js @@ -24,10 +24,15 @@ const {asyncCopyTo, asyncRimRaf} = require('./utils'); const codeFrame = require('babel-code-frame'); const Wrappers = require('./wrappers'); +const {isJunitEnabled, writeJunitReport} = require('../shared/reporting'); + // Errors in promises should be fatal. let loggedErrors = new Set(); process.on('unhandledRejection', err => { if (loggedErrors.has(err)) { + if (isJunitEnabled()) { + writeJunitReport('build', err, false); + } // No need to print it twice. process.exit(1); } @@ -400,6 +405,9 @@ async function createBundle(bundle, bundleType) { function handleRollupWarning(warning) { if (warning.code === 'UNRESOLVED_IMPORT') { console.error(warning.message); + if (isJunitEnabled()) { + writeJunitReport('build', warning.message, false); + } process.exit(1); } if (warning.code === 'UNUSED_EXTERNAL_IMPORT') { diff --git a/scripts/rollup/validate/index.js b/scripts/rollup/validate/index.js index 32cac0a3f6ad2..238fe5bfd749e 100644 --- a/scripts/rollup/validate/index.js +++ b/scripts/rollup/validate/index.js @@ -4,6 +4,7 @@ const chalk = require('chalk'); const path = require('path'); const spawnSync = require('child_process').spawnSync; const glob = require('glob'); +const {isJunitEnabled, writeJunitReport} = require('../../shared/reporting'); const extension = process.platform === 'win32' ? '.cmd' : ''; @@ -29,7 +30,11 @@ function lint({format, filePatterns}) { } ); if (result.status !== 0) { - console.error(chalk.red(`Linting of ${format} bundles has failed.`)); + const lintMessage = `Linting of ${format} bundles has failed.`; + console.error(chalk.red(lintMessage)); + if (isJunitEnabled()) { + writeJunitReport('lint-build', lintMessage, false); + } process.exit(result.status); } else { console.log(chalk.green(`Linted ${format} bundles successfully!`)); @@ -43,7 +48,11 @@ function checkFilesExist(bundle) { console.log(`Checking if files exist in ${pattern}...`); const files = glob.sync(pattern); if (files.length === 0) { - console.error(chalk.red(`Found no ${format} bundles in ${pattern}`)); + const bundleMessage = `Found no ${format} bundles in ${pattern}`; + console.error(chalk.red(bundleMessage)); + if (isJunitEnabled()) { + writeJunitReport('lint-build', bundleMessage, false); + } process.exit(1); } else { console.log(chalk.green(`Found ${files.length} bundles.`)); diff --git a/scripts/shared/__tests__/reporting-test.js b/scripts/shared/__tests__/reporting-test.js new file mode 100644 index 0000000000000..ebcbc925637c6 --- /dev/null +++ b/scripts/shared/__tests__/reporting-test.js @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +'use strict'; + +const { + buildXMLOutputAsSingleTest, + getPartialJUnitReportFileName, +} = require('../reporting'); + +describe('junitReport', () => { + describe('buildXMLOutputAsSingleTest', () => { + it('should handle test failures', () => { + expect( + buildXMLOutputAsSingleTest('Hello', 'flow', false).replace(/\s+/g, '') + ).toEqual( + ` + + + + + + + + + + `.replace(/\s+/g, '') + ); + }); + it('should handle tests passing', () => { + expect( + buildXMLOutputAsSingleTest('', 'flow', true).replace(/\s+/g, '') + ).toEqual( + ` + + + + + + `.replace(/\s+/g, '') + ); + }); + }); +}); + +describe('getPartialJUnitReportFileName', () => { + it('should return a correct JUnit report file name provided a base name and an index', () => { + expect(getPartialJUnitReportFileName('base.xml', 2)).toBe('base2.xml'); + }); +}); diff --git a/scripts/shared/reporting.js b/scripts/shared/reporting.js new file mode 100644 index 0000000000000..d4b54ec17cf30 --- /dev/null +++ b/scripts/shared/reporting.js @@ -0,0 +1,216 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const {execSync} = require('child_process'); +const chalk = require('chalk'); + +/** + * Returns whether JUnit report generation mode is enabled. The activation is done through circleci + * configuration file + * + * @returns {boolean} Whether JUnit report generation mode is enabled + */ +const isJunitEnabled = () => process.env.REPORT_FORMATTER === 'junit'; + +/** + * Returns the file path to the report corresponding to the provided build step + * @param {string} buildStep The build step for which a report will be generated + * @returns {string} The file path to the report corresponding to the provided build step + */ +const reportFilePath = buildStep => + `${process.env.REPORT_DIR}/${buildStep}-results.xml`; + +/** + * Creates directories (if missing corresponding) to a provided file path + * + * @param {string} filePath The file path + */ +const createDirectoriesIfMissing = filePath => { + const dirname = path.dirname(filePath); + if (fs.existsSync(dirname)) { + return; + } + createDirectoriesIfMissing(dirname); + fs.mkdirSync(dirname); +}; + +/** + * Builds the output that will be written in the XML JUnit report + * + * @param {string} data + * @param {string} packageName + * @param {boolean} hasPassed + * @returns {string} The output that will be written in the JUnit report + */ +const buildXMLOutputAsSingleTest = (data, packageName, hasPassed) => { + const nbrErrors = hasPassed ? 0 : 1; + const detailedMessage = hasPassed + ? '' + : ` + + + `; + + const testSuite = ` + + +${detailedMessage} + + + `; + return ` + + ${testSuite} + + `; +}; + +/** + * Writes a JUnit report as a single test + * + * @param {ReportInfo} + * @param {string} outputFile The file path describing the file that will hold the JUnit report + */ +const writeReportAsSingleTest = (data, packageName, hasPassed, outputFile) => { + const xmlOutput = buildXMLOutputAsSingleTest(data, packageName, hasPassed); + createDirectoriesIfMissing(outputFile); + fs.writeFileSync(outputFile, xmlOutput, 'utf8'); +}; + +/** + * Writes a JUnit report for a given build step + * + * @param {string} buildStep The build step name + * @param {any} data The data that will be part of the report if the build step has failed + * @param {boolean} stepHasSucceeded Whether the build step has failed + */ +const writeJunitReport = (buildStep, data, stepHasSucceeded) => { + const reportPath = reportFilePath(buildStep); + console.log(chalk.gray(`Starting to write the report in ${reportPath}`)); + writeReportAsSingleTest(data, buildStep, stepHasSucceeded, reportPath); + console.log( + chalk.gray( + `Finished writing the report in ${reportPath} for the build step ${ + buildStep + }` + ) + ); +}; + +/** + * Returns the name for the temporary JUnit report file name provided the base file name and a + * unique index. + * + * @param {string} baseFileName The name of the JUnit report file into which all temporary files + * will merged. + * @param {number} index A number identifying the temporary JUnit report file + * @returns {string} The name for the temporary JUnit report file name + * + */ +function getPartialJUnitReportFileName(baseFileName, index) { + if (!baseFileName.endsWith('.xml')) { + throw Error('Invalid XML file name provided'); + } + return baseFileName.replace('.xml', `${index}.xml`); +} + +/** + * Writes a partial JUnit report identified by an an index and output data + * + * @param {string} buildStep The build step name + * @param {string} data The data that will be part of the report if the build step has failed + * @param {number} index Number identifying the partial JUnit report + * @param {Array.} reportFileNames The list of report file names + */ +const writePartialJunitReport = (buildStep, data, index, reportFileNames) => { + if (!isJunitEnabled()) { + return; + } + const fileName = getPartialJUnitReportFileName( + reportFilePath(buildStep), + index + ); + console.log( + chalk.gray( + `Starting to write the partial report for ${buildStep} in ${fileName}` + ) + ); + createDirectoriesIfMissing(fileName); + fs.writeFileSync(fileName, data, 'utf8'); + console.log( + chalk.gray( + `Finished writing the partial report for ${buildStep} in ${fileName}` + ) + ); + // Side effect whose goal is to keep track of partial JUnit reports. This list will be used at the + // merge step into one single JUnit report + reportFileNames.push(fileName); +}; + +/** + * Merge all generated partial JUnit report files into a single one. The partial reports will then + * be deleted + * + * @param {string} buildStep The build step name + * @param {Array.} reportFileNames The list of ESLint report file names + */ +const mergePartialJunitReports = (buildStep, reportFileNames) => { + if (!isJunitEnabled()) { + return; + } + // Merge synchronously partial JUnit report files into a single one. This can be done + // asynchronously as well + try { + execSync( + `${path.join( + 'node_modules', + 'junit-merge', + 'bin', + 'junit-merge' + )} ${reportFileNames.join(' ')} --out ${reportFilePath(buildStep)}` + ); + console.log( + `Created for the step ${ + buildStep + } the JUnit merged report file ${reportFilePath(buildStep)}` + ); + } catch (e) { + throw new Error( + `could not create for the step ${ + buildStep + } the JUnit merged report file ${reportFilePath(buildStep)}` + ); + } + + // Now, we delete the partial JUnit report files again synchronously + reportFileNames.forEach(file => { + try { + fs.unlinkSync(file); + console.log(`Deleted file: ${file}`); + } catch (e) { + // we don't want to throw an error as this is not blocking the build + console.log(`Could not delete file: ${file}`); + } + }); +}; + +module.exports = { + isJunitEnabled, + reportFilePath, + writeJunitReport, + buildXMLOutputAsSingleTest, + writePartialJunitReport, + mergePartialJunitReports, + getPartialJUnitReportFileName, +}; diff --git a/scripts/tasks/flow.js b/scripts/tasks/flow.js index 30ef8a3f230e4..91f89f88d0cfa 100644 --- a/scripts/tasks/flow.js +++ b/scripts/tasks/flow.js @@ -9,18 +9,44 @@ const path = require('path'); const spawn = require('child_process').spawn; +const chalk = require('chalk'); + +const {isJunitEnabled, writeJunitReport} = require('../shared/reporting'); const extension = process.platform === 'win32' ? '.cmd' : ''; +const spawnOptions = isJunitEnabled() ? {} : {stdio: 'inherit'}; + +let createReport = () => {}; +let reportChunks = []; + +const flow = spawn( + path.join('node_modules', '.bin', 'flow' + extension), + ['check', '.'], + spawnOptions +); -spawn(path.join('node_modules', '.bin', 'flow' + extension), ['check', '.'], { - // Allow colors to pass through - stdio: 'inherit', -}).on('close', function(code) { +flow.on('close', function(code) { if (code !== 0) { - console.error('Flow failed'); + console.error(chalk.red.bold('Flow failed')); + createReport(false); } else { - console.log('Flow passed'); + console.log(chalk.green.bold('Flow passed')); + createReport(true); + } + if (reportChunks.length > 0) { + let reportJSON = reportChunks.join(''); + console.log(reportJSON); } - process.exit(code); }); + +if (isJunitEnabled()) { + flow.stdout.on('data', data => { + createReport = stepHasSucceeded => { + if (!stepHasSucceeded) { + reportChunks.push(data); + } + writeJunitReport('flow', data, stepHasSucceeded); + }; + }); +} diff --git a/scripts/tasks/junit.js b/scripts/tasks/junit.js new file mode 100644 index 0000000000000..0161b212ae690 --- /dev/null +++ b/scripts/tasks/junit.js @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +const {isJunitEnabled, writeJunitReport} = require('../shared/reporting'); + +if (!isJunitEnabled() | (process.argv.length !== 5)) { + return; +} + +writeJunitReport(process.argv[2], process.argv[3], process.argv[4] === 'true'); diff --git a/scripts/tasks/version-check.js b/scripts/tasks/version-check.js index bb15db0cc480e..60150d6093448 100644 --- a/scripts/tasks/version-check.js +++ b/scripts/tasks/version-check.js @@ -7,6 +7,8 @@ 'use strict'; +const {isJunitEnabled, writeJunitReport} = require('../shared/reporting'); + const reactVersion = require('../../package.json').version; const versions = { 'packages/react/package.json': require('../../packages/react/package.json') @@ -18,20 +20,28 @@ const versions = { 'packages/shared/ReactVersion.js': require('../../packages/shared/ReactVersion'), }; +let errorMessages = []; + let allVersionsMatch = true; Object.keys(versions).forEach(function(name) { const version = versions[name]; if (version !== reactVersion) { allVersionsMatch = false; - console.log( - '%s version does not match package.json. Expected %s, saw %s.', - name, - reactVersion, + const errorMessage = `${ + name + } version does not match package.json. Expected ${reactVersion}, saw ${ version - ); + } .`; + console.log(errorMessage); + if (isJunitEnabled()) { + errorMessages.push(errorMessage); + } } }); if (!allVersionsMatch) { + if (isJunitEnabled()) { + writeJunitReport('version-check', errorMessages.join('\n'), false); + } process.exit(1); } diff --git a/yarn.lock b/yarn.lock index 13b0b6ab4bf1a..c865aeda165b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1285,6 +1285,10 @@ combined-stream@^1.0.5, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" +commander@^2.12.2: + version "2.12.2" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555" + commander@^2.5.0, commander@^2.6.0, commander@^2.8.1, commander@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" @@ -2119,6 +2123,10 @@ fs-readdir-recursive@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz#8cd1745c8b4f8a29c8caec392476921ba195f560" +fs-readdir-recursive@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -3011,6 +3019,14 @@ jest-jasmine2@^22.0.4: jest-snapshot "^22.0.3" source-map-support "^0.5.0" +jest-junit@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/jest-junit/-/jest-junit-3.4.1.tgz#0f0aea65551290cabdf9a29a1681edb4eba418c5" + dependencies: + mkdirp "^0.5.1" + strip-ansi "^4.0.0" + xml "^1.0.1" + jest-leak-detector@^22.0.3: version "22.0.3" resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-22.0.3.tgz#b64904f0e8954a11edb79b0809ff4717fa762d99" @@ -3281,6 +3297,14 @@ jsx-ast-utils@^1.3.4: version "1.4.1" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz#3867213e8dd79bf1e8f2300c0cfc1efb182c0df1" +junit-merge@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/junit-merge/-/junit-merge-1.2.3.tgz#b861b15666cb4090dc1ce0c0d1675eb02c288800" + dependencies: + commander "^2.12.2" + fs-readdir-recursive "^1.1.0" + xmldoc "^1.1.0" + kind-of@^3.0.2: version "3.2.0" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.0.tgz#b58abe4d5c044ad33726a8c1525b48cf891bff07" @@ -5219,6 +5243,16 @@ xml-name-validator@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635" +xml@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" + +xmldoc@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/xmldoc/-/xmldoc-1.1.0.tgz#25c92f08f263f344dac8d0b32370a701ee9d0e93" + dependencies: + sax "^1.2.1" + "xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"