aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRyan Cumming <[email protected]>2019-06-26 11:14:18 +0100
committerRyan Cumming <[email protected]>2019-06-26 11:31:36 +0100
commitf82ceca0bd8de2a2b0b51c96c5c1678351a7a20a (patch)
tree89e70d9965f973615dc0dfe978db002d82bf7e25
parentafd18dbcb8147cb83de408b7da310ee187faf3df (diff)
Initial Visual Studio Code unit tests
As promised in #1439 this is an initial attempt at unit testing the VSCode extension. There are two separate parts to this: getting the test framework working and unit testing the code in #1439. The test framework nearly intact from the VSCode extension generator. The main thing missing was `test/index.ts` which acts as an entry point for Mocha. This was simply copied back in. I also needed to open the test VSCode instance inside a workspace as our file URI generation depends on a workspace being open. There are two ways to run the test framework: 1. Opening the extension's source in VSCode, pressing F5 and selecting the "Extensions Test" debug target. 2. Closing all copies of VSCode and running `npm test`. This is started from the command line but actually opens a temporary VSCode window to host the tests. This doesn't attempt to wire this up to CI. That requires running a headless X11 server which is a bit daunting. I'll assess the difficulty of that in a follow-up branch. This PR is at least helpful for local development without having to induce errors on a Rust project. For the actual tests this uses snapshots of `rustc` output from a real Rust project captured from the command line. Except for extracting the `message` object and reformatting they're copied verbatim into fixture JSON files. Only four different types of diagnostics are tested but they represent the main combinations of code actions and related information possible. They can be considered the happy path tests; as we encounter corner-cases we can introduce new tests fixtures.
-rw-r--r--editors/code/.vscode/launch.json1
-rw-r--r--editors/code/package.json1
-rw-r--r--editors/code/src/commands/cargo_watch.ts66
-rw-r--r--editors/code/src/test/fixtures/rust-diagnostics/clippy/trivially_copy_pass_by_ref.json110
-rw-r--r--editors/code/src/test/fixtures/rust-diagnostics/error/E0053.json42
-rw-r--r--editors/code/src/test/fixtures/rust-diagnostics/error/E0061.json114
-rw-r--r--editors/code/src/test/fixtures/rust-diagnostics/warning/unused_variables.json72
-rw-r--r--editors/code/src/test/index.ts22
-rw-r--r--editors/code/src/test/rust_diagnostics.test.ts161
-rw-r--r--editors/code/src/test/vscode_diagnostics.test.ts164
-rw-r--r--editors/code/src/utils/vscode_diagnostics.ts73
11 files changed, 765 insertions, 61 deletions
diff --git a/editors/code/.vscode/launch.json b/editors/code/.vscode/launch.json
index b9d14dddd..c3578f476 100644
--- a/editors/code/.vscode/launch.json
+++ b/editors/code/.vscode/launch.json
@@ -20,6 +20,7 @@
20 "request": "launch", 20 "request": "launch",
21 "runtimeExecutable": "${execPath}", 21 "runtimeExecutable": "${execPath}",
22 "args": [ 22 "args": [
23 "${workspaceFolder}/src/test/",
23 "--extensionDevelopmentPath=${workspaceFolder}", 24 "--extensionDevelopmentPath=${workspaceFolder}",
24 "--extensionTestsPath=${workspaceFolder}/out/test" 25 "--extensionTestsPath=${workspaceFolder}/out/test"
25 ], 26 ],
diff --git a/editors/code/package.json b/editors/code/package.json
index ac2ba82e3..6e2dd0494 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -23,6 +23,7 @@
23 "postinstall": "node ./node_modules/vscode/bin/install", 23 "postinstall": "node ./node_modules/vscode/bin/install",
24 "fix": "prettier **/*.{json,ts} --write && tslint --project . --fix", 24 "fix": "prettier **/*.{json,ts} --write && tslint --project . --fix",
25 "lint": "tslint --project .", 25 "lint": "tslint --project .",
26 "test": "node node_modules/vscode/bin/test",
26 "prettier": "prettier **/*.{json,ts}", 27 "prettier": "prettier **/*.{json,ts}",
27 "travis": "npm run compile && npm run lint && npm run prettier -- --list-different" 28 "travis": "npm run compile && npm run lint && npm run prettier -- --list-different"
28 }, 29 },
diff --git a/editors/code/src/commands/cargo_watch.ts b/editors/code/src/commands/cargo_watch.ts
index 126a8b1b3..1ec5f8d5f 100644
--- a/editors/code/src/commands/cargo_watch.ts
+++ b/editors/code/src/commands/cargo_watch.ts
@@ -2,12 +2,17 @@ import * as child_process from 'child_process';
2import * as fs from 'fs'; 2import * as fs from 'fs';
3import * as path from 'path'; 3import * as path from 'path';
4import * as vscode from 'vscode'; 4import * as vscode from 'vscode';
5
5import { Server } from '../server'; 6import { Server } from '../server';
6import { terminate } from '../utils/processes'; 7import { terminate } from '../utils/processes';
7import { 8import {
8 mapRustDiagnosticToVsCode, 9 mapRustDiagnosticToVsCode,
9 RustDiagnostic 10 RustDiagnostic
10} from '../utils/rust_diagnostics'; 11} from '../utils/rust_diagnostics';
12import {
13 areCodeActionsEqual,
14 areDiagnosticsEqual
15} from '../utils/vscode_diagnostics';
11import { LineBuffer } from './line_buffer'; 16import { LineBuffer } from './line_buffer';
12import { StatusDisplay } from './watch_status'; 17import { StatusDisplay } from './watch_status';
13 18
@@ -184,67 +189,6 @@ export class CargoWatchProvider
184 this.statusDisplay.hide(); 189 this.statusDisplay.hide();
185 } 190 }
186 191
187 function areDiagnosticsEqual(
188 left: vscode.Diagnostic,
189 right: vscode.Diagnostic
190 ): boolean {
191 return (
192 left.source === right.source &&
193 left.severity === right.severity &&
194 left.range.isEqual(right.range) &&
195 left.message === right.message
196 );
197 }
198
199 function areCodeActionsEqual(
200 left: vscode.CodeAction,
201 right: vscode.CodeAction
202 ): boolean {
203 if (
204 left.kind !== right.kind ||
205 left.title !== right.title ||
206 !left.edit ||
207 !right.edit
208 ) {
209 return false;
210 }
211
212 const leftEditEntries = left.edit.entries();
213 const rightEditEntries = right.edit.entries();
214
215 if (leftEditEntries.length !== rightEditEntries.length) {
216 return false;
217 }
218
219 for (let i = 0; i < leftEditEntries.length; i++) {
220 const [leftUri, leftEdits] = leftEditEntries[i];
221 const [rightUri, rightEdits] = rightEditEntries[i];
222
223 if (leftUri.toString() !== rightUri.toString()) {
224 return false;
225 }
226
227 if (leftEdits.length !== rightEdits.length) {
228 return false;
229 }
230
231 for (let j = 0; j < leftEdits.length; j++) {
232 const leftEdit = leftEdits[j];
233 const rightEdit = rightEdits[j];
234
235 if (!leftEdit.range.isEqual(rightEdit.range)) {
236 return false;
237 }
238
239 if (leftEdit.newText !== rightEdit.newText) {
240 return false;
241 }
242 }
243 }
244
245 return true;
246 }
247
248 interface CargoArtifact { 192 interface CargoArtifact {
249 reason: string; 193 reason: string;
250 package_id: string; 194 package_id: string;
diff --git a/editors/code/src/test/fixtures/rust-diagnostics/clippy/trivially_copy_pass_by_ref.json b/editors/code/src/test/fixtures/rust-diagnostics/clippy/trivially_copy_pass_by_ref.json
new file mode 100644
index 000000000..d874e99bc
--- /dev/null
+++ b/editors/code/src/test/fixtures/rust-diagnostics/clippy/trivially_copy_pass_by_ref.json
@@ -0,0 +1,110 @@
1{
2 "message": "this argument is passed by reference, but would be more efficient if passed by value",
3 "code": {
4 "code": "clippy::trivially_copy_pass_by_ref",
5 "explanation": null
6 },
7 "level": "warning",
8 "spans": [
9 {
10 "file_name": "compiler/mir/tagset.rs",
11 "byte_start": 941,
12 "byte_end": 946,
13 "line_start": 42,
14 "line_end": 42,
15 "column_start": 24,
16 "column_end": 29,
17 "is_primary": true,
18 "text": [
19 {
20 "text": " pub fn is_disjoint(&self, other: Self) -> bool {",
21 "highlight_start": 24,
22 "highlight_end": 29
23 }
24 ],
25 "label": null,
26 "suggested_replacement": null,
27 "suggestion_applicability": null,
28 "expansion": null
29 }
30 ],
31 "children": [
32 {
33 "message": "lint level defined here",
34 "code": null,
35 "level": "note",
36 "spans": [
37 {
38 "file_name": "compiler/lib.rs",
39 "byte_start": 8,
40 "byte_end": 19,
41 "line_start": 1,
42 "line_end": 1,
43 "column_start": 9,
44 "column_end": 20,
45 "is_primary": true,
46 "text": [
47 {
48 "text": "#![warn(clippy::all)]",
49 "highlight_start": 9,
50 "highlight_end": 20
51 }
52 ],
53 "label": null,
54 "suggested_replacement": null,
55 "suggestion_applicability": null,
56 "expansion": null
57 }
58 ],
59 "children": [],
60 "rendered": null
61 },
62 {
63 "message": "#[warn(clippy::trivially_copy_pass_by_ref)] implied by #[warn(clippy::all)]",
64 "code": null,
65 "level": "note",
66 "spans": [],
67 "children": [],
68 "rendered": null
69 },
70 {
71 "message": "for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#trivially_copy_pass_by_ref",
72 "code": null,
73 "level": "help",
74 "spans": [],
75 "children": [],
76 "rendered": null
77 },
78 {
79 "message": "consider passing by value instead",
80 "code": null,
81 "level": "help",
82 "spans": [
83 {
84 "file_name": "compiler/mir/tagset.rs",
85 "byte_start": 941,
86 "byte_end": 946,
87 "line_start": 42,
88 "line_end": 42,
89 "column_start": 24,
90 "column_end": 29,
91 "is_primary": true,
92 "text": [
93 {
94 "text": " pub fn is_disjoint(&self, other: Self) -> bool {",
95 "highlight_start": 24,
96 "highlight_end": 29
97 }
98 ],
99 "label": null,
100 "suggested_replacement": "self",
101 "suggestion_applicability": "Unspecified",
102 "expansion": null
103 }
104 ],
105 "children": [],
106 "rendered": null
107 }
108 ],
109 "rendered": "warning: this argument is passed by reference, but would be more efficient if passed by value\n --> compiler/mir/tagset.rs:42:24\n |\n42 | pub fn is_disjoint(&self, other: Self) -> bool {\n | ^^^^^ help: consider passing by value instead: `self`\n |\nnote: lint level defined here\n --> compiler/lib.rs:1:9\n |\n1 | #![warn(clippy::all)]\n | ^^^^^^^^^^^\n = note: #[warn(clippy::trivially_copy_pass_by_ref)] implied by #[warn(clippy::all)]\n = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#trivially_copy_pass_by_ref\n\n"
110}
diff --git a/editors/code/src/test/fixtures/rust-diagnostics/error/E0053.json b/editors/code/src/test/fixtures/rust-diagnostics/error/E0053.json
new file mode 100644
index 000000000..ea5c976d1
--- /dev/null
+++ b/editors/code/src/test/fixtures/rust-diagnostics/error/E0053.json
@@ -0,0 +1,42 @@
1{
2 "message": "method `next` has an incompatible type for trait",
3 "code": {
4 "code": "E0053",
5 "explanation": "\nThe parameters of any trait method must match between a trait implementation\nand the trait definition.\n\nHere are a couple examples of this error:\n\n```compile_fail,E0053\ntrait Foo {\n fn foo(x: u16);\n fn bar(&self);\n}\n\nstruct Bar;\n\nimpl Foo for Bar {\n // error, expected u16, found i16\n fn foo(x: i16) { }\n\n // error, types differ in mutability\n fn bar(&mut self) { }\n}\n```\n"
6 },
7 "level": "error",
8 "spans": [
9 {
10 "file_name": "compiler/ty/list_iter.rs",
11 "byte_start": 1307,
12 "byte_end": 1350,
13 "line_start": 52,
14 "line_end": 52,
15 "column_start": 5,
16 "column_end": 48,
17 "is_primary": true,
18 "text": [
19 {
20 "text": " fn next(&self) -> Option<&'list ty::Ref<M>> {",
21 "highlight_start": 5,
22 "highlight_end": 48
23 }
24 ],
25 "label": "types differ in mutability",
26 "suggested_replacement": null,
27 "suggestion_applicability": null,
28 "expansion": null
29 }
30 ],
31 "children": [
32 {
33 "message": "expected type `fn(&mut ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&ty::Ref<M>>`\n found type `fn(&ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&'list ty::Ref<M>>`",
34 "code": null,
35 "level": "note",
36 "spans": [],
37 "children": [],
38 "rendered": null
39 }
40 ],
41 "rendered": "error[E0053]: method `next` has an incompatible type for trait\n --> compiler/ty/list_iter.rs:52:5\n |\n52 | fn next(&self) -> Option<&'list ty::Ref<M>> {\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ types differ in mutability\n |\n = note: expected type `fn(&mut ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&ty::Ref<M>>`\n found type `fn(&ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&'list ty::Ref<M>>`\n\n"
42}
diff --git a/editors/code/src/test/fixtures/rust-diagnostics/error/E0061.json b/editors/code/src/test/fixtures/rust-diagnostics/error/E0061.json
new file mode 100644
index 000000000..3154d1098
--- /dev/null
+++ b/editors/code/src/test/fixtures/rust-diagnostics/error/E0061.json
@@ -0,0 +1,114 @@
1{
2 "message": "this function takes 2 parameters but 3 parameters were supplied",
3 "code": {
4 "code": "E0061",
5 "explanation": "\nThe number of arguments passed to a function must match the number of arguments\nspecified in the function signature.\n\nFor example, a function like:\n\n```\nfn f(a: u16, b: &str) {}\n```\n\nMust always be called with exactly two arguments, e.g., `f(2, \"test\")`.\n\nNote that Rust does not have a notion of optional function arguments or\nvariadic functions (except for its C-FFI).\n"
6 },
7 "level": "error",
8 "spans": [
9 {
10 "file_name": "compiler/ty/select.rs",
11 "byte_start": 8787,
12 "byte_end": 9241,
13 "line_start": 219,
14 "line_end": 231,
15 "column_start": 5,
16 "column_end": 6,
17 "is_primary": false,
18 "text": [
19 {
20 "text": " pub fn add_evidence(",
21 "highlight_start": 5,
22 "highlight_end": 25
23 },
24 {
25 "text": " &mut self,",
26 "highlight_start": 1,
27 "highlight_end": 19
28 },
29 {
30 "text": " target_poly: &ty::Ref<ty::Poly>,",
31 "highlight_start": 1,
32 "highlight_end": 41
33 },
34 {
35 "text": " evidence_poly: &ty::Ref<ty::Poly>,",
36 "highlight_start": 1,
37 "highlight_end": 43
38 },
39 {
40 "text": " ) {",
41 "highlight_start": 1,
42 "highlight_end": 8
43 },
44 {
45 "text": " match target_poly {",
46 "highlight_start": 1,
47 "highlight_end": 28
48 },
49 {
50 "text": " ty::Ref::Var(tvar, _) => self.add_var_evidence(tvar, evidence_poly),",
51 "highlight_start": 1,
52 "highlight_end": 81
53 },
54 {
55 "text": " ty::Ref::Fixed(target_ty) => {",
56 "highlight_start": 1,
57 "highlight_end": 43
58 },
59 {
60 "text": " let evidence_ty = evidence_poly.resolve_to_ty();",
61 "highlight_start": 1,
62 "highlight_end": 65
63 },
64 {
65 "text": " self.add_evidence_ty(target_ty, evidence_poly, evidence_ty)",
66 "highlight_start": 1,
67 "highlight_end": 76
68 },
69 {
70 "text": " }",
71 "highlight_start": 1,
72 "highlight_end": 14
73 },
74 {
75 "text": " }",
76 "highlight_start": 1,
77 "highlight_end": 10
78 },
79 {
80 "text": " }",
81 "highlight_start": 1,
82 "highlight_end": 6
83 }
84 ],
85 "label": "defined here",
86 "suggested_replacement": null,
87 "suggestion_applicability": null,
88 "expansion": null
89 },
90 {
91 "file_name": "compiler/ty/select.rs",
92 "byte_start": 4045,
93 "byte_end": 4057,
94 "line_start": 104,
95 "line_end": 104,
96 "column_start": 18,
97 "column_end": 30,
98 "is_primary": true,
99 "text": [
100 {
101 "text": " self.add_evidence(target_fixed, evidence_fixed, false);",
102 "highlight_start": 18,
103 "highlight_end": 30
104 }
105 ],
106 "label": "expected 2 parameters",
107 "suggested_replacement": null,
108 "suggestion_applicability": null,
109 "expansion": null
110 }
111 ],
112 "children": [],
113 "rendered": "error[E0061]: this function takes 2 parameters but 3 parameters were supplied\n --> compiler/ty/select.rs:104:18\n |\n104 | self.add_evidence(target_fixed, evidence_fixed, false);\n | ^^^^^^^^^^^^ expected 2 parameters\n...\n219 | / pub fn add_evidence(\n220 | | &mut self,\n221 | | target_poly: &ty::Ref<ty::Poly>,\n222 | | evidence_poly: &ty::Ref<ty::Poly>,\n... |\n230 | | }\n231 | | }\n | |_____- defined here\n\n"
114}
diff --git a/editors/code/src/test/fixtures/rust-diagnostics/warning/unused_variables.json b/editors/code/src/test/fixtures/rust-diagnostics/warning/unused_variables.json
new file mode 100644
index 000000000..d1e2be722
--- /dev/null
+++ b/editors/code/src/test/fixtures/rust-diagnostics/warning/unused_variables.json
@@ -0,0 +1,72 @@
1{
2 "message": "unused variable: `foo`",
3 "code": {
4 "code": "unused_variables",
5 "explanation": null
6 },
7 "level": "warning",
8 "spans": [
9 {
10 "file_name": "driver/subcommand/repl.rs",
11 "byte_start": 9228,
12 "byte_end": 9231,
13 "line_start": 291,
14 "line_end": 291,
15 "column_start": 9,
16 "column_end": 12,
17 "is_primary": true,
18 "text": [
19 {
20 "text": " let foo = 42;",
21 "highlight_start": 9,
22 "highlight_end": 12
23 }
24 ],
25 "label": null,
26 "suggested_replacement": null,
27 "suggestion_applicability": null,
28 "expansion": null
29 }
30 ],
31 "children": [
32 {
33 "message": "#[warn(unused_variables)] on by default",
34 "code": null,
35 "level": "note",
36 "spans": [],
37 "children": [],
38 "rendered": null
39 },
40 {
41 "message": "consider prefixing with an underscore",
42 "code": null,
43 "level": "help",
44 "spans": [
45 {
46 "file_name": "driver/subcommand/repl.rs",
47 "byte_start": 9228,
48 "byte_end": 9231,
49 "line_start": 291,
50 "line_end": 291,
51 "column_start": 9,
52 "column_end": 12,
53 "is_primary": true,
54 "text": [
55 {
56 "text": " let foo = 42;",
57 "highlight_start": 9,
58 "highlight_end": 12
59 }
60 ],
61 "label": null,
62 "suggested_replacement": "_foo",
63 "suggestion_applicability": "MachineApplicable",
64 "expansion": null
65 }
66 ],
67 "children": [],
68 "rendered": null
69 }
70 ],
71 "rendered": "warning: unused variable: `foo`\n --> driver/subcommand/repl.rs:291:9\n |\n291 | let foo = 42;\n | ^^^ help: consider prefixing with an underscore: `_foo`\n |\n = note: #[warn(unused_variables)] on by default\n\n"
72}
diff --git a/editors/code/src/test/index.ts b/editors/code/src/test/index.ts
new file mode 100644
index 000000000..6e565c254
--- /dev/null
+++ b/editors/code/src/test/index.ts
@@ -0,0 +1,22 @@
1//
2// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING
3//
4// This file is providing the test runner to use when running extension tests.
5// By default the test runner in use is Mocha based.
6//
7// You can provide your own test runner if you want to override it by exporting
8// a function run(testRoot: string, clb: (error:Error) => void) that the extension
9// host can call to run the tests. The test runner is expected to use console.log
10// to report the results back to the caller. When the tests are finished, return
11// a possible error to the callback or null if none.
12
13import * as testRunner from 'vscode/lib/testrunner';
14
15// You can directly control Mocha options by uncommenting the following lines
16// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info
17testRunner.configure({
18 ui: 'bdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.)
19 useColors: true // colored output from test results
20});
21
22module.exports = testRunner;
diff --git a/editors/code/src/test/rust_diagnostics.test.ts b/editors/code/src/test/rust_diagnostics.test.ts
new file mode 100644
index 000000000..5eb064b97
--- /dev/null
+++ b/editors/code/src/test/rust_diagnostics.test.ts
@@ -0,0 +1,161 @@
1import * as assert from 'assert';
2import * as fs from 'fs';
3import * as vscode from 'vscode';
4
5import {
6 MappedRustDiagnostic,
7 mapRustDiagnosticToVsCode,
8 RustDiagnostic
9} from '../utils/rust_diagnostics';
10
11function loadDiagnosticFixture(name: string): RustDiagnostic {
12 const jsonText = fs
13 .readFileSync(
14 // We're actually in our JavaScript output directory, climb out
15 `${__dirname}/../../src/test/fixtures/rust-diagnostics/${name}.json`
16 )
17 .toString();
18
19 return JSON.parse(jsonText);
20}
21
22function mapFixtureToVsCode(name: string): MappedRustDiagnostic {
23 const rd = loadDiagnosticFixture(name);
24 const mapResult = mapRustDiagnosticToVsCode(rd);
25
26 if (!mapResult) {
27 return assert.fail('Mapping unexpectedly failed');
28 }
29 return mapResult;
30}
31
32describe('mapRustDiagnosticToVsCode', () => {
33 it('should map an incompatible type for trait error', () => {
34 const { diagnostic, codeActions } = mapFixtureToVsCode('error/E0053');
35
36 assert.strictEqual(
37 diagnostic.severity,
38 vscode.DiagnosticSeverity.Error
39 );
40 assert.strictEqual(
41 diagnostic.message,
42 [
43 `method \`next\` has an incompatible type for trait`,
44 `expected type \`fn(&mut ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&ty::Ref<M>>\``,
45 ` found type \`fn(&ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&'list ty::Ref<M>>\``
46 ].join('\n')
47 );
48 assert.strictEqual(diagnostic.code, 'E0053');
49 assert.strictEqual(diagnostic.tags, undefined);
50
51 // No related information
52 assert.deepStrictEqual(diagnostic.relatedInformation, []);
53
54 // There are no code actions available
55 assert.strictEqual(codeActions.length, 0);
56 });
57
58 it('should map an unused variable warning', () => {
59 const { diagnostic, codeActions } = mapFixtureToVsCode(
60 'warning/unused_variables'
61 );
62
63 assert.strictEqual(
64 diagnostic.severity,
65 vscode.DiagnosticSeverity.Warning
66 );
67 assert.strictEqual(
68 diagnostic.message,
69 [
70 'unused variable: `foo`',
71 '#[warn(unused_variables)] on by default'
72 ].join('\n')
73 );
74 assert.strictEqual(diagnostic.code, 'unused_variables');
75 assert.deepStrictEqual(diagnostic.tags, [
76 vscode.DiagnosticTag.Unnecessary
77 ]);
78
79 // No related information
80 assert.deepStrictEqual(diagnostic.relatedInformation, []);
81
82 // One code action available to prefix the variable
83 assert.strictEqual(codeActions.length, 1);
84 const [codeAction] = codeActions;
85 assert.strictEqual(
86 codeAction.title,
87 'consider prefixing with an underscore: `_foo`'
88 );
89 assert(codeAction.isPreferred);
90 });
91
92 it('should map a wrong number of parameters error', () => {
93 const { diagnostic, codeActions } = mapFixtureToVsCode('error/E0061');
94
95 assert.strictEqual(
96 diagnostic.severity,
97 vscode.DiagnosticSeverity.Error
98 );
99 assert.strictEqual(
100 diagnostic.message,
101 'this function takes 2 parameters but 3 parameters were supplied'
102 );
103 assert.strictEqual(diagnostic.code, 'E0061');
104 assert.strictEqual(diagnostic.tags, undefined);
105
106 // One related information for the original definition
107 const relatedInformation = diagnostic.relatedInformation;
108 if (!relatedInformation) {
109 return assert.fail('Related information unexpectedly undefined');
110 }
111 assert.strictEqual(relatedInformation.length, 1);
112 const [related] = relatedInformation;
113 assert.strictEqual(related.message, 'defined here');
114
115 // There are no actions available
116 assert.strictEqual(codeActions.length, 0);
117 });
118
119 it('should map a Clippy copy pass by ref warning', () => {
120 const { diagnostic, codeActions } = mapFixtureToVsCode(
121 'clippy/trivially_copy_pass_by_ref'
122 );
123
124 assert.strictEqual(
125 diagnostic.severity,
126 vscode.DiagnosticSeverity.Warning
127 );
128 assert.strictEqual(
129 diagnostic.message,
130 [
131 'this argument is passed by reference, but would be more efficient if passed by value',
132 '#[warn(clippy::trivially_copy_pass_by_ref)] implied by #[warn(clippy::all)]',
133 'for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#trivially_copy_pass_by_ref'
134 ].join('\n')
135 );
136 assert.strictEqual(
137 diagnostic.code,
138 'clippy::trivially_copy_pass_by_ref'
139 );
140 assert.strictEqual(diagnostic.tags, undefined);
141
142 // One related information for the lint definition
143 const relatedInformation = diagnostic.relatedInformation;
144 if (!relatedInformation) {
145 return assert.fail('Related information unexpectedly undefined');
146 }
147 assert.strictEqual(relatedInformation.length, 1);
148 const [related] = relatedInformation;
149 assert.strictEqual(related.message, 'lint level defined here');
150
151 // One code action available to pass by value
152 assert.strictEqual(codeActions.length, 1);
153 const [codeAction] = codeActions;
154 assert.strictEqual(
155 codeAction.title,
156 'consider passing by value instead: `self`'
157 );
158 // Clippy does not mark this as machine applicable
159 assert.strictEqual(codeAction.isPreferred, false);
160 });
161});
diff --git a/editors/code/src/test/vscode_diagnostics.test.ts b/editors/code/src/test/vscode_diagnostics.test.ts
new file mode 100644
index 000000000..ca4345626
--- /dev/null
+++ b/editors/code/src/test/vscode_diagnostics.test.ts
@@ -0,0 +1,164 @@
1import * as assert from 'assert';
2import * as vscode from 'vscode';
3
4import {
5 areCodeActionsEqual,
6 areDiagnosticsEqual
7} from '../utils/vscode_diagnostics';
8
9const uri = vscode.Uri.file('/file/1');
10
11const range1 = new vscode.Range(
12 new vscode.Position(1, 2),
13 new vscode.Position(3, 4)
14);
15
16const range2 = new vscode.Range(
17 new vscode.Position(5, 6),
18 new vscode.Position(7, 8)
19);
20
21describe('areDiagnosticsEqual', () => {
22 it('should treat identical diagnostics as equal', () => {
23 const diagnostic1 = new vscode.Diagnostic(
24 range1,
25 'Hello, world!',
26 vscode.DiagnosticSeverity.Error
27 );
28
29 const diagnostic2 = new vscode.Diagnostic(
30 range1,
31 'Hello, world!',
32 vscode.DiagnosticSeverity.Error
33 );
34
35 assert(areDiagnosticsEqual(diagnostic1, diagnostic2));
36 });
37
38 it('should treat diagnostics with different ranges as inequal', () => {
39 const diagnostic1 = new vscode.Diagnostic(
40 range1,
41 'Hello, world!',
42 vscode.DiagnosticSeverity.Error
43 );
44
45 const diagnostic2 = new vscode.Diagnostic(
46 range2,
47 'Hello, world!',
48 vscode.DiagnosticSeverity.Error
49 );
50
51 assert(!areDiagnosticsEqual(diagnostic1, diagnostic2));
52 });
53
54 it('should treat diagnostics with different messages as inequal', () => {
55 const diagnostic1 = new vscode.Diagnostic(
56 range1,
57 'Hello, world!',
58 vscode.DiagnosticSeverity.Error
59 );
60
61 const diagnostic2 = new vscode.Diagnostic(
62 range1,
63 'Goodbye!, world!',
64 vscode.DiagnosticSeverity.Error
65 );
66
67 assert(!areDiagnosticsEqual(diagnostic1, diagnostic2));
68 });
69
70 it('should treat diagnostics with different severities as inequal', () => {
71 const diagnostic1 = new vscode.Diagnostic(
72 range1,
73 'Hello, world!',
74 vscode.DiagnosticSeverity.Warning
75 );
76
77 const diagnostic2 = new vscode.Diagnostic(
78 range1,
79 'Hello, world!',
80 vscode.DiagnosticSeverity.Error
81 );
82
83 assert(!areDiagnosticsEqual(diagnostic1, diagnostic2));
84 });
85});
86
87describe('areCodeActionsEqual', () => {
88 it('should treat identical actions as equal', () => {
89 const codeAction1 = new vscode.CodeAction(
90 'Fix me!',
91 vscode.CodeActionKind.QuickFix
92 );
93
94 const codeAction2 = new vscode.CodeAction(
95 'Fix me!',
96 vscode.CodeActionKind.QuickFix
97 );
98
99 const edit = new vscode.WorkspaceEdit();
100 edit.replace(uri, range1, 'Replace with this');
101 codeAction1.edit = edit;
102 codeAction2.edit = edit;
103
104 assert(areCodeActionsEqual(codeAction1, codeAction2));
105 });
106
107 it('should treat actions with different types as inequal', () => {
108 const codeAction1 = new vscode.CodeAction(
109 'Fix me!',
110 vscode.CodeActionKind.Refactor
111 );
112
113 const codeAction2 = new vscode.CodeAction(
114 'Fix me!',
115 vscode.CodeActionKind.QuickFix
116 );
117
118 const edit = new vscode.WorkspaceEdit();
119 edit.replace(uri, range1, 'Replace with this');
120 codeAction1.edit = edit;
121 codeAction2.edit = edit;
122
123 assert(!areCodeActionsEqual(codeAction1, codeAction2));
124 });
125
126 it('should treat actions with different titles as inequal', () => {
127 const codeAction1 = new vscode.CodeAction(
128 'Fix me!',
129 vscode.CodeActionKind.Refactor
130 );
131
132 const codeAction2 = new vscode.CodeAction(
133 'Do something different!',
134 vscode.CodeActionKind.Refactor
135 );
136
137 const edit = new vscode.WorkspaceEdit();
138 edit.replace(uri, range1, 'Replace with this');
139 codeAction1.edit = edit;
140 codeAction2.edit = edit;
141
142 assert(!areCodeActionsEqual(codeAction1, codeAction2));
143 });
144
145 it('should treat actions with different edits as inequal', () => {
146 const codeAction1 = new vscode.CodeAction(
147 'Fix me!',
148 vscode.CodeActionKind.Refactor
149 );
150 const edit1 = new vscode.WorkspaceEdit();
151 edit1.replace(uri, range1, 'Replace with this');
152 codeAction1.edit = edit1;
153
154 const codeAction2 = new vscode.CodeAction(
155 'Fix me!',
156 vscode.CodeActionKind.Refactor
157 );
158 const edit2 = new vscode.WorkspaceEdit();
159 edit2.replace(uri, range1, 'Replace with this other thing');
160 codeAction2.edit = edit2;
161
162 assert(!areCodeActionsEqual(codeAction1, codeAction2));
163 });
164});
diff --git a/editors/code/src/utils/vscode_diagnostics.ts b/editors/code/src/utils/vscode_diagnostics.ts
new file mode 100644
index 000000000..9d763c8d6
--- /dev/null
+++ b/editors/code/src/utils/vscode_diagnostics.ts
@@ -0,0 +1,73 @@
1import * as vscode from 'vscode';
2
3/** Compares two `vscode.Diagnostic`s for equality */
4export function areDiagnosticsEqual(
5 left: vscode.Diagnostic,
6 right: vscode.Diagnostic
7): boolean {
8 return (
9 left.source === right.source &&
10 left.severity === right.severity &&
11 left.range.isEqual(right.range) &&
12 left.message === right.message
13 );
14}
15
16/** Compares two `vscode.TextEdit`s for equality */
17function areTextEditsEqual(
18 left: vscode.TextEdit,
19 right: vscode.TextEdit
20): boolean {
21 if (!left.range.isEqual(right.range)) {
22 return false;
23 }
24
25 if (left.newText !== right.newText) {
26 return false;
27 }
28
29 return true;
30}
31
32/** Compares two `vscode.CodeAction`s for equality */
33export function areCodeActionsEqual(
34 left: vscode.CodeAction,
35 right: vscode.CodeAction
36): boolean {
37 if (
38 left.kind !== right.kind ||
39 left.title !== right.title ||
40 !left.edit ||
41 !right.edit
42 ) {
43 return false;
44 }
45
46 const leftEditEntries = left.edit.entries();
47 const rightEditEntries = right.edit.entries();
48
49 if (leftEditEntries.length !== rightEditEntries.length) {
50 return false;
51 }
52
53 for (let i = 0; i < leftEditEntries.length; i++) {
54 const [leftUri, leftEdits] = leftEditEntries[i];
55 const [rightUri, rightEdits] = rightEditEntries[i];
56
57 if (leftUri.toString() !== rightUri.toString()) {
58 return false;
59 }
60
61 if (leftEdits.length !== rightEdits.length) {
62 return false;
63 }
64
65 for (let j = 0; j < leftEdits.length; j++) {
66 if (!areTextEditsEqual(leftEdits[j], rightEdits[j])) {
67 return false;
68 }
69 }
70 }
71
72 return true;
73}