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

jest-diff CLI #7781

Closed
wants to merge 4 commits into from
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- `[jest-jasmine2]` Will now only execute at most 5 concurrent tests _within the same testsuite_ when using `test.concurrent` ([#7770](https://github.com/facebook/jest/pull/7770))
- `[jest-circus]` Same as `[jest-jasmine2]`, only 5 tests will run concurrently by default ([#7770](https://github.com/facebook/jest/pull/7770))
- `[jest-config]` A new `maxConcurrency` option allows to change the number of tests allowed to run concurrently ([#7770](https://github.com/facebook/jest/pull/7770))
- `[jest-diff]` Provide a CLI for diffing JSON ([#7781](https://github.com/facebook/jest/pull/7781))

### Fixes

Expand Down
8 changes: 8 additions & 0 deletions docs/JestPlatform.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ const result = diff(a, b);
console.log(result);
```

### CLI

It also comes with a CLI, so if you install `jest-diff` globally, you can use it to compare JSON files similarly to the UNIX program `diff`:

```sh-session
$ jest-diff a.json b.json
```

## jest-docblock

Tool for extracting and parsing the comments at the top of a JavaScript file. Exports various functions to manipulate the data inside the comment block.
Expand Down
24 changes: 24 additions & 0 deletions e2e/__tests__/__snapshots__/jestDiffCli.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`different 1`] = `
- Expected
+ Received

Array [
- 42,
+ 1337,
Object {
- "a": true,
+ "b": false,
},
]

`;

exports[`help 1`] = `
jest-diff.js a.json b.json

Options:
-v, --version Show version number [boolean]
-h, --help Show help [boolean]
`;
102 changes: 102 additions & 0 deletions e2e/__tests__/jestDiffCli.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import execa from 'execa';
import {tmpdir} from 'os';
import {resolve} from 'path';
import {wrap} from 'jest-snapshot-serializer-raw';
import stripAnsi from 'strip-ansi';

import {cleanup, writeFiles} from '../Utils';

const DIR = resolve(tmpdir(), 'jest-diff-cli');
const JEST_DIFF = resolve(
__dirname,
'../../packages/jest-diff/bin/jest-diff.js',
);

beforeAll(() =>
writeFiles(DIR, {
'a.json': '[42, {"a": true}]',
'b.json': '[1337, {"b": false}]',
'not.json': 'not JSON',
}),
);
afterAll(() => cleanup(DIR));

test('equal', async () => {
const {code, stdout} = await execa(JEST_DIFF, ['a.json', 'a.json'], {
cwd: DIR,
});
expect(code).toBe(0);
expect(stdout).toBe('');
});

test('different', async () => {
const {code, stdout} = await execa(JEST_DIFF, ['a.json', 'b.json'], {
cwd: DIR,
reject: false,
});
expect(code).toBe(1);
expect(wrap(stripAnsi(stdout))).toMatchSnapshot();
});

// --- IO-related ---

test('file does not exist', async () => {
const {code, stderr} = await execa(JEST_DIFF, ['a.json', 'nope.jpg'], {
cwd: DIR,
reject: false,
});
expect(code).toBe(20);
expect(stderr).toMatch(/no such file.*nope\.jpg/);
});

test('not JSON', async () => {
const {code, stderr} = await execa(JEST_DIFF, ['a.json', 'not.json'], {
cwd: DIR,
reject: false,
});
expect(code).toBe(30);
expect(stderr).toMatch(/Failed to parse.*JSON/);
});

// --- CLI-related ---

describe('invalid usage', () => {
test('too few arguments', async () => {
const {code, stderr} = await execa(JEST_DIFF, ['a'], {reject: false});
expect(code).toBe(2);
expect(stderr).toEqual(
expect.stringContaining('Not enough non-option arguments'),
);
});

test('too many arguments', async () => {
const {code, stderr} = await execa(JEST_DIFF, ['a', 'b', 'c'], {
reject: false,
});
expect(code).toBe(2);
expect(stderr).toEqual(
expect.stringContaining('Too many non-option arguments'),
);
});
});

test('version', async () => {
const {code, stdout} = await execa(JEST_DIFF, ['-v']);
expect(code).toBe(0);
expect(stdout).toMatch(/\d{2}\.\d{1,2}\.\d{1,2}[\-\S]*/);
});

test('help', async () => {
const {code, stdout} = await execa(JEST_DIFF, ['-h']);
expect(code).toBe(0);
expect(wrap(stdout)).toMatchSnapshot();
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"clean-e2e": "find ./e2e -not \\( -path ./e2e/presets/js -prune \\) -not \\( -path ./e2e/presets/json -prune \\) -mindepth 2 -type d \\( -name node_modules -prune \\) -exec rm -r '{}' +",
"jest": "node ./packages/jest-cli/bin/jest.js",
"jest-coverage": "yarn jest --coverage",
"jest-diff": "node ./packages/jest-diff/bin/jest-diff.js",
"lint": "eslint . --cache --report-unused-disable-directives --ext js,md",
"lint-es5-build": "eslint --no-eslintrc --no-ignore --env=browser packages/*/build-es5",
"lint:md": "yarn --silent lint:md:ci --fix",
Expand Down
11 changes: 11 additions & 0 deletions packages/jest-diff/bin/jest-diff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env node

/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';

require('../build/cli').default();
6 changes: 5 additions & 1 deletion packages/jest-diff/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
"chalk": "^2.0.1",
"diff-sequences": "^24.0.0",
"jest-get-type": "^24.0.0",
"pretty-format": "^24.0.0"
"pretty-format": "^24.0.0",
"yargs": "^12.0.5"
},
"bin": {
"jest-diff": "./bin/jest-diff.js"
},
"engines": {
"node": ">= 6"
Expand Down
79 changes: 79 additions & 0 deletions packages/jest-diff/src/cli/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import yargs from 'yargs';
import {readFileSync} from 'fs';

// eslint-disable-next-line import/default
import jestDiff from '../';
import {NO_DIFF_MESSAGE} from '../constants';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is IMO an example of where a separate CLI would force a better API.

It's imported to allow the CLI to remove a message that shouldn't really be in a diffing algorithm. Instead it should probably be an option (or structured return values, or something else entirely), but since we have access to private stuff, it's more natural to just import it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it really bugged me writing this that the return values of jest-diff are so weird 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, those constants :(

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we fix this without a breaking change?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, adding an option which changes the returned thing (from a string to something structured) should be fine in a minor (as long as it's opt-in). Might be a bit painful WRT flow types but probably fine

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll fool around with some non-breaking API designs.
Probably on the weekend. So much work, so little time for my Jest todo list :/

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, mee too :/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the null return values for asymmetric matchers: Would it be fine to just treat them like "no difference"? The reason they were originally introduced was #8146 - in that case, you would get the "No visual difference" message as well. Sooner or later we'll have to rethink jest-diff (and expect and pretty-format) in light of #6184 , but for now it would be good to have just "differs, here's the string" and "does not differ (visually)" as returns values so that we can come up with a clean and simple API

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Actually, what's now SIMILAR_MESSAGE will need to have a special case in the return value too :/ )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should return something like {differs: true, message: string, toJsonIgnored: boolean} | {differs: false}


/* eslint-disable sort-keys */
const EXIT_CODES = {
// like GNU diff
EQUAL: 0,
DIFFERENT: 1,
INVALID_USAGE: 2,

// more specific than GNU diff, which exits with 2 in all error cases
INTERNAL_ERROR: 10,
IO_ERROR: 20,
INVALID_JSON: 30,
};
/* eslint-enable sort-keys */

/* eslint-disable-next-line consistent-return */
const read = (path: string) => {
try {
const data = readFileSync(path);
try {
return JSON.parse(String(data));
} catch (parseErr) {
console.error(`Failed to parse file ${path} as JSON`, parseErr);
process.exit(EXIT_CODES.INVALID_JSON);
}
} catch (readErr) {
console.error(`Failed to read file ${path}`, readErr);
process.exit(EXIT_CODES.IO_ERROR);
}
};

export default async () => {
const parser = yargs(process.argv.slice(2))
.demandCommand(2, 2) // positional

.version()
.alias('v', 'version')

.usage('$0 a.json b.json')
.help()
.alias('h', 'help')

.fail((msg, err, yargs) => {
if (err) throw err;
console.error(yargs.help());
console.error(msg);
process.exit(EXIT_CODES.INVALID_USAGE);
});

const [aPath, bPath] = parser.argv._;
const a = read(aPath);
const b = read(bPath);

const diffMsg = jestDiff(a, b);
if (diffMsg == null) {
console.error('diff unexpectedly returned null');
process.exit(EXIT_CODES.INTERNAL_ERROR);
} else if (diffMsg === NO_DIFF_MESSAGE) {
process.exit(EXIT_CODES.EQUAL);
} else {
console.log(diffMsg);
process.exit(EXIT_CODES.DIFFERENT);
}
};
26 changes: 26 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -13089,6 +13089,14 @@ yargs-parser@^11.1.1:
camelcase "^5.0.0"
decamelize "^1.2.0"

yargs-parser@^11.1.1:
version "11.1.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==
dependencies:
camelcase "^5.0.0"
decamelize "^1.2.0"

yargs-parser@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9"
Expand Down Expand Up @@ -13139,6 +13147,24 @@ yargs@^12.0.1, yargs@^12.0.2:
y18n "^3.2.1 || ^4.0.0"
yargs-parser "^11.1.1"

yargs@^12.0.5:
version "12.0.5"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==
dependencies:
cliui "^4.0.0"
decamelize "^1.2.0"
find-up "^3.0.0"
get-caller-file "^1.0.1"
os-locale "^3.0.0"
require-directory "^2.1.1"
require-main-filename "^1.0.1"
set-blocking "^2.0.0"
string-width "^2.0.0"
which-module "^2.0.0"
y18n "^3.2.1 || ^4.0.0"
yargs-parser "^11.1.1"

yargs@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-2.3.0.tgz#e900c87250ec5cd080db6009fe3dd63156f1d7fb"
Expand Down