Skip to content

Commit

Permalink
First working example of create-jest-runner
Browse files Browse the repository at this point in the history
  • Loading branch information
rogeliog committed Oct 17, 2017
1 parent 6c782c7 commit 58a92fc
Show file tree
Hide file tree
Showing 19 changed files with 318 additions and 118 deletions.
72 changes: 40 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# create-jest-runner

A highly opinionated way for creating Jest Runners

## Install

```bash
Expand All @@ -13,65 +15,71 @@ create-jest-runner takes care of handling the appropriate parallelization and cr

You simply need two files:
* Entry file: Used by Jest as an entrypoint to your runner.
* Run file: Contains the domain logic of your runner.
* Run file: Runs once per test file, and it encapsulates the logic of your runner

### 1) Create your entry file

```js
// index.js
const createRunner = require('create-jest-runner');
module.exports = createRunner('/path/to/run.js')
const { createJestRunner } = require('create-jest-runner');
module.exports = createJestRunner(require.resolve('./run'));
```

### 2) Create your run file


```js
// run.js
module.exports = (options, workerCallback) => {
// ...
}
module.exports = (options) => {
};
```

## Run File API
### Run File API

This file should export a function that receives two parameters `(options, workerCallback)`
This file should export a function that receives one parameter with the options

### `options: { testPath, config, globalConfig }`
#### `options: { testPath, config, globalConfig }`
- `testPath`: Path of the file that is going to be tests
- `config`: Jest Project config used by this file
- `globalConfig`: Jest global config

### `workerCallback: (error, testResult) => void`
_Use this callback function to report back the results (needs to be called exactly one time)._
- `error`: Any Javascript error or a string.
- `testResult`: Needs to be an object of type https://github.com/facebook/jest/blob/master/types/TestResult.js#L131-L157
You can return one of the following values:
- `testResult`: Needs to be an object of type https://github.com/facebook/jest/blob/master/types/TestResult.js#L131-L157
- `Promise<testResult|Error>`: needs to be of above type.
- `Error`: good for reporting system error, not failed tests.



## Example of a runner

This runner "blade-runner" makes sure that these two emojis `⚔️ 🏃` are present in every file

### Reporting test results

#### Passing test suite
```js
// run.js
module.exports = (options, workerCallback) => {
if (/* something */) {
workerCallback(new Error('my message'));
}
}
// index.js
const { createJestRunner } = require('create-jest-runner');
module.exports = createJestRunner(require.resolve('./run'));
```
#### Failing test suite

### Reporting an error
You can report other errors by calling the `workerCallback` with the appropriate error.
```js
// run.js
module.exports = (options, workerCallback) => {
if (/* something */) {
workerCallback(new Error('my message'));
}
}
```
const fs = require('fs');
const { pass, fail } = require('create-jest-runner');

module.exports = ({ testPath }) => {
const start = +new Date();
const contents = fs.readFileSync(testPath, 'utf8');
const end = +new Date();

if (contents.includes('⚔️🏃')) {
return pass({ start, end, test: { path: testPath } });
}
const errorMessage = 'Company policies require ⚔️ 🏃 in every file';
return fail({
start,
end,
test: { path: testPath, errorMessage, title: 'Check for ⚔️ 🏃' },
});
};
```


## Add your runner to Jest config
Expand Down
1 change: 1 addition & 0 deletions integrationTests/__fixtures__/failing/__src__/file1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log();
4 changes: 4 additions & 0 deletions integrationTests/__fixtures__/failing/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
runner: require.resolve('../../runner'),
testMatch: ['**/__src__/**/*.js'],
};
2 changes: 2 additions & 0 deletions integrationTests/__fixtures__/passing/__src__/file1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// ⚔️🏃
console.log();
4 changes: 4 additions & 0 deletions integrationTests/__fixtures__/passing/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
runner: require.resolve('../../runner'),
testMatch: ['**/__src__/**/*.js'],
};
14 changes: 14 additions & 0 deletions integrationTests/__snapshots__/failing.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Works when it has failing tests 1`] = `
"FAIL integrationTests/__fixtures__/failing/__src__/file1.js
Company policies require ⚔️ 🏃 in every file
✕ Check for ⚔️ 🏃
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time:
Ran all test suites.
"
`;
12 changes: 12 additions & 0 deletions integrationTests/__snapshots__/passing.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Works when it has only passing tests 1`] = `
"PASS integrationTests/__fixtures__/passing/__src__/file1.js
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time:
Ran all test suites.
"
`;
5 changes: 5 additions & 0 deletions integrationTests/failing.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const runJest = require('./runJest');

