Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c628564
Initial plan
Copilot Sep 17, 2025
e8d910f
Add compiler compatibility test suite with anonymous records
Copilot Sep 17, 2025
bbefae5
Complete compiler compatibility test suite implementation
Copilot Sep 17, 2025
13ca28d
Final implementation of compiler compatibility test suite
Copilot Sep 17, 2025
c5a2700
Address code review feedback: parametrized tests, global.json support…
Copilot Sep 17, 2025
360ae63
Address code review: fix LoadLocalFSharpBuild, simplify versions, rem…
Copilot Sep 17, 2025
9ea30da
Fix test failures: replace Unicode checkmarks with platform-agnostic …
Copilot Sep 18, 2025
86a9207
Add .NET 9 test cases and build verification info
Copilot Sep 19, 2025
2720a6f
Enhance build verification with detailed compiler tracking
Copilot Sep 19, 2025
91da72a
Fix CI failures by removing .NET 9 dependency
Copilot Sep 19, 2025
9846cef
Merge branch 'main' into copilot/fix-7efdf64f-592c-4ce7-9871-9e7dadc5…
T-Gro Sep 22, 2025
194b5fe
Implement proper build-time generated BuildInfo files with MSBuild ta…
Copilot Sep 22, 2025
8fc9d44
Fix CI compilation errors by converting arrays to lists for String.co…
Copilot Sep 22, 2025
f3cd733
Add comprehensive build verification assertions to prevent regressions
Copilot Sep 22, 2025
8294e80
Remove BuildInfo files from repository - they should be generated at …
Copilot Sep 22, 2025
c2a1464
Merge branch 'main' into copilot/fix-7efdf64f-592c-4ce7-9871-9e7dadc5…
T-Gro Oct 16, 2025
9f5a170
address build errors
T-Gro Oct 16, 2025
ee94052
Make it actually correct via human intervention
T-Gro Oct 17, 2025
4b7d566
Apply suggestion from @T-Gro
T-Gro Oct 17, 2025
c5a8fa3
Merge branch 'main' into copilot/fix-7efdf64f-592c-4ce7-9871-9e7dadc5…
T-Gro Oct 17, 2025
45c0567
Fix SDK version compatibility and rollForward policy
Copilot Oct 17, 2025
44e61f0
Fix all remaining SDK version references to 9.0.300
Copilot Oct 17, 2025
036cf72
Move from vstest (messes up with envvars and msbuild) to standalone f…
T-Gro Oct 21, 2025
25121d0
try using the arcade dotnet script
T-Gro Oct 21, 2025
921af4a
Add comprehensive README documentation to CompilerCompatibilityTests
Copilot Oct 22, 2025
776ef49
Merge branch 'main' into copilot/fix-7efdf64f-592c-4ce7-9871-9e7dadc5…
T-Gro Oct 22, 2025
f5aedac
Apply suggestion from @T-Gro
T-Gro Oct 22, 2025
b39c8ea
Apply suggestion from @T-Gro
T-Gro Oct 22, 2025
426812c
Apply suggestion from @T-Gro
T-Gro Oct 22, 2025
c060bfd
Apply suggestion from @T-Gro
T-Gro Oct 22, 2025
4eab007
Apply suggestion from @T-Gro
T-Gro Oct 22, 2025
1cc0fbd
Apply suggestion from @T-Gro
T-Gro Oct 22, 2025
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
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
2 changes: 2 additions & 0 deletions azure-pipelines-PR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
246 changes: 246 additions & 0 deletions tests/FSharp.Compiler.ComponentTests/CompilerCompatibilityTests.fsx
Original file line number Diff line number Diff line change
@@ -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

Comment thread
T-Gro marked this conversation as resolved.
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 = $"""<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="local-packages" value="{localNuGetDir}" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>"""
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
Loading
Loading