diff --git a/src/editors/vscode/src/test/suite/netfx-msbuild-kit.ts b/src/editors/vscode/src/test/suite/netfx-msbuild-kit.ts index 3c852201..4a993ef8 100644 --- a/src/editors/vscode/src/test/suite/netfx-msbuild-kit.ts +++ b/src/editors/vscode/src/test/suite/netfx-msbuild-kit.ts @@ -240,6 +240,16 @@ export function fsharpSidecarLinesAbout(projectFile: string): string[] { export const EXIT_ON_NETFX = 48; export const EXIT_ON_NET = 10; +/** What a console program writes its exit code to, beside its own binary. */ +const RESULT_FILE = 'exit-code.txt'; + +/** + * The exit code VS Code reports when its Windows pty lost the real one: + * node-pty can close conout before ConPTY hands over the code, and VS Code's + * terminal process then fires `exitCode || 0`. These programs never return 0. + */ +const LOST_EXIT = 0; + /** One runnable fixture project. */ export interface RunnableProject { readonly name: string; @@ -261,20 +271,25 @@ export interface RunFixture extends LoadedFixture { /** * A console program named `name` returning 48 on .NET Framework and 10 on - * .NET, PLUS the number of arguments it received — so an exit code proves both - * the framework that ran and the arguments that reached it. + * .NET, PLUS the number of arguments it received — so its exit code proves both + * the framework that ran and the arguments that reached it. It also writes that + * code to {@link RESULT_FILE} beside its binary, which no pty can lose. */ function writeConsole(root: string, name: string, frameworks: ProjectFrameworks): string { const program = `${name}/Program.fs`; - const exits = [ + const code = [ + ' let code =', '#if NETFRAMEWORK', - ` ${String(EXIT_ON_NETFX)} + argv.Length`, + ` ${String(EXIT_ON_NETFX)} + argv.Length`, '#else', - ` ${String(EXIT_ON_NET)} + argv.Length`, + ` ${String(EXIT_ON_NET)} + argv.Length`, '#endif', + ` let result = System.IO.Path.Combine(System.AppContext.BaseDirectory, "${RESULT_FILE}")`, + ' System.IO.File.WriteAllText(result, string code)', + ' code', ]; writeFiles(root, { - [program]: [`module Fx.${name}`, '', '[]', 'let main argv =', ...exits, ''].join( + [program]: [`module Fx.${name}`, '', '[]', 'let main argv =', ...code, ''].join( '\n', ), }); @@ -322,6 +337,29 @@ export async function writeRunFixture(root: string): Promise { return { root, solution, probe, netfxOnly, ready }; } +/** Where `project`'s `tfm` build writes the code it returned. */ +export function resultFileOf(root: string, project: RunnableProject, tfm: string): string { + return path.join(root, project.name, 'bin', 'Debug', tfm, RESULT_FILE); +} + +/** + * `project`'s `tfm` build really returned `code`: the code it wrote, and the + * ONE task exit VS Code reported — that code, or {@link LOST_EXIT} when the + * Windows pty dropped it. + */ +export function assertRanWith( + result: string, + exits: readonly number[], + code: number, + tfm: string, +): void { + const why = `it really ran on ${tfm}: it returned ${String(code)}`; + assert.strictEqual(fs.readFileSync(result, 'utf8'), String(code), why); + assert.strictEqual(exits.length, 1, `one task process ended: ${exits.join(', ')}`); + const [reported = -1] = exits; + assert.ok([code, LOST_EXIT].includes(reported), `${why}; the task reported ${String(reported)}`); +} + /** A started task's definition, and whether it runs a process with no shell. */ export interface StartedTask { readonly definition: vscode.TaskDefinition; diff --git a/src/editors/vscode/src/test/suite/netfx-run-fsharp.test.ts b/src/editors/vscode/src/test/suite/netfx-run-fsharp.test.ts index bf87c024..0f73c0b3 100644 --- a/src/editors/vscode/src/test/suite/netfx-run-fsharp.test.ts +++ b/src/editors/vscode/src/test/suite/netfx-run-fsharp.test.ts @@ -7,20 +7,25 @@ // message, as a warning, starting nothing. // // Each console program returns 48 on .NET Framework and 10 on .NET, PLUS its -// argument count, so a task's exit code proves the framework that really ran -// and the profile arguments that really reached it. RunProbe declares two .NET +// argument count, and writes that code beside its binary, so the code proves +// the framework that really ran and the profile arguments that really reached +// it — read from the file, because VS Code's Windows pty can report a lost +// exit code as 0. RunProbe declares two .NET // versions, so F5's "active .NET" and "first .NET" cases are told apart. // // Covers [NETFX-DEBUG] and [NETFX-CONTEXT]. import * as assert from 'node:assert/strict'; +import * as fs from 'node:fs'; import * as path from 'node:path'; import type { SharpLspExtensionApi } from '../../extension.js'; import { assertContext, frameworkContextOf, underFramework } from './netfx-context-kit'; import { useLoadedFixture } from './netfx-language-kit'; import { + assertRanWith, EXIT_ON_NET, EXIT_ON_NETFX, recordStartedTasks, + resultFileOf, type RunnableProject, type StartedTask, writeRunFixture, @@ -100,13 +105,14 @@ suite('.NET Framework F# — Run on the active framework, Debug on .NET only', ( const tasks = new TaskRecorder(); const started = recordStartedTasks(); const mark = logMark(api); + const result = resultFileOf(fixture().root, project, tfm); + fs.rmSync(result, { force: true }); try { await focusDocument(path.join(fixture().root, project.program)); const run = await invokeCommand(CMD_RUN_PROGRAM); assert.ok(!run.rejected, `${CMD_RUN_PROGRAM} runs: ${run.message}`); await assertRunTask(tasks, started.started, project, tfm); - const exits = await tasks.waitForExits(1, FIXTURE_BUILD_MS); - assert.deepStrictEqual(exits, [exit], `it really ran on ${tfm}: it returned ${String(exit)}`); + assertRanWith(result, await tasks.waitForExits(1, FIXTURE_BUILD_MS), exit, tfm); await awaitLogged(api, mark, `Run: ${project.name} under ${tfm}`); } finally { tasks.dispose();