From 59885d3c64109d6fe7ac9026c47bc84168804e64 Mon Sep 17 00:00:00 2001 From: Johannes Ewald Date: Thu, 4 Oct 2018 10:32:45 +0200 Subject: [PATCH] feat: Add basic reporter (#78) Add basic reporter for non-TTY environments like CI --- README.md | 1 + src/reporters/basic.js | 212 ++++++++++++++++++ src/reporters/index.js | 3 +- .../__snapshots__/basic.test.js.snap | 132 +++++++++++ .../__snapshots__/index.test.js.snap | 1 + test/reporters/basic.test.js | 72 ++++++ 6 files changed, 420 insertions(+), 1 deletion(-) create mode 100644 src/reporters/basic.js create mode 100644 test/reporters/__snapshots__/basic.test.js.snap create mode 100644 test/reporters/basic.test.js diff --git a/README.md b/README.md index 0e4dd64..0e50711 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ Specify how updated versions should be saved to the `package.json`: Choose a reporter for the console output: - `dense` *(default*): See screenshot +- `basic`: Uses `console.log` for output, no need for a TTY (e.g when running on CI) - `none`: No console output ### `--test` `-t` diff --git a/src/reporters/basic.js b/src/reporters/basic.js new file mode 100644 index 0000000..351f4da --- /dev/null +++ b/src/reporters/basic.js @@ -0,0 +1,212 @@ +import ansiEscapes from "ansi-escapes"; +import chalk from "chalk"; +import unicons from "unicons"; +import { + filterSuccessfulUpdates, + filterFailedUpdates, +} from "../tasks/util/filterUpdateResults"; +import Message from "./util/Message"; +import Indicator, { + INDICATOR_NEUTRAL, + INDICATOR_PENDING, + INDICATOR_OK, + INDICATOR_FAIL, +} from "./util/Indicator"; +import customConfigToLines from "./util/customConfigToLines"; +import pluralize from "./util/pluralize"; +import handleError from "./util/handleError"; +import msToString from "./util/msToString"; + +function updatingLine(updateTask) { + return [ + new Indicator(INDICATOR_PENDING), + chalk.bold(updateTask.name), + chalk.grey("updating"), + updateTask.rollbackTo, + chalk.grey(unicons.arrowRight), + updateTask.updateTo + chalk.grey("..."), + ].join(" "); +} + +function testingLine(updateTask) { + return [ + new Indicator(INDICATOR_PENDING), + chalk.bold(updateTask.name), + chalk.grey("testing..."), + ].join(" "); +} + +function rollbackLine(updateTask) { + return [ + new Indicator(INDICATOR_FAIL), + chalk.bold.red(updateTask.name), + chalk.grey("rolling back"), + updateTask.updateTo, + chalk.grey(unicons.arrowRight), + updateTask.rollbackTo + chalk.grey("..."), + ].join(" "); +} + +function successLine(updateTask) { + return [ + new Indicator(INDICATOR_OK), + chalk.bold(updateTask.name), + updateTask.updateTo, + chalk.grey("success"), + ].join(" "); +} + +function failLine(updateTask) { + return [ + new Indicator(INDICATOR_FAIL), + chalk.bold.red(updateTask.name), + updateTask.updateTo, + chalk.grey("failed"), + ].join(" "); +} + +function excludedLine(excluded) { + return [ + new Indicator(INDICATOR_NEUTRAL), + chalk.bold(excluded.name), + chalk.grey(excluded.reason), + ].join(" "); +} + +function cmdToLines(description, cmd) { + const lines = Array.isArray(description) === true ? + description : + [description]; + + return lines.concat([chalk.grey(`> ${cmd} `)]); +} + +function writeLinesToConsole(lines) { + if (lines.length === 0) { + return; + } + console.log(ansiEscapes.eraseDown + lines.join("\n")); +} + +export default function basic(updtr, reporterConfig) { + const startTime = Date.now(); + let excludedModules; + + updtr.on("start", ({config}) => { + writeLinesToConsole(customConfigToLines(config)); + }); + updtr.on("init/install-missing", ({cmd}) => { + writeLinesToConsole( + cmdToLines( + "Installing missing dependencies" + chalk.grey("..."), + cmd + ) + ); + }); + updtr.on("init/collect", ({cmd}) => { + writeLinesToConsole( + cmdToLines("Looking for outdated modules" + chalk.grey("..."), cmd) + ); + }); + updtr.on("init/end", ({updateTasks, excluded}) => { + excludedModules = excluded; + if (updateTasks.length === 0 && excluded.length === 0) { + writeLinesToConsole(["Everything " + chalk.bold("up-to-date")]); + } else if (updateTasks.length === 0) { + writeLinesToConsole([ + chalk.bold("No updates available") + + " for the given modules and version range", + ]); + } else { + writeLinesToConsole([ + new Message("Found " + chalk.bold("%s update%s") + ".", [ + updateTasks.length, + pluralize(updateTasks.length), + ]), + "", + ]); + } + }); + updtr.on("batch-update/updating", event => { + writeLinesToConsole( + cmdToLines(event.updateTasks.map(updatingLine), event.cmd) + ); + }); + updtr.on("batch-update/testing", event => { + writeLinesToConsole( + cmdToLines(event.updateTasks.map(testingLine), event.cmd) + ); + }); + updtr.on("batch-update/rollback", event => { + writeLinesToConsole( + cmdToLines(event.updateTasks.map(rollbackLine), event.cmd) + ); + }); + updtr.on("batch-update/result", event => { + if (event.success === true) { + writeLinesToConsole( + event.updateTasks.map(event.success ? successLine : failLine) + ); + } + // Not showing the test stdout here when there was an error because + // we will proceed with the sequential update. + }); + updtr.on("sequential-update/updating", event => { + writeLinesToConsole(cmdToLines(updatingLine(event), event.cmd)); + }); + updtr.on("sequential-update/testing", event => { + writeLinesToConsole(cmdToLines(testingLine(event), event.cmd)); + }); + updtr.on("sequential-update/rollback", event => { + writeLinesToConsole(cmdToLines(rollbackLine(event), event.cmd)); + }); + updtr.on("sequential-update/result", event => { + writeLinesToConsole([(event.success ? successLine : failLine)(event)]); + if (reporterConfig.testStdout === true && event.success === false) { + writeLinesToConsole([event.stdout]); + } + }); + updtr.on("end", ({results}) => { + const duration = msToString(Date.now() - startTime); + const successful = filterSuccessfulUpdates(results); + const failed = filterFailedUpdates(results); + + writeLinesToConsole([""]); + + if (successful.length > 0) { + writeLinesToConsole([ + new Message(chalk.bold("%s successful") + " update%s.", [ + successful.length, + pluralize(successful.length), + ]), + ]); + } + if (failed.length > 0) { + writeLinesToConsole([ + new Message(chalk.bold("%s failed") + " update%s.", [ + failed.length, + pluralize(failed.length), + ]), + ]); + } + if (excludedModules.length > 0) { + const list = excludedModules.map(excludedLine); + + if (successful.length > 0 || failed.length > 0) { + writeLinesToConsole([""]); + } + writeLinesToConsole( + [ + new Message(chalk.bold("%s skipped") + " module%s:", [ + excludedModules.length, + pluralize(excludedModules.length), + ]), + "", + ].concat(list) + ); + } + + writeLinesToConsole(["", new Message("Finished after %s.", [duration])]); + }); + updtr.on("error", err => void handleError(err)); +} diff --git a/src/reporters/index.js b/src/reporters/index.js index 0d84e47..6d9ccc0 100644 --- a/src/reporters/index.js +++ b/src/reporters/index.js @@ -1,7 +1,8 @@ +import basic from "./basic"; import dense from "./dense"; import none from "./none"; // The first property here is the default reporter -const reporters = {dense, none}; +const reporters = {dense, basic, none}; export default reporters; diff --git a/test/reporters/__snapshots__/basic.test.js.snap b/test/reporters/__snapshots__/basic.test.js.snap new file mode 100644 index 0000000..6f010cb --- /dev/null +++ b/test/reporters/__snapshots__/basic.test.js.snap @@ -0,0 +1,132 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`basic() batch-update fail and show test stdout should print the expected lines: batch-update fail and show test stdout 1`] = ` +"Installing missing dependencies... +> npm install  +Looking for outdated modules... +> npm outdated  +Found 4 updates. + +- module updating 0.0.0 → 0.0.1... +- module updating 0.0.0 → 0.1.0... +- module updating 0.0.0 → 1.0.0... +- module updating 0.0.0 → 0.0.1... +> npm install  +- module testing... +- module testing... +- module testing... +- module testing... +> npm test  +- module rolling back 0.0.1 → 0.0.0... +- module rolling back 0.1.0 → 0.0.0... +- module rolling back 1.0.0 → 0.0.0... +- module rolling back 0.0.1 → 0.0.0... +> npm install  + +2 successful updates. +1 failed update. + +Finished after 1.0s." +`; + +exports[`basic() batch-update success and show test stdout should print the expected lines: batch-update success and show test stdout 1`] = ` +"Installing missing dependencies... +> npm install  +Looking for outdated modules... +> npm outdated  +Found 4 updates. + +- module updating 0.0.0 → 0.0.1... +- module updating 0.0.0 → 0.1.0... +- module updating 0.0.0 → 1.0.0... +- module updating 0.0.0 → 0.0.1... +> npm install  +- module testing... +- module testing... +- module testing... +- module testing... +> npm test  +- module 0.0.1 success +- module 0.1.0 success +- module 1.0.0 success +- module 0.0.1 success + +3 successful updates. + +Finished after 1.0s." +`; + +exports[`basic() custom config and only excluded modules should print the expected lines: custom config and only excluded modules 1`] = ` +"Running updtr with custom configuration: + +- exclude: b, c + +Installing missing dependencies... +> npm install  +Looking for outdated modules... +> npm outdated  +No updates available for the given modules and version range + +3 skipped modules: + +- a git +- b excluded +- c excluded + +Finished after 1.0s." +`; + +exports[`basic() custom config and sequential-update with mixed success and show test stdout should print the expected lines: custom config and sequential-update with mixed success and show test stdout 1`] = ` +"Running updtr with custom configuration: + +- exclude: b, c + +Installing missing dependencies... +> npm install  +Looking for outdated modules... +> npm outdated  +Found 4 updates. + +- module updating 0.0.0 → 0.0.1... +> npm install  +- module testing... +> npm test  +- module 0.0.1 success +- module updating 0.0.0 → 0.1.0... +> npm install  +- module testing... +> npm test  +- module rolling back 0.1.0 → 0.0.0... +> npm install  +- module 0.1.0 failed +This is the test stdout +- module updating 0.0.0 → 1.0.0... +> npm install  +- module testing... +> npm test  +- module 1.0.0 success +- module updating 0.0.0 → 0.0.1... +> npm install  +- module testing... +> npm test  +- module rolling back 0.0.1 → 0.0.0... +> npm install  +- module 0.0.1 failed +This is the test stdout + +2 successful updates. +2 failed updates. + +Finished after 1.0s." +`; + +exports[`basic() no outdated modules should print the expected lines: no outdated modules 1`] = ` +"Installing missing dependencies... +> npm install  +Looking for outdated modules... +> npm outdated  +Everything up-to-date + + +Finished after 1.0s." +`; diff --git a/test/reporters/__snapshots__/index.test.js.snap b/test/reporters/__snapshots__/index.test.js.snap index d93d42e..e88fea0 100644 --- a/test/reporters/__snapshots__/index.test.js.snap +++ b/test/reporters/__snapshots__/index.test.js.snap @@ -3,6 +3,7 @@ exports[`reporters should export all available reporters 1`] = ` Array [ "dense", + "basic", "none", ] `; diff --git a/test/reporters/basic.test.js b/test/reporters/basic.test.js new file mode 100644 index 0000000..1344786 --- /dev/null +++ b/test/reporters/basic.test.js @@ -0,0 +1,72 @@ +import EventEmitter from "events"; +import unicons from "unicons"; +import sinon from "sinon"; +import basic from "../../src/reporters/basic"; +import events from "../fixtures/events"; + +let consoleStub; + +function setup(reporterConfig = {}) { + const updtr = new EventEmitter(); + const output = []; + + consoleStub.callsFake((...args) => output.push(args)); + basic(updtr, reporterConfig); + + return { + updtr, + output, + }; +} + +beforeAll(() => { + // We need to replace platform-dependent characters with static counter parts to make snapshot testing + // consistent across platforms. + unicons.cli = sign => { + switch (sign) { + case "circle": + return "-"; + default: + return ""; + } + }; + consoleStub = sinon.stub(console, "log"); +}); + +afterEach(() => { + consoleStub.reset(); +}); + +afterAll(() => { + consoleStub.restore(); +}); + +describe("basic()", () => { + Object.keys(events).forEach(caseName => { + describe(caseName, () => { + it("should print the expected lines", async () => { + const testCase = events[caseName]; + const {updtr, output} = setup(testCase.reporterConfig); + + await testCase.events.reduce(async (previous, [ + eventName, + event, + ]) => { + await previous; + updtr.emit(eventName, event); + + // Faking async events + return Promise.resolve(); + }, Promise.resolve()); + + expect( + output.join("\n").replace( + // We need to replace the timing because that is non-deterministic + /Finished after \d+\.\ds/, + "Finished after 1.0s" + ) + ).toMatchSnapshot(caseName); + }); + }); + }); +});