From b3b31043235f8f9beff44b31f4e2895fa0cab152 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Tue, 24 Feb 2026 12:46:52 +0100 Subject: [PATCH 1/3] Restore XunitTestFramework --- tests/FSharp.Test.Utilities/TestFramework.fs | 30 +++++--------------- tests/FSharp.Test.Utilities/XunitHelpers.fs | 24 ++++++---------- tests/FSharp.Test.Utilities/XunitSetup.fs | 17 ++--------- 3 files changed, 19 insertions(+), 52 deletions(-) diff --git a/tests/FSharp.Test.Utilities/TestFramework.fs b/tests/FSharp.Test.Utilities/TestFramework.fs index dcb64909ad..edfd00f9b6 100644 --- a/tests/FSharp.Test.Utilities/TestFramework.fs +++ b/tests/FSharp.Test.Utilities/TestFramework.fs @@ -515,33 +515,19 @@ module Command = let exec dir envVars (redirect:RedirectInfo) path args = -#if !NETCOREAPP - let ensureConsole () = - // Set UTF-8 encoding for console input/output to ensure FSI receives UTF-8 data. - // This is needed because on net472 ProcessStartInfo.StandardInputEncoding is unavailable, - // so the spawned process inherits the console's encoding settings. - Console.InputEncoding <- Text.UTF8Encoding(false) - Console.OutputEncoding <- Text.UTF8Encoding(false) -#else - let ensureConsole () = () -#endif - let inputWriter sources (writer: StreamWriter) = let pipeFile name = async { let path = Commands.getfullpath dir name - - // Read file content as text using UTF-8 (the standard encoding for F# source files) - let! content = async { - use reader = new StreamReader(path, Text.Encoding.UTF8, detectEncodingFromByteOrderMarks = true) - return! reader.ReadToEndAsync() |> Async.AwaitTask - } - - // Write using the StreamWriter which now uses UTF-8 encoding (set in ensureConsole). + use reader = File.OpenRead (path) + use ms = new MemoryStream() + do! reader.CopyToAsync (ms) |> (Async.AwaitIAsyncResult >> Async.Ignore) + ms.Position <- 0L try - do! writer.WriteAsync(content) |> Async.AwaitTask + do! ms.CopyToAsync(writer.BaseStream) |> (Async.AwaitIAsyncResult >> Async.Ignore) do! writer.FlushAsync() |> (Async.AwaitIAsyncResult >> Async.Ignore) with - | :? System.IO.IOException -> () + | :? System.IO.IOException -> //input closed is ok if process is closed + () } sources |> pipeFile |> Async.RunSynchronously @@ -585,8 +571,6 @@ module Command = let exec cmdArgs = printfn "%s" (logExec dir path args redirect) - if cmdArgs.RedirectInput.IsSome then - ensureConsole() Process.exec cmdArgs dir envVars path args { RedirectOutput = None; RedirectError = None; RedirectInput = None } diff --git a/tests/FSharp.Test.Utilities/XunitHelpers.fs b/tests/FSharp.Test.Utilities/XunitHelpers.fs index e74e8c38f7..5941248a05 100644 --- a/tests/FSharp.Test.Utilities/XunitHelpers.fs +++ b/tests/FSharp.Test.Utilities/XunitHelpers.fs @@ -189,35 +189,29 @@ module OneTimeSetup = // Ensure that the initialization is done only once per test run. init.Force() -/// `XunitTestFramework` providing parallel console support and conditionally enabling optional xUnit customizations. -/// NOTE: Temporarily disabled due to xUnit3 API incompatibilities -/// TODO: Reimplement for xUnit3 if OneTimeSetup, OpenTelemetry, or cleanup functionality is needed -(* -type FSharpXunitFramework(sink: IMessageSink) = - inherit XunitTestFramework(sink) +type FSharpXunitFramework() = + inherit XunitTestFramework() do OneTimeSetup.EnsureInitialized() - override this.CreateExecutor (assemblyName) = - { new XunitTestFrameworkExecutor(assemblyName, this.SourceInformationProvider, this.DiagnosticMessageSink) with + override this.CreateExecutor (assembly) = + { new XunitTestFrameworkExecutor(new XunitTestAssembly(assembly)) with - // Because xUnit v2 lacks assembly fixture, this is a good place to ensure things get called right at the start of the test run. - override x.RunTestCases(testCases, executionMessageSink, executionOptions) = + // Because xUnit v3 lacks assembly fixture, this is a good place to ensure things get called right at the start of the test run. + override x.RunTestCases(testCases, executionMessageSink, executionOptions, cancellationToken) = - let testRunName = $"RunTests_{assemblyName.Name} {Runtime.InteropServices.RuntimeInformation.FrameworkDescription}" + let testRunName = $"RunTests_{assembly.GetName().Name} {Runtime.InteropServices.RuntimeInformation.FrameworkDescription}" use _ = new OpenTelemetryExport(testRunName, Environment.GetEnvironmentVariable("FSHARP_OTEL_EXPORT") <> null) begin use _ = Activity.startNoTags testRunName - // We can't just call base.RunTestCases here, because it's implementation is async void. - use runner = new XunitTestAssemblyRunner (x.TestAssembly, testCases, x.DiagnosticMessageSink, executionMessageSink, executionOptions) - runner.RunAsync().Wait() + base.RunTestCases(testCases, executionMessageSink, executionOptions, cancellationToken).AsTask().Wait() end cleanUpTemporaryDirectoryOfThisTestRun () + new ValueTask() } -*) #if XUNIT_EXTRAS // Rewrites discovered test cases to support extra parallelization and batch trait injection. diff --git a/tests/FSharp.Test.Utilities/XunitSetup.fs b/tests/FSharp.Test.Utilities/XunitSetup.fs index 93d2b09121..7f00c00d36 100644 --- a/tests/FSharp.Test.Utilities/XunitSetup.fs +++ b/tests/FSharp.Test.Utilities/XunitSetup.fs @@ -24,17 +24,6 @@ type NotThreadSafeResourceCollection() = class end module XUnitSetup = - // NOTE: Custom TestFramework temporarily disabled due to xUnit3 API incompatibilities - // TODO: Reimplement FSharpXunitFramework for xUnit3 if needed - // [] - - // NOTE: CaptureTrace is disabled because it conflicts with TestConsole.ExecutionCapture - // which is used by FSI tests to capture console output. xUnit3's trace capture intercepts - // console output before it can reach TestConsole's redirectors. - // [] - - /// Call this to ensure TestConsole is installed. Safe to call multiple times. - let initialize() = XUnitInit.initialize() - - // Force initialization when module is loaded - do initialize() + [)>] + do () + From e0999093aaf42307f2256fac79f10ea1e1566906 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Wed, 25 Feb 2026 17:13:58 +0100 Subject: [PATCH 2/3] Move InputEncoding workaround --- tests/FSharp.Test.Utilities/XunitHelpers.fs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/FSharp.Test.Utilities/XunitHelpers.fs b/tests/FSharp.Test.Utilities/XunitHelpers.fs index 5941248a05..b78ce32969 100644 --- a/tests/FSharp.Test.Utilities/XunitHelpers.fs +++ b/tests/FSharp.Test.Utilities/XunitHelpers.fs @@ -18,6 +18,7 @@ open FSharp.Compiler.Diagnostics open OpenTelemetry.Resources open OpenTelemetry.Trace open OpenTelemetry.Metrics +open System.Text /// Disables custom internal parallelization added with XUNIT_EXTRAS. /// Execute test cases in a class or a module one by one instead of all at once. Allow other collections to run simultaneously. @@ -200,6 +201,16 @@ type FSharpXunitFramework() = // Because xUnit v3 lacks assembly fixture, this is a good place to ensure things get called right at the start of the test run. override x.RunTestCases(testCases, executionMessageSink, executionOptions, cancellationToken) = + // When running in Azure DevOps, we end up with codepage 65001 (UTF-8). + // So, the process where the tests are running has UTF-8 with BOM, by default. + // The child fsi.exe process is started with CreateNoWindow, causing it to not inherit the same encoding. + // So, when we attempt to write to its standard input, we right with BOM, but the child process isn't expecting the BOM character. + // To workaround this, we set the console input encoding to UTF-8 without BOM, explicitly. + // This is probably a bug in .NET Framework Process API when CreateNoWindow is set to true. + // It looks like the following logic assumes that the child process always inherits the same input encoding: + // https://github.com/microsoft/referencesource/blob/ec9fa9ae770d522a5b5f0607898044b7478574a3/System/services/monitoring/system/diagnosticts/Process.cs#L2153-L2156 + Console.InputEncoding <- UTF8Encoding(false) + let testRunName = $"RunTests_{assembly.GetName().Name} {Runtime.InteropServices.RuntimeInformation.FrameworkDescription}" use _ = new OpenTelemetryExport(testRunName, Environment.GetEnvironmentVariable("FSHARP_OTEL_EXPORT") <> null) From bc5c61c93b351875daf6fa0d830f306e0394192f Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Wed, 25 Feb 2026 17:14:54 +0100 Subject: [PATCH 3/3] Comment --- tests/FSharp.Test.Utilities/XunitHelpers.fs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/FSharp.Test.Utilities/XunitHelpers.fs b/tests/FSharp.Test.Utilities/XunitHelpers.fs index b78ce32969..91f230277a 100644 --- a/tests/FSharp.Test.Utilities/XunitHelpers.fs +++ b/tests/FSharp.Test.Utilities/XunitHelpers.fs @@ -198,9 +198,8 @@ type FSharpXunitFramework() = override this.CreateExecutor (assembly) = { new XunitTestFrameworkExecutor(new XunitTestAssembly(assembly)) with - // Because xUnit v3 lacks assembly fixture, this is a good place to ensure things get called right at the start of the test run. + // TODO: Consider moving most of that to ITestPipelineStartup implementation. override x.RunTestCases(testCases, executionMessageSink, executionOptions, cancellationToken) = - // When running in Azure DevOps, we end up with codepage 65001 (UTF-8). // So, the process where the tests are running has UTF-8 with BOM, by default. // The child fsi.exe process is started with CreateNoWindow, causing it to not inherit the same encoding.