Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 44 additions & 6 deletions src/editors/vscode/src/test/suite/netfx-msbuild-kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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}`, '', '[<EntryPoint>]', 'let main argv =', ...exits, ''].join(
[program]: [`module Fx.${name}`, '', '[<EntryPoint>]', 'let main argv =', ...code, ''].join(
'\n',
),
});
Expand Down Expand Up @@ -322,6 +337,29 @@ export async function writeRunFixture(root: string): Promise<RunFixture> {
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;
Expand Down
14 changes: 10 additions & 4 deletions src/editors/vscode/src/test/suite/netfx-run-fsharp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down
Loading