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

Display test results in CodeLens #24

Merged
merged 13 commits into from
Nov 26, 2017
Merged

Display test results in CodeLens #24

merged 13 commits into from
Nov 26, 2017

Conversation

samcragg
Copy link
Contributor

Hope you don't mind me suggesting an enhancement. Also, it's my first foray into TypeScript so happy to change things to match your preferences. I wanted to display the test result status next to the method, like VS does so I looked at getting the results from the most recent test run.:

preview

The results are outputted to a TRX file and then that file is parsed to get the test results. Finally, the vscode.executeDocumentSymbolProvider command is used to get all the symbols in the current document to match the method to the test name.

Things to note. I've tested it with xUnit - it probably needs checking with NUnit as well. Also, I'm using emoji's to display the results, as you can only have text in the CodeLens. This looks fine on Windows 10 but might need a switch to use normal text instead of emoji's in case it looks wrong on other OS's.

Finally, there are things I'd like to enhance but not sure how to implement. I'd like it so you can click on the status and a popup show you the test output (useful for failing tests).

+ Output the test results to the TRX logger with a temporary filename
+ Listen to changes to the temporary directory to see when the TRX file has been written to.
+ Add dependency on xmldom
+ Parse the XML into strongly types objects
Copy link
Owner

@formulahendry formulahendry left a comment

Choose a reason for hiding this comment

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

Thanks for your PR! Please help resolve the TSLint error in CI first. 😄

@samcragg
Copy link
Contributor Author

Sorry about that, still learning the ropes 😄

const nodes = xdoc.documentElement.getElementsByTagName("UnitTestResult");

