1- import { readFile } from "fs-extra" ;
1+ import { copy , createFile , lstat , pathExists , readFile } from "fs-extra" ;
22import type {
33 CancellationToken ,
44 TestController ,
55 TestItem ,
66 TestRun ,
77 TestRunRequest ,
8+ TextDocumentShowOptions ,
89 WorkspaceFolder ,
910 WorkspaceFoldersChangeEvent ,
1011} from "vscode" ;
@@ -15,6 +16,7 @@ import {
1516 TestRunProfileKind ,
1617 Uri ,
1718 tests ,
19+ window ,
1820 workspace ,
1921} from "vscode" ;
2022import { DisposableObject } from "../common/disposable-object" ;
@@ -23,11 +25,46 @@ import type { CodeQLCliServer } from "../codeql-cli/cli";
2325import { getErrorMessage } from "../common/helpers-pure" ;
2426import type { BaseLogger , LogOptions } from "../common/logging" ;
2527import type { TestRunner } from "./test-runner" ;
26- import { TestManagerBase } from "./test-manager-base" ;
2728import type { App } from "../common/app" ;
2829import { isWorkspaceFolderOnDisk } from "../common/vscode/workspace-folders" ;
2930import type { FileTreeNode } from "../common/file-tree-nodes" ;
3031import { FileTreeDirectory , FileTreeLeaf } from "../common/file-tree-nodes" ;
32+ import type { TestUICommands } from "../common/commands" ;
33+ import { basename , extname } from "path" ;
34+
35+ /**
36+ * Get the full path of the `.expected` file for the specified QL test.
37+ * @param testPath The full path to the test file.
38+ */
39+ function getExpectedFile ( testPath : string ) : string {
40+ return getTestOutputFile ( testPath , ".expected" ) ;
41+ }
42+
43+ /**
44+ * Get the full path of the `.actual` file for the specified QL test.
45+ * @param testPath The full path to the test file.
46+ */
47+ function getActualFile ( testPath : string ) : string {
48+ return getTestOutputFile ( testPath , ".actual" ) ;
49+ }
50+
51+ /**
52+ * Gets the the full path to a particular output file of the specified QL test.
53+ * @param testPath The full path to the QL test.
54+ * @param extension The file extension of the output file.
55+ */
56+ function getTestOutputFile ( testPath : string , extension : string ) : string {
57+ return changeExtension ( testPath , extension ) ;
58+ }
59+
60+ /**
61+ * Change the file extension of the specified path.
62+ * @param p The original file path.
63+ * @param ext The new extension, including the `.`.
64+ */
65+ function changeExtension ( p : string , ext : string ) : string {
66+ return p . slice ( 0 , - extname ( p ) . length ) + ext ;
67+ }
3168
3269/**
3370 * Returns the complete text content of the specified file. If there is an error reading the file,
@@ -108,7 +145,7 @@ class WorkspaceFolderHandler extends DisposableObject {
108145 * Service that populates the VS Code "Test Explorer" panel for CodeQL, and handles running and
109146 * debugging of tests.
110147 */
111- export class TestManager extends TestManagerBase {
148+ export class TestManager extends DisposableObject {
112149 /**
113150 * Maps from each workspace folder being tracked to the `WorkspaceFolderHandler` responsible for
114151 * tracking it.
@@ -119,7 +156,7 @@ export class TestManager extends TestManagerBase {
119156 > ( ) ;
120157
121158 public constructor (
122- app : App ,
159+ private readonly app : App ,
123160 private readonly testRunner : TestRunner ,
124161 private readonly cliServer : CodeQLCliServer ,
125162 // Having this as a parameter with a default value makes passing in a mock easier.
@@ -128,7 +165,7 @@ export class TestManager extends TestManagerBase {
128165 "CodeQL Tests" ,
129166 ) ,
130167 ) {
131- super ( app ) ;
168+ super ( ) ;
132169
133170 this . testController . createRunProfile (
134171 "Run" ,
@@ -151,13 +188,64 @@ export class TestManager extends TestManagerBase {
151188 super . dispose ( ) ;
152189 }
153190
191+ public getCommands ( ) : TestUICommands {
192+ return {
193+ "codeQLTests.showOutputDifferences" :
194+ this . showOutputDifferences . bind ( this ) ,
195+ "codeQLTests.acceptOutput" : this . acceptOutput . bind ( this ) ,
196+ "codeQLTests.acceptOutputContextTestItem" : this . acceptOutput . bind ( this ) ,
197+ } ;
198+ }
199+
154200 protected getTestPath ( node : TestItem ) : string {
155201 if ( node . uri === undefined || node . uri . scheme !== "file" ) {
156202 throw new Error ( "Selected test is not a CodeQL test." ) ;
157203 }
158204 return node . uri . fsPath ;
159205 }
160206
207+ private async acceptOutput ( node : TestItem ) : Promise < void > {
208+ const testPath = this . getTestPath ( node ) ;
209+ const stat = await lstat ( testPath ) ;
210+ if ( stat . isFile ( ) ) {
211+ const expectedPath = getExpectedFile ( testPath ) ;
212+ const actualPath = getActualFile ( testPath ) ;
213+ await copy ( actualPath , expectedPath , { overwrite : true } ) ;
214+ }
215+ }
216+
217+ private async showOutputDifferences ( node : TestItem ) : Promise < void > {
218+ const testId = this . getTestPath ( node ) ;
219+ const stat = await lstat ( testId ) ;
220+ if ( stat . isFile ( ) ) {
221+ const expectedPath = getExpectedFile ( testId ) ;
222+ const expectedUri = Uri . file ( expectedPath ) ;
223+ const actualPath = getActualFile ( testId ) ;
224+ const options : TextDocumentShowOptions = {
225+ preserveFocus : true ,
226+ preview : true ,
227+ } ;
228+
229+ if ( ! ( await pathExists ( expectedPath ) ) ) {
230+ // Just create a new file.
231+ await createFile ( expectedPath ) ;
232+ }
233+
234+ if ( await pathExists ( actualPath ) ) {
235+ const actualUri = Uri . file ( actualPath ) ;
236+ await this . app . commands . execute (
237+ "vscode.diff" ,
238+ expectedUri ,
239+ actualUri ,
240+ `Expected vs. Actual for ${ basename ( testId ) } ` ,
241+ options ,
242+ ) ;
243+ } else {
244+ await window . showTextDocument ( expectedUri , options ) ;
245+ }
246+ }
247+ }
248+
161249 /** Start tracking tests in the specified workspace folders. */
162250 private startTrackingWorkspaceFolders (
163251 workspaceFolders : readonly WorkspaceFolder [ ] ,
0 commit comments