Skip to content

Commit

Permalink
Add test results UI (#104)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcomorain authored and avli committed Jun 15, 2018
1 parent 4b091db commit 808ea90
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 8 deletions.
8 changes: 5 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
"out": false // set this to true to hide the "out" folder with the compiled JS files
"out": false, // set this to true to hide the "out" folder with the compiled JS files
"node_modules": true
},
"search.exclude": {
"out": true // set this to false to include "out" folder in search results
"out": true, // set this to false to include "out" folder in search results
"node_modules": true
},
"typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version
}
}
16 changes: 16 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@
"command": "clojureVSCode.evalAndShowResult",
"title": "Clojure: Eval and show the result"
},
{
"command": "clojureVSCode.testNamespace",
"title": "Clojure: Test the current namespace."
},
{
"command": "clojureVSCode.runAllTests",
"title": "Clojure: Run All Tests."
},
{
"command": "clojureVSCode.manuallyConnectToNRepl",
"title": "Clojure: Connect to a running nREPL"
Expand All @@ -89,6 +97,14 @@
"title": "Clojure: Format file or selection"
}
],
"views": {
"test": [
{
"id": "clojure",
"name": "Clojure"
}
]
},
"configuration": {
"type": "object",
"title": "Clojure extension configuration",
Expand Down
116 changes: 116 additions & 0 deletions src/clojureEval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as vscode from 'vscode';
import { cljConnection } from './cljConnection';
import { cljParser } from './cljParser';
import { nreplClient } from './nreplClient';
import { TestListener } from './testRunner';

export function clojureEval(outputChannel: vscode.OutputChannel): void {
evaluate(outputChannel, false);
Expand All @@ -12,6 +13,121 @@ export function clojureEvalAndShowResult(outputChannel: vscode.OutputChannel): v
evaluate(outputChannel, true);
}

type TestResults = {
summary: {
error: number
fail: number
ns: number
pass: number
test: number
var: number
}
'testing-ns': string
'gen-input': any[]
status?: string[]
results: {
[key: string]: { // Namespace
[key: string]: {
context: any
file?: string
index: number
line?: number
message?: string
ns: string
type: string
var: string
actual?: string
expected?: string
}[];
}
}
session: string
}

function runTests(outputChannel: vscode.OutputChannel, listener: TestListener, namespace?: string): void {
if (!cljConnection.isConnected()) {
vscode.window.showWarningMessage('You must be connected to an nREPL session to test a namespace.');
return;
}

const promise: Promise<TestResults[]> = nreplClient.runTests(namespace);

promise.then((responses) => {

console.log("Test result promise delivery");

responses.forEach(response => {

console.log(response);
console.log(response.results);

if (response.status && response.status.indexOf("unknown-op") != -1) {
outputChannel.appendLine("Failed to run tests: the cider.nrepl.middleware.test middleware in not loaded.");
return;
}

for (const ns in response.results) {

const namespace = response.results[ns];

outputChannel.appendLine("Results for " + ns)

for (const varName in namespace) {

// Each var being tested reports a list of statuses, one for each
// `is` assertion in the test. Here we just want to reduce this
// down to a single pass/fail.
const statuses = new Set(namespace[varName].map(r => r.type));
const passed = (statuses.size == 0) ||
((statuses.size == 1) && statuses.has('pass'));
listener.onTestResult(ns, varName, passed);

namespace[varName].forEach(r => {
if (r.type != 'pass') {
outputChannel.appendLine(r.type + " in (" + r.var + ") (" + r.file + ":" + r.line + ")");
if (typeof r.message === 'string') {
outputChannel.appendLine(r.message);
}
if (r.expected) {
outputChannel.append("expected: " + r.expected)
}
if (r.actual) {
outputChannel.append(" actual: " + r.actual)
}
}
});
}
}

if ('summary' in response) {
const failed = response.summary.fail + response.summary.error;
if (failed > 0) {
vscode.window.showErrorMessage(failed + " tests failed.")
} else {
vscode.window.showInformationMessage(response.summary.var + " tests passed")
}
}
});

}).catch((reason): void => {
const message: string = "" + reason;
outputChannel.append("Tests failed: ");
outputChannel.appendLine(message);
});
}

export function testNamespace(outputChannel: vscode.OutputChannel, listener: TestListener): void {
const editor = vscode.window.activeTextEditor;
const ns = cljParser.getNamespace(editor.document.getText()); // log ns and 'starting'
outputChannel.appendLine("Testing " + ns)
runTests(outputChannel, listener, ns);
}

export function runAllTests(outputChannel: vscode.OutputChannel, listener: TestListener): void {
outputChannel.appendLine("Testing all namespaces");
runTests(outputChannel, listener);
}

function evaluate(outputChannel: vscode.OutputChannel, showResults: boolean): void {
if (!cljConnection.isConnected()) {
vscode.window.showWarningMessage('You should connect to nREPL first to evaluate code.');
Expand Down
10 changes: 9 additions & 1 deletion src/clojureMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as vscode from 'vscode';

import { CLOJURE_MODE } from './clojureMode';
import { ClojureCompletionItemProvider } from './clojureSuggest';
import { clojureEval, clojureEvalAndShowResult } from './clojureEval';
import { clojureEval, clojureEvalAndShowResult, testNamespace, runAllTests } from './clojureEval';
import { ClojureDefinitionProvider } from './clojureDefinition';
import { ClojureLanguageConfiguration } from './clojureConfiguration';
import { ClojureHoverProvider } from './clojureHover';
Expand All @@ -12,6 +12,8 @@ import { nreplController } from './nreplController';
import { cljConnection } from './cljConnection';
import { formatFile, maybeActivateFormatOnSave } from './clojureFormat';

import { buildTestProvider } from './testRunner'

export function activate(context: vscode.ExtensionContext) {
cljConnection.setCljContext(context);
context.subscriptions.push(nreplController);
Expand All @@ -23,6 +25,8 @@ export function activate(context: vscode.ExtensionContext) {

maybeActivateFormatOnSave();

const testResultDataProvidier = buildTestProvider();

vscode.commands.registerCommand('clojureVSCode.manuallyConnectToNRepl', cljConnection.manuallyConnect);
vscode.commands.registerCommand('clojureVSCode.stopDisconnectNRepl', cljConnection.disconnect);
vscode.commands.registerCommand('clojureVSCode.startNRepl', cljConnection.startNRepl);
Expand All @@ -31,6 +35,10 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand('clojureVSCode.eval', () => clojureEval(evaluationResultChannel));
vscode.commands.registerCommand('clojureVSCode.evalAndShowResult', () => clojureEvalAndShowResult(evaluationResultChannel));

vscode.commands.registerCommand('clojureVSCode.testNamespace', () => testNamespace(evaluationResultChannel, testResultDataProvidier));
vscode.commands.registerCommand('clojureVSCode.runAllTests', () => runAllTests(evaluationResultChannel, testResultDataProvidier));
vscode.window.registerTreeDataProvider('clojure', testResultDataProvidier);

vscode.commands.registerTextEditorCommand('clojureVSCode.formatFile', formatFile);

context.subscriptions.push(vscode.languages.registerCompletionItemProvider(CLOJURE_MODE, new ClojureCompletionItemProvider(), '.', '/'));
Expand Down
28 changes: 24 additions & 4 deletions src/nreplClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ interface nREPLInfoMessage {
session?: string;
}

type TestMessage = {
op: "test" | "test-all" | "test-stacktrace" | "retest"
ns?: string,
'load?'?: any
}

interface nREPLEvalMessage {
op: string;
file: string;
Expand Down Expand Up @@ -68,10 +74,18 @@ const evaluateFile = (code: string, filepath: string, session?: string): Promise

const stacktrace = (session: string): Promise<any> => send({ op: 'stacktrace', session: session });

const clone = (session?: string): Promise<string> => {
return send({ op: 'clone', session: session }).then(respObjs => respObjs[0]['new-session']);
const runTests = function (namespace: string): Promise<any[]> {
const message: TestMessage = {
op: (namespace ? "test" : "test-all"),
ns: namespace,
'load?': 1
}
return send(message);
}


const clone = (session?: string): Promise<any[]> => send({ op: 'clone', session: session }).then(respObjs => respObjs[0]);

const test = (connectionInfo: CljConnectionInformation): Promise<any[]> => {
return send({ op: 'clone' }, connectionInfo)
.then(respObjs => respObjs[0])
Expand All @@ -87,15 +101,20 @@ const test = (connectionInfo: CljConnectionInformation): Promise<any[]> => {
const close = (session?: string): Promise<any[]> => send({ op: 'close', session: session });

const listSessions = (): Promise<[string]> => {
return send({op: 'ls-sessions'}).then(respObjs => {
return send({ op: 'ls-sessions' }).then(respObjs => {
const response = respObjs[0];
if (response.status[0] == "done") {
return Promise.resolve(response.sessions);
}
});
}

const send = (msg: nREPLCompleteMessage | nREPLInfoMessage | nREPLEvalMessage | nREPLStacktraceMessage | nREPLCloneMessage | nREPLCloseMessage | nREPLSingleEvalMessage, connection?: CljConnectionInformation): Promise<any[]> => {
type Message = TestMessage | nREPLCompleteMessage | nREPLInfoMessage | nREPLEvalMessage | nREPLStacktraceMessage | nREPLCloneMessage | nREPLCloseMessage | nREPLSingleEvalMessage;

const send = (msg: Message, connection?: CljConnectionInformation): Promise<any[]> => {

console.log("nREPL: Sending op", msg);

return new Promise<any[]>((resolve, reject) => {
connection = connection || cljConnection.getConnection();

Expand Down Expand Up @@ -151,6 +170,7 @@ export const nreplClient = {
stacktrace,
clone,
test,
runTests,
close,
listSessions
};
109 changes: 109 additions & 0 deletions src/testRunner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { TreeDataProvider, EventEmitter, Event, TreeItemCollapsibleState, TreeItem, ProviderResult } from 'vscode';

// A map from namespace => var => boolean
// true if the running the test was successful.
type Results = {
[key: string]: {
[key: string]: boolean
}
}

type NamespaceNode = {
type: 'ns'
ns: string
}

type VarNode = {
type: 'var'
varName: string
nsName: string
}

// The test result tree-view has 2 type of node:
// Namespace nodes and var nodes.
// The (root) contains NamespaceNodes, which have VarNodes as children.
type TestNode = NamespaceNode | VarNode

export interface TestListener {
onTestResult(ns: string, varName: string, success: boolean): void;
}

class ClojureTestDataProvider implements TreeDataProvider<TestNode>, TestListener {

onTestResult(ns: string, varName: string, success: boolean): void {

console.log(ns, varName, success);

// Make a copy of result with the new result assoc'ed in.
this.results = {
...this.results,
[ns]: {
...this.results[ns],
[varName]: success
}
}

this.testsChanged.fire(); // Trigger the UI to update.
}

private testsChanged: EventEmitter<TestNode> = new EventEmitter<TestNode>();
readonly onDidChangeTreeData: Event<TestNode | undefined | null> = this.testsChanged.event;

private results: Results = {}

getNamespaceItem(element: NamespaceNode): TreeItem {
return {
label: element.ns,
collapsibleState: TreeItemCollapsibleState.Expanded
};
}

getVarItem(element: VarNode): TreeItem {
const passed: boolean = this.results[element.nsName][element.varName];
return {
label: (passed ? "✅ " : "❌ ") + element.varName,
collapsibleState: TreeItemCollapsibleState.None
};
}

getTreeItem(element: TestNode): TreeItem | Thenable<TreeItem> {
switch (element.type) {
case 'ns': return this.getNamespaceItem(element);
case 'var': return this.getVarItem(element);
}
}

getChildren(element?: TestNode): ProviderResult<TestNode[]> {

if (!element) {
return Object.keys(this.results).map((ns) => {
const node: NamespaceNode = {
type: 'ns',
ns: ns
};
return node;
});

}

switch (element.type) {
case 'ns': {
const vars = Object.keys(this.results[element.ns]);

return vars.map((varName) => {
const node: VarNode = {
type: 'var',
nsName: element.ns,
varName: varName
};
return node;
});
}
}
return null;
}
}

export const buildTestProvider = function (): ClojureTestDataProvider {
return new ClojureTestDataProvider();
};

0 comments on commit 808ea90

Please sign in to comment.