// TSLint wants to use for-of here, but nodes doesn't support it
for (let i = 0; i < nodes.length; i++) { // tslint:disable-line
Copy link
Owner

Choose a reason for hiding this comment

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

Copy link
Contributor Author

@samcragg samcragg Nov 15, 2017

Choose a reason for hiding this comment

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

I did try that, however, at runtime it throws an exception TypeError: undefined is not a function as I don't believe HTMLCollection (returned by getElementsByTagName) has the iterator property so it can't be used in for-of. May be there's a better XML library I could use to parse the TRX file?

Copy link
Owner

Choose a reason for hiding this comment

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

In that case, let's just keep this for loop.

let timeout: NodeJS.Timer;
this.watcher = fs.watch(tempFolder, (eventType, fileName) => {
if ((fileName === TestResultsFile.ResultsFileName) && (eventType === "change")) {
if (!timeout) {
Copy link
Owner

Choose a reason for hiding this comment

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

I think you could use clearTimeout here

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'm not sure that does the same thing, as it a timeout previously established by calling setTimeout, however, we're calling it in the callback so there's nothing to cancel. I'm setting it to null so that I can see if I've already triggered a wait or not. I can introduce another flag variable if that would make the code easier to follow?

Copy link
Owner

Choose a reason for hiding this comment

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

I think you could refer to https://github.com/formulahendry/vscode-docker-explorer/blob/master/src/dockerTreeBase.ts#L21:31 The logic should be similar.
In current implementation, if the change event lasts for 30 seconds continuously (say every 100ms), parseResults will be called 30 times.
With the clearTimeout, it would be called only once (if the change event is 100ms, or less then 1000ms).

When the test is running, I guess the trx file will be changed continuously. I am not sure whether the TRX will always be a valid XML and could be read normally during the test is running. If it is, I think current implementation is good, since we could have a quick update when test is running.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Like it, done! 😄

try {
this.watcher.close();
fs.unlinkSync(this.resultsFile);
} catch {
Copy link
Owner

Choose a reason for hiding this comment

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

miss (error) --> catch (error)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added

+ Use clearTimeout when filtering multiple file change events
+ Fix empty catch block
* Gets the dotnet test argument to speicfy the output for the test results.
*/
private outputTestResults(): string {
return " --logger 'trx;LogFileName=" + this.resultsFile.fileName + "'";
Copy link
Owner

Choose a reason for hiding this comment

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

I test it on Windows, it is failed, After I change single quote to double quote, it is working. How about you?

Copy link
Owner

Choose a reason for hiding this comment

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

image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It did work for me, but I've replaced with double quotes and it still works for me so hopefully that should fix it for you

Copy link
Owner

Choose a reason for hiding this comment

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

I guess you are using PowerShell. Both are working on PowerShell, while single quote does not work on CMD.

for (const symbol of symbols.filter((x) => x.kind === SymbolKind.Method)) {
const fullName = symbol.containerName + "." + symbol.name;
for (const result of results.values()) {
if (result.testName.endsWith(fullName)) {
Copy link
Owner

@formulahendry formulahendry Nov 15, 2017

Choose a reason for hiding this comment

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

There is a corner case (when only namespace is different):
image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The symbol provider doesn't return anything for the namespace and the container of the class is empty, so would need to look at perhaps using omni-sharp directly? There's actually an issue with the test discovery with NUnit that only lists the method names, so if two tests exist with the same method name in different classes, NUnit only returns one of them.

Not sure how you want to approach this one? Accept it as a corner case until the extension uses omni-sharp to parse the files to discover the unit test methods?

Copy link
Owner

Choose a reason for hiding this comment

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

I think this corner case could be accepted.

Copy link
Owner

Choose a reason for hiding this comment

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

If you have interest to look at omni-sharp or other approach, it is warmly welcome! You could send PR in the future.

@@ -198,7 +215,7 @@ export class DotnetTestExplorer implements TreeDataProvider<TestNode> {
return new Promise((c, e) => {
try {
const results = Executor
.execSync(`dotnet test -t -v=q${this.checkBuildOption()}${this.checkRestoreOption()}`, this.testDirectoryPath)
.execSync(`dotnet test -t -v=q${this.getDotNetTestOptions()}`, this.testDirectoryPath)
Copy link
Owner

Choose a reason for hiding this comment

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

You did not add ${this.outputTestResults()}, so the test result will not be shown when VS Code starts. Is it by design?

Copy link
Owner

Choose a reason for hiding this comment

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

Oh, please ignore. It only lists the tests.

}

export class TestResultsFile implements Disposable {
private static readonly ResultsFileName = "TestExplorerResults.trx";
Copy link
Owner

Choose a reason for hiding this comment

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

What if we have multiple test project opened in different VS Code?

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'm now using a temporary random folder to store the results, so now works across multiple instances

return this.onDidChangeCodeLensesEmitter.event;
}

public provideCodeLenses(document: TextDocument, token: CancellationToken): CodeLens[] | Thenable<CodeLens[]> {
Copy link
Owner

Choose a reason for hiding this comment

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

Try with xUnit, NUnit and MSTest. Seems only xUnit is showing the test result icon. It would be great if you could also support NUnit and MSTest. 😃

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should now be fixed in the latest version, I've tried xUnit, MSTest and NUnit (the later two have the same format)

}

private checkEnabledOption(): void {
this.enabled = workspace.getConfiguration("csharp").get<boolean>("testsCodeLens.enabled", true);
Copy link
Owner

Choose a reason for hiding this comment

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

Not sure it is better to add an extra boolean setting, in case user want to only show the run test | debug test. When user click run test, our test result icon would not updated. This may confuse users.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added an extra setting dotnet-test-explorer.showCodeLens

+ Map the test definition to the test result in the TRX file
+ Use double quotes instead of single quotes
+ Use a specific setting for this extension to determine whether to show/hide the CodeLens test status
+ Use a random temporary folder to store the test results
// delay before we read anything to avoid doing too much work
const me = this;
let changeDelay: NodeJS.Timer;
this.watcher = fs.watch(tempFolder, (eventType, fileName) => {
Copy link
Owner

Choose a reason for hiding this comment

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

Only do this when 'showCodeLens' is true

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, done

* Gets the dotnet test argument to speicfy the output for the test results.
*/
private outputTestResults(): string {
return " --logger \"trx;LogFileName=" + this.resultsFile.fileName + "\"";
Copy link
Owner

Choose a reason for hiding this comment

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

return empty string when 'showCodeLens' is false

private watcher: fs.FSWatcher;

public constructor() {
const tempFolder = fs.mkdtempSync(path.join(os.tmpdir(), "test-explorer-"));
Copy link
Owner

Choose a reason for hiding this comment

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

Same here. Only do this when 'showCodeLens' is true.
Just want to make sure no unnecessary code is running


public dispose(): void {
try {
this.watcher.close();
Copy link
Owner

Choose a reason for hiding this comment

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

Also handle when when 'showCodeLens' is false

+ Only create the temproary folder if the CodeLens setting is enabled
+ Don't output the test results if CodeLens is disabled
const me = this;
let changeDelay: NodeJS.Timer;
this.watcher = fs.watch(folder, (eventType, fileName) => {
if ((fileName === TestResultsFile.ResultsFileName) && (eventType === "change")) {
Copy link
Owner

Choose a reason for hiding this comment

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

Test it on macOS, when first run the test, it will only have eventType === "rename". So we could just remove (eventType === "change")

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed

export class TestStatusCodeLens extends CodeLens {
public static parseOutcome(outcome: string): string {
if (outcome === "Passed") {
return "✔️";
Copy link
Owner

Choose a reason for hiding this comment

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

Test on macOS, it is showing a black check mark (https://emojipedia.org/heavy-check-mark/) which is hard to see.
Two improvements:

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've added settings now so the user can override what we display.

I've kept the heavy check mark for platforms apart from OS X, in which case it uses the white heavy check mark (I'm not a big fan of it, but I can't find any alternatives).

Since the user can specify the text, they could even switch to "passing", "FAILING" etc if they want so hopefully this is OK?

Copy link
Owner

Choose a reason for hiding this comment

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

Looks great to me!

if (outcome === "Passed") {
return "✔️";
} else if (outcome === "Failed") {
return "❌";
Copy link
Owner

Choose a reason for hiding this comment

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

Same here, using unicode: https://emojipedia.org/cross-mark/

} else if (outcome === "Failed") {
return "❌";
} else if (outcome === "NotExecuted") {
return "️️⚠️";
Copy link
Owner

Choose a reason for hiding this comment

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

Same here, using unicode

Copy link
Owner

@formulahendry formulahendry left a comment

Choose a reason for hiding this comment

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

I left some comments which should just have some small code change. I think we are closing to ship it!

src/utility.ts Outdated
const osx = platform() === "darwin";

Utility.showCodeLens = configuration.get<boolean>("showCodeLens", true);
Utility.failed = Utility.getLensText(configuration, "codeLensPassed", "\u274c"); // Cross Mark
Copy link
Owner

Choose a reason for hiding this comment

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

codeLensPassed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, I've actually tested them all this time before submitting (and had my ☕️!)

src/utility.ts Outdated
Utility.showCodeLens = configuration.get<boolean>("showCodeLens", true);
Utility.failed = Utility.getLensText(configuration, "codeLensPassed", "\u274c"); // Cross Mark
Utility.passed = Utility.getLensText(configuration, "codeLensPassed", osx ? "\u2705" : "\u2714"); // White Heavy Check Mark / Heavy Check Mark
Utility.skipped = Utility.getLensText(configuration, "codeLensPassed", "\u26a0"); // Warning
Copy link
Owner

Choose a reason for hiding this comment

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

codeLensPassed?

Don't commit code before coffee...
@formulahendry
Copy link
Owner

@samcragg , it is all good for C# in xUnit, NUnit and MSTest, also Windows and macOS. I just try it on F# but not show the CodeLens. I think F# extension also implements the executeDocumentSymbolProvider similar to C# extension. Would you please take a look? Or you want to support C# first?

@formulahendry
Copy link
Owner

I just realize that you registerCodeLensProvider for csharp only. I think it is good enough to support C# first. 😃 Thank you for your contribution! This feature is awesome! If you have interest, welcome PR to support F# or any other features.

Copy link
Owner

@formulahendry formulahendry left a comment

Choose a reason for hiding this comment

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

:shipit:

@formulahendry formulahendry merged commit 2aceb7f into formulahendry:master Nov 26, 2017
@samcragg
Copy link
Contributor Author

Thanks! I'll take a look at F# and submit another PR.

I also want to make it so you can click on the lens and it shows you the test output inline (similar to how clicking on the references CodeLens shows them inline) so you can see why a test failed without having to dig through the output yourself (the output is in the XML, just need somewhere to display it).

@formulahendry
Copy link
Owner

Thanks @samcragg ! That would be great!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants