Repository: rust-analyzer/rust-analyzer
Pull request #1446 (2019-06-26 10:33:40)

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](https://github.com/etaoins/arret) 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.


Patch:
diff --git a/docs/dev/README.md b/docs/dev/README.md
index 3dc37e86eb..0a148ed323 100644
--- a/docs/dev/README.md
+++ b/docs/dev/README.md
@@ -97,6 +97,25 @@ To work on the VS Code extension, launch code inside `editors/code` and use `F5`
 to launch/debug. To automatically apply formatter and linter suggestions, use
 `npm run fix`.
 
+Tests are located inside `src/test` and are named `*.test.ts`. They use the
+[Mocha](https://mochajs.org) test framework and the builtin Node
+[assert](https://nodejs.org/api/assert.html) module. Unlike normal Node tests
+they must be hosted inside a VS Code instance. This can be done in one of two
+ways:
+
+1. When `F5` debugging in VS Code select the `Extension Tests` configuration
+   from the drop-down at the top of the Debug View. This will launch a temporary
+   instance of VS Code. The test results will appear in the "Debug Console" tab
+   of the primary VS Code instance.
+
+2. Run `npm test` from the command line. Although this is initiated from the
+   command line it is not headless; it will also launch a temporary instance of
+   VS Code. 
+
+Due to the requirements of running the tests inside VS Code they are **not run
+on CI**. When making changes to the extension please ensure the tests are not
+broken locally before opening a Pull Request.
+
 # Logging
 
 Logging is done by both rust-analyzer and VS Code, so it might be tricky to
diff --git a/editors/code/.vscode/launch.json b/editors/code/.vscode/launch.json
index b9d14dddd1..c3578f476f 100644
--- a/editors/code/.vscode/launch.json
+++ b/editors/code/.vscode/launch.json
@@ -20,6 +20,7 @@
             "request": "launch",
             "runtimeExecutable": "${execPath}",
             "args": [
+                "${workspaceFolder}/src/test/",
                 "--extensionDevelopmentPath=${workspaceFolder}",
                 "--extensionTestsPath=${workspaceFolder}/out/test"
             ],
diff --git a/editors/code/package.json b/editors/code/package.json
index ac2ba82e3e..6e2dd04944 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -23,6 +23,7 @@
         "postinstall": "node ./node_modules/vscode/bin/install",
         "fix": "prettier **/*.{json,ts} --write && tslint --project . --fix",
         "lint": "tslint --project .",
+        "test": "node node_modules/vscode/bin/test",
         "prettier": "prettier **/*.{json,ts}",
         "travis": "npm run compile && npm run lint && npm run prettier -- --list-different"
     },
diff --git a/editors/code/src/commands/cargo_watch.ts b/editors/code/src/commands/cargo_watch.ts
index 126a8b1b3b..1ec5f8d5f0 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';
 import * as fs from 'fs';
 import * as path from 'path';
 import * as vscode from 'vscode';
+
 import { Server } from '../server';
 import { terminate } from '../utils/processes';
 import {
     mapRustDiagnosticToVsCode,
     RustDiagnostic
 } from '../utils/rust_diagnostics';
+import {
+    areCodeActionsEqual,
+    areDiagnosticsEqual
+} from '../utils/vscode_diagnostics';
 import { LineBuffer } from './line_buffer';
 import { StatusDisplay } from './watch_status';
 
@@ -184,67 +189,6 @@ export class CargoWatchProvider
             this.statusDisplay.hide();
         }
 
-        function areDiagnosticsEqual(
-            left: vscode.Diagnostic,
-            right: vscode.Diagnostic
-        ): boolean {
-            return (
-                left.source === right.source &&
-                left.severity === right.severity &&
-                left.range.isEqual(right.range) &&
-                left.message === right.message
-            );
-        }
-
-        function areCodeActionsEqual(
-            left: vscode.CodeAction,
-            right: vscode.CodeAction
-        ): boolean {
-            if (
-                left.kind !== right.kind ||
-                left.title !== right.title ||
-                !left.edit ||
-                !right.edit
-            ) {
-                return false;
-            }
-
-            const leftEditEntries = left.edit.entries();
-            const rightEditEntries = right.edit.entries();
-
-            if (leftEditEntries.length !== rightEditEntries.length) {
-                return false;
-            }
-
-            for (let i = 0; i < leftEditEntries.length; i++) {
-                const [leftUri, leftEdits] = leftEditEntries[i];
-                const [rightUri, rightEdits] = rightEditEntries[i];
-
-                if (leftUri.toString() !== rightUri.toString()) {
-                    return false;
-                }
-
-                if (leftEdits.length !== rightEdits.length) {
-                    return false;
-                }
-
-                for (let j = 0; j < leftEdits.length; j++) {
-                    const leftEdit = leftEdits[j];
-                    const rightEdit = rightEdits[j];
-
-                    if (!leftEdit.range.isEqual(rightEdit.range)) {
-                        return false;
-                    }
-
-                    if (leftEdit.newText !== rightEdit.newText) {
-                        return false;
-                    }
-                }
-            }
-
-            return true;
-        }
-
         interface CargoArtifact {
             reason: string;
             package_id: string;
diff --git a/editors/code/src/utils/vscode_diagnostics.ts b/editors/code/src/utils/vscode_diagnostics.ts
new file mode 100644
index 0000000000..9d763c8d64
--- /dev/null
+++ b/editors/code/src/utils/vscode_diagnostics.ts
@@ -0,0 +1,73 @@
+import * as vscode from 'vscode';
+
+/** Compares two `vscode.Diagnostic`s for equality */
+export function areDiagnosticsEqual(
+    left: vscode.Diagnostic,
+    right: vscode.Diagnostic
+): boolean {
+    return (
+        left.source === right.source &&
+        left.severity === right.severity &&
+        left.range.isEqual(right.range) &&
+        left.message === right.message
+    );
+}
+
+/** Compares two `vscode.TextEdit`s for equality */
+function areTextEditsEqual(
+    left: vscode.TextEdit,
+    right: vscode.TextEdit
+): boolean {
+    if (!left.range.isEqual(right.range)) {
+        return false;
+    }
+
+    if (left.newText !== right.newText) {
+        return false;
+    }
+
+    return true;
+}
+
+/** Compares two `vscode.CodeAction`s for equality */
+export function areCodeActionsEqual(
+    left: vscode.CodeAction,
+    right: vscode.CodeAction
+): boolean {
+    if (
+        left.kind !== right.kind ||
+        left.title !== right.title ||
+        !left.edit ||
+        !right.edit
+    ) {
+        return false;
+    }
+
+    const leftEditEntries = left.edit.entries();
+    const rightEditEntries = right.edit.entries();
+
+    if (leftEditEntries.length !== rightEditEntries.length) {
+        return false;
+    }
+
+    for (let i = 0; i < leftEditEntries.length; i++) {
+        const [leftUri, leftEdits] = leftEditEntries[i];
+        const [rightUri, rightEdits] = rightEditEntries[i];
+
+        if (leftUri.toString() !== rightUri.toString()) {
+            return false;
+        }
+
+        if (leftEdits.length !== rightEdits.length) {
+            return false;
+        }
+
+        for (let j = 0; j < leftEdits.length; j++) {
+            if (!areTextEditsEqual(leftEdits[j], rightEdits[j])) {
+                return false;
+            }
+        }
+    }
+
+    return true;
+}


Test patch:
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 0000000000..d874e99bc5
--- /dev/null
+++ b/editors/code/src/test/fixtures/rust-diagnostics/clippy/trivially_copy_pass_by_ref.json
@@ -0,0 +1,110 @@
+{
+    "message": "this argument is passed by reference, but would be more efficient if passed by value",
+    "code": {
+        "code": "clippy::trivially_copy_pass_by_ref",
+        "explanation": null
+    },
+    "level": "warning",
+    "spans": [
+        {
+            "file_name": "compiler/mir/tagset.rs",
+            "byte_start": 941,
+            "byte_end": 946,
+            "line_start": 42,
+            "line_end": 42,
+            "column_start": 24,
+            "column_end": 29,
+            "is_primary": true,
+            "text": [
+                {
+                    "text": "    pub fn is_disjoint(&self, other: Self) -> bool {",
+                    "highlight_start": 24,
+                    "highlight_end": 29
+                }
+            ],
+            "label": null,
+            "suggested_replacement": null,
+            "suggestion_applicability": null,
+            "expansion": null
+        }
+    ],
+    "children": [
+        {
+            "message": "lint level defined here",
+            "code": null,
+            "level": "note",
+            "spans": [
+                {
+                    "file_name": "compiler/lib.rs",
+                    "byte_start": 8,
+                    "byte_end": 19,
+                    "line_start": 1,
+                    "line_end": 1,
+                    "column_start": 9,
+                    "column_end": 20,
+                    "is_primary": true,
+                    "text": [
+                        {
+                            "text": "#![warn(clippy::all)]",
+                            "highlight_start": 9,
+                            "highlight_end": 20
+                        }
+                    ],
+                    "label": null,
+                    "suggested_replacement": null,
+                    "suggestion_applicability": null,
+                    "expansion": null
+                }
+            ],
+            "children": [],
+            "rendered": null
+        },
+        {
+            "message": "#[warn(clippy::trivially_copy_pass_by_ref)] implied by #[warn(clippy::all)]",
+            "code": null,
+            "level": "note",
+            "spans": [],
+            "children": [],
+            "rendered": null
+        },
+        {
+            "message": "for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#trivially_copy_pass_by_ref",
+            "code": null,
+            "level": "help",
+            "spans": [],
+            "children": [],
+            "rendered": null
+        },
+        {
+            "message": "consider passing by value instead",
+            "code": null,
+            "level": "help",
+            "spans": [
+                {
+                    "file_name": "compiler/mir/tagset.rs",
+                    "byte_start": 941,
+                    "byte_end": 946,
+                    "line_start": 42,
+                    "line_end": 42,
+                    "column_start": 24,
+                    "column_end": 29,
+                    "is_primary": true,
+                    "text": [
+                        {
+                            "text": "    pub fn is_disjoint(&self, other: Self) -> bool {",
+                            "highlight_start": 24,
+                            "highlight_end": 29
+                        }
+                    ],
+                    "label": null,
+                    "suggested_replacement": "self",
+                    "suggestion_applicability": "Unspecified",
+                    "expansion": null
+                }
+            ],
+            "children": [],
+            "rendered": null
+        }
+    ],
+    "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"
+}
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 0000000000..ea5c976d1d
--- /dev/null
+++ b/editors/code/src/test/fixtures/rust-diagnostics/error/E0053.json
@@ -0,0 +1,42 @@
+{
+    "message": "method `next` has an incompatible type for trait",
+    "code": {
+        "code": "E0053",
+        "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"
+    },
+    "level": "error",
+    "spans": [
+        {
+            "file_name": "compiler/ty/list_iter.rs",
+            "byte_start": 1307,
+            "byte_end": 1350,
+            "line_start": 52,
+            "line_end": 52,
+            "column_start": 5,
+            "column_end": 48,
+            "is_primary": true,
+            "text": [
+                {
+                    "text": "    fn next(&self) -> Option<&'list ty::Ref<M>> {",
+                    "highlight_start": 5,
+                    "highlight_end": 48
+                }
+            ],
+            "label": "types differ in mutability",
+            "suggested_replacement": null,
+            "suggestion_applicability": null,
+            "expansion": null
+        }
+    ],
+    "children": [
+        {
+            "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>>`",
+            "code": null,
+            "level": "note",
+            "spans": [],
+            "children": [],
+            "rendered": null
+        }
+    ],
+    "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"
+}
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 0000000000..3154d1098c
--- /dev/null
+++ b/editors/code/src/test/fixtures/rust-diagnostics/error/E0061.json
@@ -0,0 +1,114 @@
+{
+    "message": "this function takes 2 parameters but 3 parameters were supplied",
+    "code": {
+        "code": "E0061",
+        "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"
+    },
+    "level": "error",
+    "spans": [
+        {
+            "file_name": "compiler/ty/select.rs",
+            "byte_start": 8787,
+            "byte_end": 9241,
+            "line_start": 219,
+            "line_end": 231,
+            "column_start": 5,
+            "column_end": 6,
+            "is_primary": false,
+            "text": [
+                {
+                    "text": "    pub fn add_evidence(",
+                    "highlight_start": 5,
+                    "highlight_end": 25
+                },
+                {
+                    "text": "        &mut self,",
+                    "highlight_start": 1,
+                    "highlight_end": 19
+                },
+                {
+                    "text": "        target_poly: &ty::Ref<ty::Poly>,",
+                    "highlight_start": 1,
+                    "highlight_end": 41
+                },
+                {
+                    "text": "        evidence_poly: &ty::Ref<ty::Poly>,",
+                    "highlight_start": 1,
+                    "highlight_end": 43
+                },
+                {
+                    "text": "    ) {",
+                    "highlight_start": 1,
+                    "highlight_end": 8
+                },
+                {
+                    "text": "        match target_poly {",
+                    "highlight_start": 1,
+                    "highlight_end": 28
+                },
+                {
+                    "text": "            ty::Ref::Var(tvar, _) => self.add_var_evidence(tvar, evidence_poly),",
+                    "highlight_start": 1,
+                    "highlight_end": 81
+                },
+                {
+                    "text": "            ty::Ref::Fixed(target_ty) => {",
+                    "highlight_start": 1,
+                    "highlight_end": 43
+                },
+                {
+                    "text": "                let evidence_ty = evidence_poly.resolve_to_ty();",
+                    "highlight_start": 1,
+                    "highlight_end": 65
+                },
+                {
+                    "text": "                self.add_evidence_ty(target_ty, evidence_poly, evidence_ty)",
+                    "highlight_start": 1,
+                    "highlight_end": 76
+                },
+                {
+                    "text": "            }",
+                    "highlight_start": 1,
+                    "highlight_end": 14
+                },
+                {
+                    "text": "        }",
+                    "highlight_start": 1,
+                    "highlight_end": 10
+                },
+                {
+                    "text": "    }",
+                    "highlight_start": 1,
+                    "highlight_end": 6
+                }
+            ],
+            "label": "defined here",
+            "suggested_replacement": null,
+            "suggestion_applicability": null,
+            "expansion": null
+        },
+        {
+            "file_name": "compiler/ty/select.rs",
+            "byte_start": 4045,
+            "byte_end": 4057,
+            "line_start": 104,
+            "line_end": 104,
+            "column_start": 18,
+            "column_end": 30,
+            "is_primary": true,
+            "text": [
+                {
+                    "text": "            self.add_evidence(target_fixed, evidence_fixed, false);",
+                    "highlight_start": 18,
+                    "highlight_end": 30
+                }
+            ],
+            "label": "expected 2 parameters",
+            "suggested_replacement": null,
+            "suggestion_applicability": null,
+            "expansion": null
+        }
+    ],
+    "children": [],
+    "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"
+}
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 0000000000..d1e2be7221
--- /dev/null
+++ b/editors/code/src/test/fixtures/rust-diagnostics/warning/unused_variables.json
@@ -0,0 +1,72 @@
+{
+    "message": "unused variable: `foo`",
+    "code": {
+        "code": "unused_variables",
+        "explanation": null
+    },
+    "level": "warning",
+    "spans": [
+        {
+            "file_name": "driver/subcommand/repl.rs",
+            "byte_start": 9228,
+            "byte_end": 9231,
+            "line_start": 291,
+            "line_end": 291,
+            "column_start": 9,
+            "column_end": 12,
+            "is_primary": true,
+            "text": [
+                {
+                    "text": "    let foo = 42;",
+                    "highlight_start": 9,
+                    "highlight_end": 12
+                }
+            ],
+            "label": null,
+            "suggested_replacement": null,
+            "suggestion_applicability": null,
+            "expansion": null
+        }
+    ],
+    "children": [
+        {
+            "message": "#[warn(unused_variables)] on by default",
+            "code": null,
+            "level": "note",
+            "spans": [],
+            "children": [],
+            "rendered": null
+        },
+        {
+            "message": "consider prefixing with an underscore",
+            "code": null,
+            "level": "help",
+            "spans": [
+                {
+                    "file_name": "driver/subcommand/repl.rs",
+                    "byte_start": 9228,
+                    "byte_end": 9231,
+                    "line_start": 291,
+                    "line_end": 291,
+                    "column_start": 9,
+                    "column_end": 12,
+                    "is_primary": true,
+                    "text": [
+                        {
+                            "text": "    let foo = 42;",
+                            "highlight_start": 9,
+                            "highlight_end": 12
+                        }
+                    ],
+                    "label": null,
+                    "suggested_replacement": "_foo",
+                    "suggestion_applicability": "MachineApplicable",
+                    "expansion": null
+                }
+            ],
+            "children": [],
+            "rendered": null
+        }
+    ],
+    "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"
+}
diff --git a/editors/code/src/test/index.ts b/editors/code/src/test/index.ts
new file mode 100644
index 0000000000..6e565c254a
--- /dev/null
+++ b/editors/code/src/test/index.ts
@@ -0,0 +1,22 @@
+//
+// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING
+//
+// This file is providing the test runner to use when running extension tests.
+// By default the test runner in use is Mocha based.
+//
+// You can provide your own test runner if you want to override it by exporting
+// a function run(testRoot: string, clb: (error:Error) => void) that the extension
+// host can call to run the tests. The test runner is expected to use console.log
+// to report the results back to the caller. When the tests are finished, return
+// a possible error to the callback or null if none.
+
+import * as testRunner from 'vscode/lib/testrunner';
+
+// You can directly control Mocha options by uncommenting the following lines
+// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info
+testRunner.configure({
+    ui: 'bdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.)
+    useColors: true // colored output from test results
+});
+
+module.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 0000000000..5eb064b979
--- /dev/null
+++ b/editors/code/src/test/rust_diagnostics.test.ts
@@ -0,0 +1,161 @@
+import * as assert from 'assert';
+import * as fs from 'fs';
+import * as vscode from 'vscode';
+
+import {
+    MappedRustDiagnostic,
+    mapRustDiagnosticToVsCode,
+    RustDiagnostic
+} from '../utils/rust_diagnostics';
+
+function loadDiagnosticFixture(name: string): RustDiagnostic {
+    const jsonText = fs
+        .readFileSync(
+            // We're actually in our JavaScript output directory, climb out
+            `${__dirname}/../../src/test/fixtures/rust-diagnostics/${name}.json`
+        )
+        .toString();
+
+    return JSON.parse(jsonText);
+}
+
+function mapFixtureToVsCode(name: string): MappedRustDiagnostic {
+    const rd = loadDiagnosticFixture(name);
+    const mapResult = mapRustDiagnosticToVsCode(rd);
+
+    if (!mapResult) {
+        return assert.fail('Mapping unexpectedly failed');
+    }
+    return mapResult;
+}
+
+describe('mapRustDiagnosticToVsCode', () => {
+    it('should map an incompatible type for trait error', () => {
+        const { diagnostic, codeActions } = mapFixtureToVsCode('error/E0053');
+
+        assert.strictEqual(
+            diagnostic.severity,
+            vscode.DiagnosticSeverity.Error
+        );
+        assert.strictEqual(
+            diagnostic.message,
+            [
+                `method \`next\` has an incompatible type for trait`,
+                `expected type \`fn(&mut ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&ty::Ref<M>>\``,
+                `   found type \`fn(&ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&'list ty::Ref<M>>\``
+            ].join('\n')
+        );
+        assert.strictEqual(diagnostic.code, 'E0053');
+        assert.strictEqual(diagnostic.tags, undefined);
+
+        // No related information
+        assert.deepStrictEqual(diagnostic.relatedInformation, []);
+
+        // There are no code actions available
+        assert.strictEqual(codeActions.length, 0);
+    });
+
+    it('should map an unused variable warning', () => {
+        const { diagnostic, codeActions } = mapFixtureToVsCode(
+            'warning/unused_variables'
+        );
+
+        assert.strictEqual(
+            diagnostic.severity,
+            vscode.DiagnosticSeverity.Warning
+        );
+        assert.strictEqual(
+            diagnostic.message,
+            [
+                'unused variable: `foo`',
+                '#[warn(unused_variables)] on by default'
+            ].join('\n')
+        );
+        assert.strictEqual(diagnostic.code, 'unused_variables');
+        assert.deepStrictEqual(diagnostic.tags, [
+            vscode.DiagnosticTag.Unnecessary
+        ]);
+
+        // No related information
+        assert.deepStrictEqual(diagnostic.relatedInformation, []);
+
+        // One code action available to prefix the variable
+        assert.strictEqual(codeActions.length, 1);
+        const [codeAction] = codeActions;
+        assert.strictEqual(
+            codeAction.title,
+            'consider prefixing with an underscore: `_foo`'
+        );
+        assert(codeAction.isPreferred);
+    });
+
+    it('should map a wrong number of parameters error', () => {
+        const { diagnostic, codeActions } = mapFixtureToVsCode('error/E0061');
+
+        assert.strictEqual(
+            diagnostic.severity,
+            vscode.DiagnosticSeverity.Error
+        );
+        assert.strictEqual(
+            diagnostic.message,
+            'this function takes 2 parameters but 3 parameters were supplied'
+        );
+        assert.strictEqual(diagnostic.code, 'E0061');
+        assert.strictEqual(diagnostic.tags, undefined);
+
+        // One related information for the original definition
+        const relatedInformation = diagnostic.relatedInformation;
+        if (!relatedInformation) {
+            return assert.fail('Related information unexpectedly undefined');
+        }
+        assert.strictEqual(relatedInformation.length, 1);
+        const [related] = relatedInformation;
+        assert.strictEqual(related.message, 'defined here');
+
+        // There are no actions available
+        assert.strictEqual(codeActions.length, 0);
+    });
+
+    it('should map a Clippy copy pass by ref warning', () => {
+        const { diagnostic, codeActions } = mapFixtureToVsCode(
+            'clippy/trivially_copy_pass_by_ref'
+        );
+
+        assert.strictEqual(
+            diagnostic.severity,
+            vscode.DiagnosticSeverity.Warning
+        );
+        assert.strictEqual(
+            diagnostic.message,
+            [
+                'this argument is passed by reference, but would be more efficient if passed by value',
+                '#[warn(clippy::trivially_copy_pass_by_ref)] implied by #[warn(clippy::all)]',
+                'for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#trivially_copy_pass_by_ref'
+            ].join('\n')
+        );
+        assert.strictEqual(
+            diagnostic.code,
+            'clippy::trivially_copy_pass_by_ref'
+        );
+        assert.strictEqual(diagnostic.tags, undefined);
+
+        // One related information for the lint definition
+        const relatedInformation = diagnostic.relatedInformation;
+        if (!relatedInformation) {
+            return assert.fail('Related information unexpectedly undefined');
+        }
+        assert.strictEqual(relatedInformation.length, 1);
+        const [related] = relatedInformation;
+        assert.strictEqual(related.message, 'lint level defined here');
+
+        // One code action available to pass by value
+        assert.strictEqual(codeActions.length, 1);
+        const [codeAction] = codeActions;
+        assert.strictEqual(
+            codeAction.title,
+            'consider passing by value instead: `self`'
+        );
+        // Clippy does not mark this as machine applicable
+        assert.strictEqual(codeAction.isPreferred, false);
+    });
+});
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 0000000000..ca43456269
--- /dev/null
+++ b/editors/code/src/test/vscode_diagnostics.test.ts
@@ -0,0 +1,164 @@
+import * as assert from 'assert';
+import * as vscode from 'vscode';
+
+import {
+    areCodeActionsEqual,
+    areDiagnosticsEqual
+} from '../utils/vscode_diagnostics';
+
+const uri = vscode.Uri.file('/file/1');
+
+const range1 = new vscode.Range(
+    new vscode.Position(1, 2),
+    new vscode.Position(3, 4)
+);
+
+const range2 = new vscode.Range(
+    new vscode.Position(5, 6),
+    new vscode.Position(7, 8)
+);
+
+describe('areDiagnosticsEqual', () => {
+    it('should treat identical diagnostics as equal', () => {
+        const diagnostic1 = new vscode.Diagnostic(
+            range1,
+            'Hello, world!',
+            vscode.DiagnosticSeverity.Error
+        );
+
+        const diagnostic2 = new vscode.Diagnostic(
+            range1,
+            'Hello, world!',
+            vscode.DiagnosticSeverity.Error
+        );
+
+        assert(areDiagnosticsEqual(diagnostic1, diagnostic2));
+    });
+
+    it('should treat diagnostics with different ranges as inequal', () => {
+        const diagnostic1 = new vscode.Diagnostic(
+            range1,
+            'Hello, world!',
+            vscode.DiagnosticSeverity.Error
+        );
+
+        const diagnostic2 = new vscode.Diagnostic(
+            range2,
+            'Hello, world!',
+            vscode.DiagnosticSeverity.Error
+        );
+
+        assert(!areDiagnosticsEqual(diagnostic1, diagnostic2));
+    });
+
+    it('should treat diagnostics with different messages as inequal', () => {
+        const diagnostic1 = new vscode.Diagnostic(
+            range1,
+            'Hello, world!',
+            vscode.DiagnosticSeverity.Error
+        );
+
+        const diagnostic2 = new vscode.Diagnostic(
+            range1,
+            'Goodbye!, world!',
+            vscode.DiagnosticSeverity.Error
+        );
+
+        assert(!areDiagnosticsEqual(diagnostic1, diagnostic2));
+    });
+
+    it('should treat diagnostics with different severities as inequal', () => {
+        const diagnostic1 = new vscode.Diagnostic(
+            range1,
+            'Hello, world!',
+            vscode.DiagnosticSeverity.Warning
+        );
+
+        const diagnostic2 = new vscode.Diagnostic(
+            range1,
+            'Hello, world!',
+            vscode.DiagnosticSeverity.Error
+        );
+
+        assert(!areDiagnosticsEqual(diagnostic1, diagnostic2));
+    });
+});
+
+describe('areCodeActionsEqual', () => {
+    it('should treat identical actions as equal', () => {
+        const codeAction1 = new vscode.CodeAction(
+            'Fix me!',
+            vscode.CodeActionKind.QuickFix
+        );
+
+        const codeAction2 = new vscode.CodeAction(
+            'Fix me!',
+            vscode.CodeActionKind.QuickFix
+        );
+
+        const edit = new vscode.WorkspaceEdit();
+        edit.replace(uri, range1, 'Replace with this');
+        codeAction1.edit = edit;
+        codeAction2.edit = edit;
+
+        assert(areCodeActionsEqual(codeAction1, codeAction2));
+    });
+
+    it('should treat actions with different types as inequal', () => {
+        const codeAction1 = new vscode.CodeAction(
+            'Fix me!',
+            vscode.CodeActionKind.Refactor
+        );
+
+        const codeAction2 = new vscode.CodeAction(
+            'Fix me!',
+            vscode.CodeActionKind.QuickFix
+        );
+
+        const edit = new vscode.WorkspaceEdit();
+        edit.replace(uri, range1, 'Replace with this');
+        codeAction1.edit = edit;
+        codeAction2.edit = edit;
+
+        assert(!areCodeActionsEqual(codeAction1, codeAction2));
+    });
+
+    it('should treat actions with different titles as inequal', () => {
+        const codeAction1 = new vscode.CodeAction(
+            'Fix me!',
+            vscode.CodeActionKind.Refactor
+        );
+
+        const codeAction2 = new vscode.CodeAction(
+            'Do something different!',
+            vscode.CodeActionKind.Refactor
+        );
+
+        const edit = new vscode.WorkspaceEdit();
+        edit.replace(uri, range1, 'Replace with this');
+        codeAction1.edit = edit;
+        codeAction2.edit = edit;
+
+        assert(!areCodeActionsEqual(codeAction1, codeAction2));
+    });
+
+    it('should treat actions with different edits as inequal', () => {
+        const codeAction1 = new vscode.CodeAction(
+            'Fix me!',
+            vscode.CodeActionKind.Refactor
+        );
+        const edit1 = new vscode.WorkspaceEdit();
+        edit1.replace(uri, range1, 'Replace with this');
+        codeAction1.edit = edit1;
+
+        const codeAction2 = new vscode.CodeAction(
+            'Fix me!',
+            vscode.CodeActionKind.Refactor
+        );
+        const edit2 = new vscode.WorkspaceEdit();
+        edit2.replace(uri, range1, 'Replace with this other thing');
+        codeAction2.edit = edit2;
+
+        assert(!areCodeActionsEqual(codeAction1, codeAction2));
+    });
+});