diff --git a/.gitignore b/.gitignore index 819656f16c3..d9a2e5384de 100644 --- a/.gitignore +++ b/.gitignore @@ -148,3 +148,13 @@ TestResults/*.trx StandardOutput.txt StandardError.txt **/TestResults/ + +# CompilerCompat test project generated files +tests/projects/CompilerCompat/**/nuget.config +tests/projects/CompilerCompat/**/global.json +tests/projects/CompilerCompat/**/*.deps.json +tests/projects/CompilerCompat/**/*.xml +tests/projects/CompilerCompat/local-nuget-packages/ +tests/projects/CompilerCompat/lib-output-*/ +tests/projects/CompilerCompat/**/bin/ +tests/projects/CompilerCompat/**/obj/ diff --git a/azure-pipelines-PR.yml b/azure-pipelines-PR.yml index cfa5411ee33..ff316adf56a 100644 --- a/azure-pipelines-PR.yml +++ b/azure-pipelines-PR.yml @@ -740,6 +740,8 @@ stages: env: FSHARP_EXPERIMENTAL_FEATURES: $(_experimental_flag) displayName: End to end build tests + - script: .\eng\common\dotnet.cmd fsi .\tests\FSharp.Compiler.ComponentTests\CompilerCompatibilityTests.fsx + displayName: Compiler compatibility tests # Up-to-date - disabled due to it being flaky #- job: UpToDate_Windows diff --git a/tests/FSharp.Compiler.ComponentTests/CompilerCompatibilityTests.fsx b/tests/FSharp.Compiler.ComponentTests/CompilerCompatibilityTests.fsx new file mode 100644 index 00000000000..e2c91e120f2 --- /dev/null +++ b/tests/FSharp.Compiler.ComponentTests/CompilerCompatibilityTests.fsx @@ -0,0 +1,246 @@ +#!/usr/bin/env dotnet fsi + +// Standalone F# script to test compiler compatibility across different F# SDK versions +// Can be run with: dotnet fsi CompilerCompatibilityTests.fsx + +open System +open System.IO +open System.Diagnostics + +// Configuration +let compilerConfiguration = "Release" +let repoRoot = Path.GetFullPath(Path.Combine(__SOURCE_DIRECTORY__, "../..")) +let projectsPath = Path.Combine(__SOURCE_DIRECTORY__, "../projects/CompilerCompat") +let libProjectPath = Path.Combine(projectsPath, "CompilerCompatLib") +let appProjectPath = Path.Combine(projectsPath, "CompilerCompatApp") + +// Test scenarios: (libCompiler, appCompiler, description) +let testScenarios = [ + ("local", "local", "Baseline - Both library and app built with local compiler") + ("latest", "local", "Forward compatibility - Library with SDK, app with local") + ("local", "latest", "Backward compatibility - Library with local, app with SDK") + ("latest", "latest", "SDK only - Both library and app built with latest SDK") + ("net9", "local", "Net9 forward compatibility - Library with .NET 9 SDK, app with local") + ("local", "net9", "Net9 backward compatibility - Library with local, app with .NET 9 SDK") +] + +// Helper functions +let runCommand (command: string) (args: string) (workingDir: string) (envVars: (string * string) list) = + let psi = ProcessStartInfo() + psi.FileName <- command + psi.Arguments <- args + psi.WorkingDirectory <- workingDir + psi.RedirectStandardOutput <- true + psi.RedirectStandardError <- true + psi.UseShellExecute <- false + psi.CreateNoWindow <- true + + // Set environment variables + for (key, value) in envVars do + psi.EnvironmentVariables.[key] <- value + + use p = new Process() + p.StartInfo <- psi + + if not (p.Start()) then + failwith $"Failed to start process: {command} {args}" + + let stdout = p.StandardOutput.ReadToEnd() + let stderr = p.StandardError.ReadToEnd() + p.WaitForExit() + + if p.ExitCode <> 0 then + printfn "Command failed: %s %s" command args + printfn "Working directory: %s" workingDir + printfn "Exit code: %d" p.ExitCode + printfn "Stdout: %s" stdout + printfn "Stderr: %s" stderr + failwith $"Command exited with code {p.ExitCode}" + + stdout + +let cleanDirectory path = + if Directory.Exists(path) then + Directory.Delete(path, true) + +let cleanBinObjDirectories projectPath = + cleanDirectory (Path.Combine(projectPath, "bin")) + cleanDirectory (Path.Combine(projectPath, "obj")) + let libBuildInfo = Path.Combine(projectPath, "LibBuildInfo.fs") + let appBuildInfo = Path.Combine(projectPath, "AppBuildInfo.fs") + if File.Exists(libBuildInfo) then File.Delete(libBuildInfo) + if File.Exists(appBuildInfo) then File.Delete(appBuildInfo) + +let manageGlobalJson compilerVersion enable = + let globalJsonPath = Path.Combine(projectsPath, "global.json") + if compilerVersion = "net9" then + if enable && not (File.Exists(globalJsonPath) && File.ReadAllText(globalJsonPath).Contains("9.0.0")) then + printfn " Enabling .NET 9 SDK via global.json..." + let globalJsonContent = """{ + "sdk": { + "version": "9.0.0", + "rollForward": "latestMajor" + }, + "msbuild-sdks": { + "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.25509.1" + } +}""" + File.WriteAllText(globalJsonPath, globalJsonContent) + elif not enable && File.Exists(globalJsonPath) then + printfn " Removing global.json..." + File.Delete(globalJsonPath) + +let packProject projectPath compilerVersion outputDir = + let useLocal = (compilerVersion = "local") + // Use timestamp-based version to ensure fresh package each time + let timestamp = DateTime.Now.ToString("HHmmss") + let envVars = [ + ("LoadLocalFSharpBuild", if useLocal then "True" else "False") + ("LocalFSharpCompilerConfiguration", compilerConfiguration) + ("PackageVersion", $"1.0.{timestamp}") + ] + + // Manage global.json for net9 compiler + manageGlobalJson compilerVersion true + + printfn " Packing library with %s compiler..." compilerVersion + let projectFile = Path.Combine(projectPath, "CompilerCompatLib.fsproj") + let output = runCommand "dotnet" $"pack \"{projectFile}\" -c {compilerConfiguration} -o \"{outputDir}\"" projectPath envVars + + // Clean up global.json after pack + manageGlobalJson compilerVersion false + + output |> ignore + +let buildApp projectPath compilerVersion = + let useLocal = (compilerVersion = "local") + let envVars = [ + ("LoadLocalFSharpBuild", if useLocal then "True" else "False") + ("LocalFSharpCompilerConfiguration", compilerConfiguration) + ] + + // Manage global.json for net9 compiler + manageGlobalJson compilerVersion true + + printfn " Building app with %s compiler..." compilerVersion + let projectFile = Path.Combine(projectPath, "CompilerCompatApp.fsproj") + + // First restore with force to get fresh NuGet packages + runCommand "dotnet" $"restore \"{projectFile}\" --force --no-cache" projectPath envVars |> ignore + + // Then build + runCommand "dotnet" $"build \"{projectFile}\" -c {compilerConfiguration} --no-restore" projectPath envVars + |> ignore + + // Clean up global.json after build + manageGlobalJson compilerVersion false + +let runApp() = + let appDll = Path.Combine(appProjectPath, "bin", compilerConfiguration, "net8.0", "CompilerCompatApp.dll") + printfn " Running app..." + // Use --roll-forward Major to allow running net8.0 app on net10.0 runtime + let envVars = [ + ("DOTNET_ROLL_FORWARD", "Major") + ] + let output = runCommand "dotnet" $"\"{appDll}\"" appProjectPath envVars + output + +let extractValue (sectionHeader: string) (searchPattern: string) (lines: string array) = + lines + |> Array.tryFindIndex (fun (l: string) -> l.StartsWith(sectionHeader)) + |> Option.bind (fun startIdx -> + lines + |> Array.skip (startIdx + 1) + |> Array.take (min 10 (lines.Length - startIdx - 1)) + |> Array.tryFind (fun (l: string) -> l.Contains(searchPattern))) + +let verifyOutput libCompilerVersion appCompilerVersion (output: string) = + let lines = output.Split('\n') |> Array.map (fun (s: string) -> s.Trim()) + + // Check for success message + if not (Array.exists (fun (l: string) -> l.Contains("SUCCESS: All compiler compatibility tests passed")) lines) then + failwith "App did not report success" + + // Extract build info + let getBool section pattern = + extractValue section pattern lines + |> Option.map (fun l -> l.Contains("true")) + |> Option.defaultValue false + + let libIsLocal = getBool "Library Build Info:" "Is Local Build:" + let appIsLocal = getBool "Application Build Info:" "Is Local Build:" + + // Verify - both "latest" and "net9" should result in isLocalBuild=false + let expectedLibIsLocal = (libCompilerVersion = "local") + let expectedAppIsLocal = (appCompilerVersion = "local") + + if libIsLocal <> expectedLibIsLocal then + failwith $"Library: expected isLocalBuild={expectedLibIsLocal} for '{libCompilerVersion}', but got {libIsLocal}" + + if appIsLocal <> expectedAppIsLocal then + failwith $"App: expected isLocalBuild={expectedAppIsLocal} for '{appCompilerVersion}', but got {appIsLocal}" + + printfn " ✓ Build info verification passed" + +// Main test execution +let runTest (libCompiler, appCompiler, description) = + printfn "\n=== Test: %s ===" description + printfn "Library compiler: %s, App compiler: %s" libCompiler appCompiler + + try + // Clean previous builds + cleanBinObjDirectories libProjectPath + cleanBinObjDirectories appProjectPath + + // Create local NuGet directory + let localNuGetDir = Path.Combine(projectsPath, "local-nuget-packages") + cleanDirectory localNuGetDir + Directory.CreateDirectory(localNuGetDir) |> ignore + + // Create nuget.config for app + let nugetConfig = Path.Combine(appProjectPath, "nuget.config") + let nugetConfigContent = $""" + + + + + + +""" + File.WriteAllText(nugetConfig, nugetConfigContent) + + // Pack library + packProject libProjectPath libCompiler localNuGetDir + + // Build and run app + buildApp appProjectPath appCompiler + let output = runApp() + + // Verify + verifyOutput libCompiler appCompiler output + + printfn "✓ PASSED: %s" description + true + with ex -> + printfn "✗ FAILED: %s" description + printfn "Error: %s" ex.Message + false + +// Run all tests +printfn "F# Compiler Compatibility Test Suite" +printfn "======================================" + +let results = testScenarios |> List.map runTest + +let passed = results |> List.filter id |> List.length +let total = results |> List.length + +printfn "\n======================================" +printfn "Results: %d/%d tests passed" passed total + +if passed = total then + printfn "All tests PASSED ✓" + exit 0 +else + printfn "Some tests FAILED ✗" + exit 1 diff --git a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/checked/checked.fs b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/checked/checked.fs index b842f04de77..7421bd50261 100644 --- a/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/checked/checked.fs +++ b/tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/checked/checked.fs @@ -23,7 +23,7 @@ module Checked = compilation |> getCompilation |> asFs - |> withOptions["--checked"] + |> withOptions ["--checked"] |> compile |> shouldSucceed @@ -33,7 +33,7 @@ module Checked = compilation |> getCompilation |> asFs - |> withOptions["--checked+"] + |> withOptions ["--checked+"] |> compile |> shouldSucceed @@ -43,7 +43,7 @@ module Checked = compilation |> getCompilation |> asFs - |> withOptions["--checked-"] + |> withOptions ["--checked-"] |> compile |> shouldSucceed @@ -53,7 +53,7 @@ module Checked = compilation |> getCompilation |> asFsx - |> withOptions["--checked-"] + |> withOptions ["--checked-"] |> compile |> shouldSucceed @@ -63,7 +63,7 @@ module Checked = compilation |> getCompilation |> asFsx - |> withOptions["--checked"] + |> withOptions ["--checked"] |> compile |> shouldSucceed @@ -73,7 +73,7 @@ module Checked = compilation |> getCompilation |> asFsx - |> withOptions["--checked+"] + |> withOptions ["--checked+"] |> compile |> shouldSucceed @@ -83,7 +83,7 @@ module Checked = compilation |> getCompilation |> asFsx - |> withOptions["--checked-"] + |> withOptions ["--checked-"] |> compile |> shouldSucceed @@ -96,7 +96,7 @@ module Checked = compilation |> getCompilation |> asFs - |> withOptions["--checked"; "--checked+"] + |> withOptions ["--checked"; "--checked+"] |> compile |> shouldSucceed @@ -106,7 +106,7 @@ module Checked = compilation |> getCompilation |> asFs - |> withOptions["--checked-"; "--checked+"] + |> withOptions ["--checked-"; "--checked+"] |> compile |> shouldSucceed @@ -116,7 +116,7 @@ module Checked = compilation |> getCompilation |> asFs - |> withOptions["--checked+"; "--checked-"] + |> withOptions ["--checked+"; "--checked-"] |> compile |> shouldSucceed @@ -126,7 +126,7 @@ module Checked = compilation |> getCompilation |> asFsx - |> withOptions["--checked"; "--checked+"] + |> withOptions ["--checked"; "--checked+"] |> compile |> shouldSucceed @@ -136,7 +136,7 @@ module Checked = compilation |> getCompilation |> asFsx - |> withOptions["--checked-"; "--checked+"] + |> withOptions ["--checked-"; "--checked+"] |> compile |> shouldSucceed @@ -146,7 +146,7 @@ module Checked = compilation |> getCompilation |> asFsx - |> withOptions["--checked+"; "--checked-"] + |> withOptions ["--checked+"; "--checked-"] |> compile |> shouldSucceed @@ -157,7 +157,7 @@ module Checked = compilation |> getCompilation |> asFs - |> withOptions["--Checked"] + |> withOptions ["--Checked"] |> compile |> shouldFail |> withDiagnostics [ @@ -170,7 +170,7 @@ module Checked = compilation |> getCompilation |> asFs - |> withOptions["--checked*"] + |> withOptions ["--checked*"] |> compile |> shouldFail |> withDiagnostics [ diff --git a/tests/projects/CompilerCompat/.gitignore b/tests/projects/CompilerCompat/.gitignore new file mode 100644 index 00000000000..658db31ab74 --- /dev/null +++ b/tests/projects/CompilerCompat/.gitignore @@ -0,0 +1,6 @@ +# Ignore test-generated files +global.json +commandline.txt +**/*BuildInfo.fs +StandardOutput.txt +StandardError.txt \ No newline at end of file diff --git a/tests/projects/CompilerCompat/CompilerCompatApp/CompilerCompatApp.fsproj b/tests/projects/CompilerCompat/CompilerCompatApp/CompilerCompatApp.fsproj new file mode 100644 index 00000000000..311c09b259b --- /dev/null +++ b/tests/projects/CompilerCompat/CompilerCompatApp/CompilerCompatApp.fsproj @@ -0,0 +1,53 @@ + + + + + false + false + + + + Exe + net8.0 + + + bin\ + bin\$(Configuration)\ + + + + + + + + + + + + + + + + + + + + + + true + false + $(DotnetFscCompilerPath.Replace('"', '')) + N/A + + + + + \ No newline at end of file diff --git a/tests/projects/CompilerCompat/CompilerCompatApp/Program.fs b/tests/projects/CompilerCompat/CompilerCompatApp/Program.fs new file mode 100644 index 00000000000..3463bb05d20 --- /dev/null +++ b/tests/projects/CompilerCompat/CompilerCompatApp/Program.fs @@ -0,0 +1,54 @@ +open CompilerCompatLib +open CompilerCompatApp +open System + +[] +let main _argv = + try + // Helper to get the actual compiler path (prefer dotnetFscCompilerPath when using local build) + let getActualCompilerPath (dotnetPath: string) (fallbackPath: string) = + if dotnetPath <> "N/A" && dotnetPath <> "" then dotnetPath else fallbackPath + + // Print detailed build information to verify which compiler was used + printfn "=== BUILD VERIFICATION ===" + printfn "Library Build Info:" + printfn " SDK Version: %s" LibBuildInfo.sdkVersion + printfn " F# Compiler Path: %s" (getActualCompilerPath LibBuildInfo.dotnetFscCompilerPath LibBuildInfo.fsharpCompilerPath) + printfn " Is Local Build: %b" LibBuildInfo.isLocalBuild + printfn "Application Build Info:" + printfn " SDK Version: %s" AppBuildInfo.sdkVersion + printfn " F# Compiler Path: %s" (getActualCompilerPath AppBuildInfo.dotnetFscCompilerPath AppBuildInfo.fsharpCompilerPath) + printfn " Is Local Build: %b" AppBuildInfo.isLocalBuild + printfn "==========================" + + // Test basic anonymous record functionality + let record = Library.getAnonymousRecord() + printfn "Basic record: X=%d, Y=%s" record.X record.Y + + // Verify expected values + if record.X <> 42 || record.Y <> "hello" then + printfn "ERROR: Basic record values don't match expected" + 1 + else + printfn "SUCCESS: Basic record test passed" + + // Test complex anonymous record functionality + let complex = Library.getComplexAnonymousRecord() + printfn "Complex record: Simple.A=%d, Simple.B=%s" complex.Simple.A complex.Simple.B + printfn "Complex record: List has %d items" complex.List.Length + printfn "Complex record: Tuple=(%d, Value=%f)" (fst complex.Tuple) (snd complex.Tuple).Value + + // Test function that takes anonymous record + let processed = Library.processAnonymousRecord({| X = 123; Y = "test" |}) + printfn "Processed result: %s" processed + + if processed = "Processed: X=123, Y=test" then + printfn "SUCCESS: All compiler compatibility tests passed" + 0 + else + printfn "ERROR: Processed result doesn't match expected" + 1 + + with ex -> + printfn "ERROR: Exception occurred: %s" ex.Message + 1 \ No newline at end of file diff --git a/tests/projects/CompilerCompat/CompilerCompatLib/CompilerCompatLib.fsproj b/tests/projects/CompilerCompat/CompilerCompatLib/CompilerCompatLib.fsproj new file mode 100644 index 00000000000..a9442854e07 --- /dev/null +++ b/tests/projects/CompilerCompat/CompilerCompatLib/CompilerCompatLib.fsproj @@ -0,0 +1,64 @@ + + + + + false + false + + + + net8.0 + true + + + bin\ + bin\$(Configuration)\ + + + true + CompilerCompatLib + $(PackageVersion) + 1.0.0 + Test + Test library for compiler compatibility tests + + + + + + + + + + + + + + + + lib/$(TargetFramework) + PreserveNewest + true + + + + + + + true + false + $(DotnetFscCompilerPath.Replace('"', '')) + N/A + + + + + \ No newline at end of file diff --git a/tests/projects/CompilerCompat/CompilerCompatLib/Library.fs b/tests/projects/CompilerCompat/CompilerCompatLib/Library.fs new file mode 100644 index 00000000000..e0b4f380802 --- /dev/null +++ b/tests/projects/CompilerCompat/CompilerCompatLib/Library.fs @@ -0,0 +1,17 @@ +namespace CompilerCompatLib + +module Library = + /// Returns an anonymous record to test compiler compatibility + let getAnonymousRecord () = {| X = 42; Y = "hello" |} + + /// Returns a more complex anonymous record with nested structure + let getComplexAnonymousRecord () = + {| + Simple = {| A = 1; B = "test" |}; + List = [ {| Id = 1; Name = "first" |}; {| Id = 2; Name = "second" |} ]; + Tuple = (42, {| Value = 3.14; Label = "pi" |}) + |} + + /// Function that takes an anonymous record as parameter + let processAnonymousRecord (record: {| X: int; Y: string |}) = + sprintf "Processed: X=%d, Y=%s" record.X record.Y \ No newline at end of file