Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve integ. with Jest in terms of logging #1351

Merged
merged 8 commits into from
May 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions detox/local-cli/templates/jest.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
const firstTestContent = require('./firstTestContent');
const runnerConfig = `{
"setupFilesAfterEnv": ["./init.js"],
"testEnvironment": "node"
"testEnvironment": "node",
"reporters": ["detox/runners/jest/streamlineReporter"],
"verbose": true
}
`;

const initjsContent = `const detox = require('detox');
const config = require('../package.json').detox;
const adapter = require('detox/runners/jest/adapter');
const specReporter = require('detox/runners/jest/specReporter');

// Set the default timeout
jest.setTimeout(120000);
jasmine.getEnv().addReporter(adapter);

// This takes care of generating status logs on a per-spec basis. By default, jest only reports at file-level.
// This is strictly optional.
jasmine.getEnv().addReporter(specReporter);

beforeAll(async () => {
await detox.init(config);
});
Expand All @@ -23,7 +31,8 @@ beforeEach(async () => {
afterAll(async () => {
await adapter.afterAll();
await detox.cleanup();
});`;
});
`;
noomorph marked this conversation as resolved.
Show resolved Hide resolved

exports.initjs = initjsContent;
exports.firstTest = firstTestContent;
Expand Down
12 changes: 12 additions & 0 deletions detox/local-cli/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ module.exports.builder = {
default: 1,
number: true
},
'jest-report-specs': {
group: 'Execution:',
describe: '[Jest Only] Whether to output logs per each running spec, in real-time. By default, disabled with multiple workers.',
},
H: {
alias: 'headless',
group: 'Execution:',
Expand Down Expand Up @@ -235,6 +239,13 @@ module.exports.handler = async function test(program) {
program.w = program.workers = 1;
}

const jestReportSpecsArg = program['jest-report-specs'];
if (!_.isUndefined(jestReportSpecsArg)) {
program.reportSpecs = (jestReportSpecsArg.toString() === 'true');
} else {
program.reportSpecs = (program.workers === 1);
}

const command = _.compact([
path.join('node_modules', '.bin', runner),
(runnerConfig ? `--config=${runnerConfig}` : ''),
Expand All @@ -258,6 +269,7 @@ module.exports.handler = async function test(program) {
'recordVideos',
'recordPerformance',
'deviceName',
'reportSpecs',
]);

log.info(printEnvironmentVariables(detoxEnvironmentVariables) + command);
Expand Down
73 changes: 73 additions & 0 deletions detox/local-cli/test.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,79 @@ describe('test', () => {
);
});

describe('specs reporting (propagated) switch', () => {
const expectReportSpecsArg = ({value}) => expect(mockExec).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
env: expect.objectContaining({
reportSpecs: value,
}),
})
);

describe('ios', () => {
beforeEach(() => mockPackageJson({
'test-runner': 'jest',
configurations: {
only: {
type: 'ios.sim'
}
}
}));

it('should be enabled for a single worker', async () => {
await callCli('./test', 'test --workers 1');
expectReportSpecsArg({value: true});
});

it('should be disabled for multiple workers', async () => {
await callCli('./test', 'test --workers 2');
expectReportSpecsArg({value: false});
});

it('should be enabled in case no specific workers config has been specified', async () => {
await callCli('./test', 'test');
expectReportSpecsArg({value: true});
});

it('should be enabled if custom --jest-report-specs switch is specified', async () => {
await callCli('./test', 'test --workers 2 --jest-report-specs');
expectReportSpecsArg({value: true});
});

it('should be disabled if custom switch has non-true value', async () => {
await callCli('./test', 'test --jest-report-specs meh');
expectReportSpecsArg({value: false});
});

it('should be enabled if custom switch has explicit value of \'true\'', async () => {
await callCli('./test', 'test --workers 2 --jest-report-specs true');
expectReportSpecsArg({value: true});
});
});

describe('android', () => {
beforeEach(() => mockPackageJson({
'test-runner': 'jest',
configurations: {
only: {
type: 'android.emulator'
}
}
}));

it('should align with fallback to single-worker', async () => {
await callCli('./test', 'test --workers 2');
expectReportSpecsArg({value: true});
});

it('should adhere to custom --jest-report-specs switch, as with ios', async () => {
await callCli('./test', 'test --workers 2 --jest-report-specs false');
expectReportSpecsArg({value: false});
});
});
});

it('sets default value for debugSynchronization', async () => {
mockPackageJson({
configurations: {
Expand Down
96 changes: 96 additions & 0 deletions detox/runners/jest/DetoxStreamlineJestReporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
const {VerboseReporter: JestVerboseReporter} = require('@jest/reporters'); // eslint-disable-line node/no-extraneous-require
const DetoxRuntimeError = require('../../src/errors/DetoxRuntimeError');

class DetoxStreamlineJestReporter extends JestVerboseReporter {

constructor(globalConfig) {
super(globalConfig);
this._assertConfig();
}

/**
* Monkey patch for _wrapStdio method of Jest's DefaultReporter class
* https://github.com/facebook/jest/blob/84466b7bb187d33ffd336bd9fc76111bba511fe6/packages/jest-reporters/src/default_reporter.ts#L47
*
* The official implementation does the following:
* - For the <b>stderr</b> stream, it overrides the 'write' method with a simple bulked output mechanism,
* which aggregates output onto a buffer but flushes it immediately.
* - For the <b>stdout</b> stream, it overrides the 'write' method with a time-based bulked output mechanism,
* which aggregates output onto a buffer and flushes only in 100ms intervals.
*
* This gives priority, to a certain extent, to stderr output, over stdout.
* See: https://github.com/facebook/jest/blob/84466b7bb187d33ffd336bd9fc76111bba511fe6/packages/jest-reporters/src/default_reporter.ts#L73
*
* Typically, user logs are sent to stdout, and Jest reporter's (e.g. test-suite summary) - to stderr.
*
* ---
* Our goal is to have these 3 types of output streamlined in real time:
*
* 1. Jest suite-level lifecycle logging, typically done by the super-class' impl.
* Note: Jest does not notify spec-level events to reporters.
* 2. Jasmine real-time, spec-level lifecycle logging.
* 3. User in-test logging (e.g. for debugging).
*
* It's easy to see that this cannot be done while stderr and stdout are not of equal priority.
* Therefore, this hack enforces immediate-flushing approach to <b>both</b> stderr and stdout.
*/
_wrapStdio(stream) {
const originalWrite = stream.write;
let buffer = [];

const flushBufferedOutput = () => {
const string = buffer.join('');
buffer = []; // This is to avoid conflicts between random output and status text

this._clearStatus();

if (string) {
originalWrite.call(stream, string);
}

this._printStatus();

this._bufferedOutput.delete(flushBufferedOutput);
};

this._bufferedOutput.add(flushBufferedOutput);

stream.write = chunk => {
buffer.push(chunk);
flushBufferedOutput();
return true;
};
}

_assertConfig() {
if (!this._isVerboseEnabled()) {
// Non-verbose mode makes Jest swizzle 'console' with a buffered output impl, which prevents
// user and detox' jasmine-lifecycle logs from showing in real time.
throw new DetoxRuntimeError({
message: 'Cannot run properly unless Jest is in verbose mode',
hint: 'See https://jestjs.io/docs/en/configuration#verbose-boolean for more details',
});
}

if (this._hasDefaultReporter()) {
// This class overrides Jest's VerboseReporter, which is set by default. Can't have both.
throw new DetoxRuntimeError({
message: 'Cannot work alongside the default Jest reporter. Please remove it from the reporters list.',
hint: 'See https://jestjs.io/docs/en/configuration#reporters-array-modulename-modulename-options for more details',
});
}
}

_isVerboseEnabled() {
return !!this._globalConfig.verbose;
}

_hasDefaultReporter() {
return !!this._globalConfig.reporters.find(reporterDef => {
const [reporterName] = reporterDef;
return reporterName === 'default';
});
}
}

module.exports = DetoxStreamlineJestReporter;
69 changes: 69 additions & 0 deletions detox/runners/jest/JasmineSpecReporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
const chalk = require('chalk').default;

/***
* @see {@link https://jasmine.github.io/api/2.9/Reporter.html}
*/
class JasmineSpecReporter {

constructor() {
this._suites = [];
this._suitesDesc = '';
}

suiteStarted(suiteInfo) {
this._suites.push(suiteInfo);
this._regenerateSuitesDesc();
}

suiteDone() {
this._suites.pop();
this._regenerateSuitesDesc();

if (!this._suites.length) {
this._traceln('');
}
}

specStarted(result) {
this._traceSpec(result);
}

specDone(result) {
if (result.status === 'disabled') {
this._traceSpec(result, chalk.yellow('SKIPPED'));
} else if (result.status === 'failed') {
this._traceSpec(result, chalk.red('FAIL'));
} else if (result.pendingReason) {
this._traceSpec(result, chalk.yellow('PENDING'));
} else {
this._traceSpec(result, chalk.green('OK'));
}
}

_regenerateSuitesDesc() {
this._suitesDesc = '';

const total = this._suites.length;
this._suites.forEach((suite, index) => {
this._suitesDesc = this._suitesDesc
.concat((index > 0) ? ' > ' : '')
.concat(chalk.bold.white(suite.description))
.concat((index === total - 1) ? ': ' : '');
});
}

_traceSpec({description}, status) {
this._traceln(this._suitesDesc + chalk.gray(description) + chalk.gray(status ? ` [${status}]` : ''));
}

_trace(message) {
process.stdout.write(message);
}

_traceln(message) {
this._trace(message);
process.stdout.write('\n');
}
}

module.exports = JasmineSpecReporter;
2 changes: 1 addition & 1 deletion detox/runners/jest/adapter.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const detox = require('../../src/index');
const DetoxJestAdapter = require('./DetoxJestAdapter');

module.exports = new DetoxJestAdapter(detox);
module.exports = new DetoxJestAdapter(detox);
8 changes: 8 additions & 0 deletions detox/runners/jest/specReporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const argparse = require('../../src/utils/argparse');

if (argparse.getArgValue('reportSpecs') === 'true') {
const Reporter = require('./JasmineSpecReporter');
module.exports = new Reporter();
} else {
module.exports = {};
}
6 changes: 6 additions & 0 deletions detox/runners/jest/streamlineReporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* This is here to make it easier for users to apply this reporter in config.json (naming-wise)
*/

const Reporter = require('./DetoxStreamlineJestReporter');
module.exports = Reporter;
12 changes: 6 additions & 6 deletions docs/APIRef.DetoxCLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ Initiating your test suite. <sup>[[1]](#notice-passthrough)</sup>
| --record-performance [all/none] | [iOS Only] Save Detox Instruments performance recordings of each test to artifacts directory. The default value is **none**. |
| -r, --reuse | Reuse existing installed app (do not delete + reinstall) for a faster run. |
| -u, --cleanup | Shutdown simulator when test is over, useful for CI scripts, to make sure detox exists cleanly with no residue |
| -w, --workers | [iOS Only] Specifies number of workers the test runner should spawn, requires a test runner with parallel execution support (Detox CLI currently supports Jest) |
| -w, --workers | [iOS Only] Specifies number of workers the test runner should spawn, requires a test runner with parallel execution support (Detox CLI currently supports Jest). *Note: For workers > 1, Jest's spec-level reporting is disabled, by default (can be overridden using --jest-report-specs).* |
| --jest-report-specs | [Jest Only] Whether to output logs per each running spec, in real-time. By default, disabled with multiple workers. |
| -H, --headless | [Android Only] Launch Emulator in headless mode. Useful when running on CI. |
| --gpu | [Android Only] Launch Emulator with the specific -gpu [gpu mode] parameter. |
| --no-color | Disable colors in log output |
Expand All @@ -91,12 +92,11 @@ Initiating your test suite. <sup>[[1]](#notice-passthrough)</sup>
of a supported test runner, so for the most part it reads configuration from CLI args and `package.json` and remaps it
to command-line arguments or environment variables that are supported by (or not conflict with) the test runner.
Hence, **extra arguments to** `detox test` **will be forwarded to your test runner**, e.g:

* You run `detox test --bail`, and since `--bail` is an unknown option, it will be forwarded to the test runner as-is.
* You run `detox test --bail`, and since `--bail` is an unknown option, it will be forwarded to the test runner as-is.
* If there is a name conflict for some option (between the test runner and `detox test`), you can pass it explicitly
after the reserved `--` sequence. For instance, `detox test -- --help`, will pass `--help` to the test runner CLI
itself.

after the reserved `--` sequence. For instance, `detox test -- --help`, will pass `--help` to the test runner CLI
itself.
2. <a name="notice-artifacts">If</a> `--artifacts-location` path does not end with a slash (`/`) or a backslash, then detox CLI will append to the
path a subdirectory with configuration name and timestamp (e.g. `artifacts/android.emu.release.2018-06-12 05:52:43Z`).
In other words, the path with a slash at the end assumes you do not want a subdirectory inside.
Expand Down
Loading