diff --git a/runtime-nodejs/.dockerignore b/runtime-nodejs/.dockerignore index d7e7f87..6e21da9 100644 --- a/runtime-nodejs/.dockerignore +++ b/runtime-nodejs/.dockerignore @@ -2,6 +2,7 @@ !.gitignore !package.json !package-lock.json +!src !jest.*.js !specs !waiting-for-specs diff --git a/runtime-nodejs/.eslintrc.js b/runtime-nodejs/.eslintrc.js new file mode 100644 index 0000000..d96fcc1 --- /dev/null +++ b/runtime-nodejs/.eslintrc.js @@ -0,0 +1,15 @@ +module.exports = { + "env": { + "browser": true, + "commonjs": true, + "es2021": true, + "node": true, + "jest": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 12 + }, + "rules": { + } +}; diff --git a/runtime-nodejs/Dockerfile b/runtime-nodejs/Dockerfile index e1349a9..6d6688d 100644 --- a/runtime-nodejs/Dockerfile +++ b/runtime-nodejs/Dockerfile @@ -1,4 +1,4 @@ -FROM yolean/node:8fa14c0f3581c0e225072e303d6e011e861582cc@sha256:25830c903c575a26a88a2c20ddd7b221886063eef17590a80cf9c3cbed168f44 \ +FROM yolean/node:682b35452193e841717e6749a7de1df6e1ac4f6a@sha256:2ae6c3cfd92c8298cb57ce304c18d58523759e92ae92f668e8d1de97fa9876fb \ as unittest ENV CI=true @@ -13,7 +13,7 @@ RUN [ "npm", "ci", "--ignore-scripts" ] COPY --chown=nonroot:nogroup . . RUN [ "npm", "test" ] -FROM yolean/node:8fa14c0f3581c0e225072e303d6e011e861582cc@sha256:25830c903c575a26a88a2c20ddd7b221886063eef17590a80cf9c3cbed168f44 +FROM yolean/node:682b35452193e841717e6749a7de1df6e1ac4f6a@sha256:2ae6c3cfd92c8298cb57ce304c18d58523759e92ae92f668e8d1de97fa9876fb ENV CI=true COPY --chown=nonroot:nogroup package*.json .gitignore /usr/src/ diff --git a/runtime-nodejs/jest.kubernetes-assertions-reporter.js b/runtime-nodejs/jest.kubernetes-assertions-reporter.js index df74a08..eb94897 100644 --- a/runtime-nodejs/jest.kubernetes-assertions-reporter.js +++ b/runtime-nodejs/jest.kubernetes-assertions-reporter.js @@ -1,11 +1,12 @@ -const fs = require('fs'); -const http = require('http'); - const PORT = process.env.PORT ? parseInt(process.env.PORT) : 9091; const RERUN_WAIT = parseInt(process.env.RERUN_WAIT); const ASSERT_IS_DEV = process.env.ASSERT_IS_DEV === 'true'; const client = require('prom-client'); +const MetricsReporter = require('./src/MetricsReporter'); +const MetricsServer = require('./src/MetricsServer'); +const Reruns = require('./src/Reruns'); +const SpecFilesTracker = require('./src/SpecFilesTracker'); const register = client.register; const assertions_failed = new client.Gauge({ @@ -43,166 +44,36 @@ const assert_files_seen = new client.Gauge({ help: 'Unique spec file paths that have been seen in onTestResult' }); -class SpecFilesTracker { - - constructor() { - this._pathsSeen = {}; - } - - pathSeen(path, { numFailingTests }) { - if (this._pathsSeen.hasOwnProperty(path)) { - const delta = numFailingTests - this._pathsSeen[path].numFailingTests; - if (delta > 0) assertions_failed.inc(delta); - if (delta < 0) assertions_failed.dec(-delta); - } else { - this._pathsSeen[path] = {}; - assert_files_seen.inc(); - if (numFailingTests > 0) assertions_failed.inc(numFailingTests); - console.log('Path reported for the first time:', path); - } - this._pathsSeen[path].numFailingTests = numFailingTests; - } - - modify(path) { - const insignificant = '\n'; - fs.appendFile(path, insignificant, 'utf8', (err) => { - if (err) throw err; - //console.log('Modified', path); - }); - } - - modifyAll() { - const modify = this.modify.bind(this); - const paths = Object.keys(this._pathsSeen); - console.log('Files will be modified to trigger rerun:', paths); - paths.map(path => { - // We prefer to do this as concurrent as possible, to trigger a single run, so don't wait - modify(path); - }); - } - -} - const tracker = new SpecFilesTracker(); -/* - * The idea ... - * We'll only get good specs if the development experience is attractive - * which is why runtime-nodejs prioritizes the use case skaffold dev with sync - * Until https://github.com/facebook/jest/issues/5048 https://github.com/facebook/jest/issues/8868 are fixed - * ... or we want to give something like https://medium.com/web-developers-path/how-to-run-jest-programmatically-in-node-js-jest-javascript-api-492a8bc250de a try - * ... or we find a way to emulate interactive watch keypresses - * it'll mean that we run watch always and that the unattended run mode will be a bit of a hack - * The basic requirement is that tests are rerun. - * We rerun all tests, not only failed, because when it comes to infra things go up and down. - */ -class Reruns { - - constructor({ tracker, intervalMs }) { - console.log('Activating reruns with interval (ms)', intervalMs); - this._intervalMs = intervalMs; - this._timeout = null; - } - - onRunComplete() { - this._timeout !== null && clearTimeout(this._timeout); - if (!ASSERT_IS_DEV && RERUN_WAIT) { - this._timeout = setTimeout(() => { - tracker.modifyAll(); - }, this._intervalMs); - } - } - -} - const reruns = new Reruns({ tracker, - intervalMs: RERUN_WAIT * 1000 + rerunWaitMs: RERUN_WAIT * 1000, + assertIsDev: ASSERT_IS_DEV }); -class MetricsServer { - - constructor({ port, getMetrics }) { - this.port = port; - this.getMetrics = getMetrics; - } - - serveMetrics(res) { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end(this.getMetrics()); - } - - serveRerun(res) { - console.log('Rerun endpoint called'); - tracker.modifyAll(); - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('{}'); - } - - start() { - this.server = http.createServer((req, res) => { - //console.log('req', req.url, req.headers); - if ('/metrics' == req.url) return this.serveMetrics(res); - if ('POST' == req.method && '/rerun' == req.url) return this.serveRerun(res); - res.writeHead(404, { 'Content-Length': '0' }); - res.end(); - }); - this.server.on('clientError', (err, socket) => { - socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); - }); - this.server.listen(this.port, '0.0.0.0'); - console.log('Server listening on port', this.port); - } - - stop() { - this.server.close(); - } - -} - const server = new MetricsServer({ port: PORT, - getMetrics: () => register.metrics() + getMetrics: () => register.metrics(), + tracker }); server.start(); -class MetricsReporter { - - constructor(globalConfig, options) { - this._globalConfig = globalConfig; - this._options = options; - // Can't change how Jest instantiates the reporter, so get this one from global - this._tracker = tracker; - } - - onRunStart() { - //console.log('onRunStart', arguments); - } - - onTestStart() { - //console.log('onTestStart', arguments); - } - - onRunComplete(contexts, results) { - //console.log('onRunComplete', contexts, results); - const { testResults } = results; - for (let i = 0; i < testResults.length; i++) { - const { testFilePath, numFailingTests } = testResults[i]; - this._tracker.pathSeen(testFilePath, { numFailingTests }); - } - test_suites_run.set(results.numTotalTestSuites); - test_suites_run_total.inc(results.numTotalTestSuites); - tests_run.set(results.numTotalTests); - tests_run_total.inc(results.numTotalTests); - assertions_failed_total.inc(results.numFailedTests); - if (!this._globalConfig.watch && !this._globalConfig.watchAll) { - //console.log('Not a watch run. Exiting'); - server.stop(); - } - reruns.onRunComplete(); - } - -} - -module.exports = MetricsReporter; +module.exports = function (globalConfig, options) { + return new MetricsReporter({ + isWatching: !globalConfig.watch && !globalConfig.watchAll, + tracker, + metrics: { + assertions_failed, + assertions_failed_total, + tests_run, + tests_run_total, + test_suites_run, + test_suites_run_total, + assert_files_seen, + }, + reruns, + server + }) +}; diff --git a/runtime-nodejs/package-lock.json b/runtime-nodejs/package-lock.json index e090bf7..4800124 100644 --- a/runtime-nodejs/package-lock.json +++ b/runtime-nodejs/package-lock.json @@ -362,6 +362,47 @@ "minimist": "^1.2.0" } }, + "@eslint/eslintrc": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.2.tgz", + "integrity": "sha512-EfB5OHNYp1F4px/LI/FEnGylop7nOqkQ1LRzCM0KccA2U8tvV8w01KBv37LbO7nW4H+YhKyo2LcJhRwjjV17QQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + } + } + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -779,6 +820,12 @@ "acorn-walk": "^7.1.1" } }, + "acorn-jsx": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", + "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", + "dev": true + }, "acorn-walk": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", @@ -804,6 +851,12 @@ "uri-js": "^4.2.2" } }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, "ansi-escapes": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", @@ -888,6 +941,12 @@ "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, "async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", @@ -1459,6 +1518,15 @@ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.0.0.tgz", "integrity": "sha512-JC/eHYEC3aSS0vZGjuoc4vHA0yAQTzhQQldXMeMF+JlxLGJlCO38Gma82NV9gk1jGFz8mDzUMeaKXvjRRdJ2dg==" }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, "domexception": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", @@ -1501,6 +1569,15 @@ "once": "^1.4.0" } }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -1531,11 +1608,263 @@ "source-map": "~0.6.1" } }, + "eslint": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.17.0.tgz", + "integrity": "sha512-zJk08MiBgwuGoxes5sSQhOtibZ75pz0J35XTRlZOk9xMffhpA9BTbQZxoXZzOl5zMbleShbGwtw+1kGferfFwQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@eslint/eslintrc": "^0.2.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^6.0.0", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.4", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + }, + "espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, + "esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, "estraverse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", @@ -1800,6 +2129,15 @@ "bser": "2.1.1" } }, + "file-entry-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz", + "integrity": "sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -1817,6 +2155,22 @@ "path-exists": "^4.0.0" } }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.0.tgz", + "integrity": "sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==", + "dev": true + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -1865,6 +2219,12 @@ "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", "optional": true }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, "gensync": { "version": "1.0.0-beta.1", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", @@ -1914,6 +2274,15 @@ "path-is-absolute": "^1.0.0" } }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -2091,6 +2460,30 @@ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } + } + }, "import-local": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", @@ -2225,6 +2618,12 @@ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2235,6 +2634,15 @@ "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==" }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -2882,6 +3290,12 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -3413,6 +3827,15 @@ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, "parse-json": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.1.tgz", @@ -3521,6 +3944,12 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, "prom-client": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-12.0.0.tgz", @@ -3648,6 +4077,12 @@ "safe-regex": "^1.1.0" } }, + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -3725,6 +4160,12 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -4032,6 +4473,17 @@ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -4344,6 +4796,12 @@ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, "supports-color": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", @@ -4366,6 +4824,44 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, + "table": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/table/-/table-6.0.7.tgz", + "integrity": "sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g==", + "dev": true, + "requires": { + "ajv": "^7.0.2", + "lodash": "^4.17.20", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.0" + }, + "dependencies": { + "ajv": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.0.3.tgz", + "integrity": "sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + } + } + }, "tdigest": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz", @@ -4393,6 +4889,12 @@ "minimatch": "^3.0.4" } }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, "throat": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", @@ -4604,6 +5106,12 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" }, + "v8-compile-cache": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", + "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==", + "dev": true + }, "v8-to-istanbul": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-4.1.4.tgz", diff --git a/runtime-nodejs/package.json b/runtime-nodejs/package.json index ac765bf..b6d3626 100644 --- a/runtime-nodejs/package.json +++ b/runtime-nodejs/package.json @@ -4,7 +4,8 @@ "description": "", "main": "index.js", "scripts": { - "test": "./node_modules/.bin/jest" + "test": "./node_modules/.bin/jest", + "unit-tests": "./node_modules/.bin/jest ./src --reporters=default" }, "author": "", "license": "ISC", @@ -15,5 +16,8 @@ "node-fetch": "2.6.1", "prom-client": "12.0.0", "uuid": "3.4.0" + }, + "devDependencies": { + "eslint": "7.17.0" } } diff --git a/runtime-nodejs/src/MetricsReporter.js b/runtime-nodejs/src/MetricsReporter.js new file mode 100644 index 0000000..21dc5ce --- /dev/null +++ b/runtime-nodejs/src/MetricsReporter.js @@ -0,0 +1,38 @@ +module.exports = class MetricsReporter { + + constructor({ tracker, isWatching, metrics, server, reruns }) { + this._tracker = tracker; + this._isWatching = isWatching; + this._metrics = metrics; + this._server = server; + this._reruns = reruns; + } + + onRunStart() { + //console.log('onRunStart', arguments); + } + + onTestStart() { + //console.log('onTestStart', arguments); + } + + onRunComplete(contexts, results) { + //console.log('onRunComplete', contexts, results); + const { testResults } = results; + for (let i = 0; i < testResults.length; i++) { + const { testFilePath, numFailingTests } = testResults[i]; + this._tracker.pathSeen(testFilePath, { numFailingTests }); + } + this._metrics.test_suites_run.set(results.numTotalTestSuites); + this._metrics.test_suites_run_total.inc(results.numTotalTestSuites); + this._metrics.tests_run.set(results.numTotalTests); + this._metrics.tests_run_total.inc(results.numTotalTests); + this._metrics.assertions_failed_total.inc(results.numFailedTests); + if (!this._isWatching) { + //console.log('Not a watch run. Exiting'); + this._server.stop(); + } + this._reruns.onRunComplete(contexts, results); + } + +} \ No newline at end of file diff --git a/runtime-nodejs/src/MetricsServer.js b/runtime-nodejs/src/MetricsServer.js new file mode 100644 index 0000000..a604509 --- /dev/null +++ b/runtime-nodejs/src/MetricsServer.js @@ -0,0 +1,42 @@ +const http = require('http'); + +module.exports = class MetricsServer { + + constructor({ port, getMetrics, tracker }) { + this.port = port; + this.getMetrics = getMetrics; + this._tracker = tracker; + } + + serveMetrics(res) { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end(this.getMetrics()); + } + + serveRerun(res) { + console.log('Rerun endpoint called'); + this._tracker.modifyAll(); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('{}'); + } + + start() { + this.server = http.createServer((req, res) => { + //console.log('req', req.url, req.headers); + if ('/metrics' == req.url) return this.serveMetrics(res); + if ('POST' == req.method && '/rerun' == req.url) return this.serveRerun(res); + res.writeHead(404, { 'Content-Length': '0' }); + res.end(); + }); + this.server.on('clientError', (err, socket) => { + socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); + }); + this.server.listen(this.port, '0.0.0.0'); + console.log('Server listening on port', this.port); + } + + stop() { + this.server.close(); + } + +} \ No newline at end of file diff --git a/runtime-nodejs/src/Reruns.js b/runtime-nodejs/src/Reruns.js new file mode 100644 index 0000000..ce2b112 --- /dev/null +++ b/runtime-nodejs/src/Reruns.js @@ -0,0 +1,41 @@ + +/* + * The idea ... + * We'll only get good specs if the development experience is attractive + * which is why runtime-nodejs prioritizes the use case skaffold dev with sync + * Until https://github.com/facebook/jest/issues/5048 https://github.com/facebook/jest/issues/8868 are fixed + * ... or we want to give something like https://medium.com/web-developers-path/how-to-run-jest-programmatically-in-node-js-jest-javascript-api-492a8bc250de a try + * ... or we find a way to emulate interactive watch keypresses + * it'll mean that we run watch always and that the unattended run mode will be a bit of a hack + * The basic requirement is that tests are rerun. + * We rerun all tests, not only failed, because when it comes to infra things go up and down. + */ +module.exports = class Reruns { + + constructor({ tracker, rerunWaitMs, assertIsDev, requiredSuccessions = 3 }) { + console.log('Activating reruns with interval (ms)', rerunWaitMs); + this._rerunWaitMs = rerunWaitMs; + this._timeout = null; + this._tracker = tracker; + this._assertIsDev = assertIsDev; + this._requiredSuccessions = requiredSuccessions; + + this._state = { successions: 0 }; + } + + onRunComplete(contexts, results) { + if (results.numFailedTests === 0) this._state.successions++; + else this._state.successions = 0; + + this._timeout !== null && clearTimeout(this._timeout); + + if (this._state.successions > this._requiredSuccessions) return; + + if (!this._assertIsDev && this._rerunWaitMs) { + this._timeout = setTimeout(() => { + this._tracker.modifyAll(); + }, this._rerunWaitMs); + } + } + +} \ No newline at end of file diff --git a/runtime-nodejs/src/Reruns.spec.js b/runtime-nodejs/src/Reruns.spec.js new file mode 100644 index 0000000..9b405b3 --- /dev/null +++ b/runtime-nodejs/src/Reruns.spec.js @@ -0,0 +1,59 @@ +const Reruns = require("./Reruns"); + +describe('Reruns', function () { + + it('helps us rerun tests by triggering file modifications outside of development', async function () { + const trackerMock = { + modifyAll: jest.fn() + }; + + const reruns = new Reruns({ + assertIsDev: false, + rerunWaitMs: 1, + tracker: trackerMock + }); + + reruns.onRunComplete({}, { numFailedTests: 0 }); + await new Promise(resolve => setTimeout(resolve, 1)); + expect(trackerMock.modifyAll).toHaveBeenCalledTimes(1); + await new Promise(resolve => setTimeout(resolve, 1)); + expect(trackerMock.modifyAll).toHaveBeenCalledTimes(1); + + reruns.onRunComplete({}, { numFailedTests: 0 }); + await new Promise(resolve => setTimeout(resolve, 1)); + expect(trackerMock.modifyAll).toHaveBeenCalledTimes(2); + }); + + it('stops reruns after a number of consequtive successions', async function () { + const trackerMock = { + modifyAll: jest.fn() + }; + + const reruns = new Reruns({ + assertIsDev: false, + requiredSuccessions: 2, + rerunWaitMs: 1, + tracker: trackerMock + }); + + reruns.onRunComplete({}, { numFailedTests: 0 }); + await new Promise(resolve => setTimeout(resolve, 1)); + expect(trackerMock.modifyAll).toHaveBeenCalledTimes(1); + + reruns.onRunComplete({}, { numFailedTests: 1 }); + await new Promise(resolve => setTimeout(resolve, 1)); + expect(trackerMock.modifyAll).toHaveBeenCalledTimes(2); + + reruns.onRunComplete({}, { numFailedTests: 0 }); + await new Promise(resolve => setTimeout(resolve, 1)); + expect(trackerMock.modifyAll).toHaveBeenCalledTimes(3); + + reruns.onRunComplete({}, { numFailedTests: 0 }); + await new Promise(resolve => setTimeout(resolve, 1)); + expect(trackerMock.modifyAll).toHaveBeenCalledTimes(4); + + reruns.onRunComplete({}, { numFailedTests: 0 }); + await new Promise(resolve => setTimeout(resolve, 1)); + expect(trackerMock.modifyAll).toHaveBeenCalledTimes(4); + }); +}); \ No newline at end of file diff --git a/runtime-nodejs/src/SpecFilesTracker.js b/runtime-nodejs/src/SpecFilesTracker.js new file mode 100644 index 0000000..d8ac913 --- /dev/null +++ b/runtime-nodejs/src/SpecFilesTracker.js @@ -0,0 +1,43 @@ +const fs = require('fs'); + +module.exports = class SpecFilesTracker { + + constructor({ metrics }) { + this._pathsSeen = {}; + this._metrics = metrics; + } + + pathSeen(path, { numFailingTests }) { + // eslint-disable-next-line no-prototype-builtins + if (this._pathsSeen.hasOwnProperty(path)) { + const delta = numFailingTests - this._pathsSeen[path].numFailingTests; + if (delta > 0) this._metrics.assertions_failed.inc(delta); + if (delta < 0) this._metrics.assertions_failed.dec(-delta); + } else { + this._pathsSeen[path] = {}; + this._metrics.assert_files_seen.inc(); + if (numFailingTests > 0) this._metrics.assertions_failed.inc(numFailingTests); + console.log('Path reported for the first time:', path); + } + this._pathsSeen[path].numFailingTests = numFailingTests; + } + + modify(path) { + const insignificant = '\n'; + fs.appendFile(path, insignificant, 'utf8', (err) => { + if (err) throw err; + //console.log('Modified', path); + }); + } + + modifyAll() { + const modify = this.modify.bind(this); + const paths = Object.keys(this._pathsSeen); + console.log('Files will be modified to trigger rerun:', paths); + paths.map(path => { + // We prefer to do this as concurrent as possible, to trigger a single run, so don't wait + modify(path); + }); + } + +} \ No newline at end of file