it('Works when it has failing tests', () => {
return expect(runJest('failing')).resolves.toMatchSnapshot();
});
5 changes: 5 additions & 0 deletions integrationTests/passing.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const runJest = require('./runJest');

it('Works when it has only passing tests', () => {
return expect(runJest('passing')).resolves.toMatchSnapshot();
});
37 changes: 37 additions & 0 deletions integrationTests/runJest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const execa = require('execa');
const path = require('path');

const rootDir = path.join(__dirname, '..');

const normalize = output =>
output
.replace(/\(?\d*\.?\d+m?s\)?/g, '')
.replace(/, estimated/g, '')
.replace(new RegExp(rootDir, 'g'), '/mocked-path-to-jest-runner-mocha')
.replace(new RegExp('.*at .*\\n', 'g'), 'mocked-stack-trace')
.replace(/.*at .*\\n/g, 'mocked-stack-trace')
.replace(/(mocked-stack-trace)+/, ' at mocked-stack-trace')
.replace(/\s+\n/g, '\n');

const runJest = (project, options = []) => {
// eslint-disable-next-line
jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000;
return execa(
'jest',
[
'--useStderr',
'--no-watchman',
'--no-cache',
'--projects',
path.join(__dirname, '__fixtures__', project),
].concat(options),
{
env: process.env,
},
)
.catch(t => t)
.then(({ stdout, stderr }) => `${normalize(stderr)}\n${normalize(stdout)}`);
};

module.exports = runJest;
2 changes: 2 additions & 0 deletions integrationTests/runner/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const { createJestRunner } = require('../../');
module.exports = createJestRunner(require.resolve('./run'));
18 changes: 18 additions & 0 deletions integrationTests/runner/run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const fs = require('fs');
const { pass, fail } = require('../../');

module.exports = ({ testPath }) => {
const start = +new Date();
const contents = fs.readFileSync(testPath, 'utf8');
const end = +new Date();

if (contents.includes('⚔️🏃')) {
return pass({ start, end, test: { path: testPath } });
}
const errorMessage = 'Company policies require ⚔️ 🏃 in every file';
return fail({
start,
end,
test: { path: testPath, errorMessage, title: 'Check for ⚔️ 🏃' },
});
};
81 changes: 81 additions & 0 deletions lib/createJestRunner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const Worker = require('jest-worker').default;

class CancelRun extends Error {
constructor(message) {
super(message);
this.name = 'CancelRun';
}
}

const createRunner = runPath => {
class BaseTestRunner {
constructor(globalConfig) {
this._globalConfig = globalConfig;
}

// eslint-disable-next-line
runTests(tests, watcher, onStart, onResult, onFailure, options) {
const worker = new Worker(runPath, {
exposedMethods: ['default'],
numWorkers: this._globalConfig.maxWorkers,
});

const runTestInWorker = test => {
if (watcher.isInterrupted()) {
throw new CancelRun();
}

return onStart(test).then(() => {
const baseOptions = {
config: test.context.config,
globalConfig: this._globalConfig,
testPath: test.path,
rawModuleMap: watcher.isWatchMode()
? test.context.moduleMap.getRawModuleMap()
: null,
options,
};

return worker.default(baseOptions);
});
};

const onError = (err, test) => {
return onFailure(test, err).then(() => {
if (err.type === 'ProcessTerminatedError') {
// eslint-disable-next-line no-console
console.error(
'A worker process has quit unexpectedly! ' +
'Most likely this is an initialization error.',
);
process.exit(1);
}
});
};

const onInterrupt = new Promise((_, reject) => {
watcher.on('change', state => {
if (state.interrupted) {
reject(new CancelRun());
}
});
});

const runAllTests = Promise.all(
tests.map(test =>
runTestInWorker(test)
.then(testResult => onResult(test, testResult))
.catch(error => onError(error, test)),
),
);

const cleanup = () => worker.end();

return Promise.race([runAllTests, onInterrupt]).then(cleanup, cleanup);
}
}

return BaseTestRunner;
};

module.exports = createRunner;
17 changes: 17 additions & 0 deletions lib/fail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const toTestResult = require('./toTestResult');

const fail = ({ start, end, test, errorMessage }) =>
toTestResult({
errorMessage: errorMessage || test.errorMessage,
stats: {
failures: 1,
pending: 0,
passes: 0,
start,
end,
},
tests: [Object.assign({ duration: end - start }, test)],
jestTestPath: test.path,
});

module.exports = fail;
Loading

0 comments on commit 58a92fc

Please sign in to comment.