Skip to content

Commit ee94052

Browse files
committed
Make it actually correct via human intervention
1 parent 9f5a170 commit ee94052

File tree

7 files changed

+282
-33
lines changed

7 files changed

+282
-33
lines changed

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,13 @@ TestResults/*.trx
148148
StandardOutput.txt
149149
StandardError.txt
150150
**/TestResults/
151+
152+
# CompilerCompat test project generated files
153+
tests/projects/CompilerCompat/**/nuget.config
154+
tests/projects/CompilerCompat/**/global.json
155+
tests/projects/CompilerCompat/**/*.deps.json
156+
tests/projects/CompilerCompat/**/*.xml
157+
tests/projects/CompilerCompat/local-nuget-packages/
158+
tests/projects/CompilerCompat/lib-output-*/
159+
tests/projects/CompilerCompat/**/bin/
160+
tests/projects/CompilerCompat/**/obj/

tests/FSharp.Compiler.ComponentTests/CompilerCompatibilityTests.fs

Lines changed: 224 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module FSharp.Compiler.ComponentTests.CompilerCompatibilityTests
22

33

44
open System.IO
5+
open System.Diagnostics
56
open Xunit
67
open TestFramework
78

@@ -12,18 +13,124 @@ type CompilerCompatibilityTests() =
1213
let libProjectPath = Path.Combine(projectsPath, "CompilerCompatLib")
1314
let appProjectPath = Path.Combine(projectsPath, "CompilerCompatApp")
1415

15-
let runDotnetBuild projectPath compilerVersion =
16-
let args =
17-
match compilerVersion with
18-
| "local" -> "build -c Release -p:LoadLocalFSharpBuild=True"
19-
| _ -> "build -c Release"
20-
21-
let (exitCode, output, error) = Commands.executeProcess "dotnet" args projectPath
16+
let createGlobalJson (directory: string) (sdkVersion: string) =
17+
let globalJsonPath = Path.Combine(directory, "global.json")
18+
let content = $"""{{
19+
"sdk": {{
20+
"version": "{sdkVersion}",
21+
"rollForward": "latestPatch"
22+
}}
23+
}}"""
24+
File.WriteAllText(globalJsonPath, content)
25+
globalJsonPath
26+
27+
let deleteGlobalJson (directory: string) =
28+
let globalJsonPath = Path.Combine(directory, "global.json")
29+
if File.Exists(globalJsonPath) then
30+
File.Delete(globalJsonPath)
31+
32+
let executeDotnetProcess (command: string) (workingDir: string) (clearDotnetRoot: bool) =
33+
let psi = ProcessStartInfo()
34+
// For net9 scenarios, use full path to system dotnet to bypass repo's .dotnet
35+
// This ensures the spawned process uses system SDKs instead of repo's local SDK
36+
let dotnetExe =
37+
if clearDotnetRoot then
38+
if System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows) then
39+
@"C:\Program Files\dotnet\dotnet.exe"
40+
elif System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.OSX) then
41+
"/usr/local/share/dotnet/dotnet"
42+
else // Linux
43+
"/usr/share/dotnet/dotnet"
44+
else
45+
"dotnet"
2246

23-
if exitCode <> 0 then
24-
failwith $"Build failed with exit code {exitCode}. Output: {output}. Error: {error}"
47+
psi.FileName <- dotnetExe
48+
psi.WorkingDirectory <- workingDir
49+
psi.RedirectStandardOutput <- true
50+
psi.RedirectStandardError <- true
51+
psi.Arguments <- command
52+
psi.CreateNoWindow <- true
53+
psi.EnvironmentVariables["DOTNET_ROLL_FORWARD"] <- "LatestMajor"
54+
psi.EnvironmentVariables["DOTNET_ROLL_FORWARD_TO_PRERELEASE"] <- "1"
55+
psi.EnvironmentVariables.Remove("MSBuildSDKsPath")
2556

26-
output
57+
// For net9 scenarios, remove DOTNET_ROOT so dotnet looks in its default locations
58+
if clearDotnetRoot then
59+
psi.EnvironmentVariables.Remove("DOTNET_ROOT")
60+
61+
psi.UseShellExecute <- false
62+
63+
use p = new Process()
64+
p.StartInfo <- psi
65+
66+
if not (p.Start()) then failwith "new process did not start"
67+
68+
let readOutput = backgroundTask { return! p.StandardOutput.ReadToEndAsync() }
69+
let readErrors = backgroundTask { return! p.StandardError.ReadToEndAsync() }
70+
71+
p.WaitForExit()
72+
73+
p.ExitCode, readOutput.Result, readErrors.Result
74+
75+
let runDotnetCommand (command: string) (workingDir: string) (compilerVersion: string) =
76+
let clearDotnetRoot = (compilerVersion = "net9")
77+
executeDotnetProcess command workingDir clearDotnetRoot
78+
|> fun (exitCode, stdout, stderr) ->
79+
if exitCode <> 0 then
80+
failwith $"Command failed with exit code {exitCode}:\nCommand: dotnet {command}\nStdout:\n{stdout}\nStderr:\n{stderr}"
81+
stdout
82+
83+
let buildProject (projectPath: string) (compilerVersion: string) =
84+
// Use the same configuration as the test project itself
85+
#if DEBUG
86+
let configuration = "Debug"
87+
#else
88+
let configuration = "Release"
89+
#endif
90+
let projectDir = Path.GetDirectoryName(projectPath)
91+
92+
// For net9, create a global.json to pin SDK version
93+
if compilerVersion = "net9" then
94+
createGlobalJson projectDir "9.0.306" |> ignore
95+
96+
let buildArgs =
97+
if compilerVersion = "local" then
98+
$"build \"{projectPath}\" -c {configuration} --no-restore -p:LoadLocalFSharpBuild=true -p:LocalFSharpCompilerConfiguration={configuration}"
99+
else
100+
$"build \"{projectPath}\" -c {configuration} --no-restore"
101+
102+
try
103+
runDotnetCommand buildArgs projectDir compilerVersion
104+
finally
105+
// Clean up global.json after build
106+
if compilerVersion = "net9" then
107+
deleteGlobalJson projectDir
108+
109+
let packProject (projectPath: string) (compilerVersion: string) (outputDir: string) =
110+
// Use the same configuration as the test project itself
111+
#if DEBUG
112+
let configuration = "Debug"
113+
#else
114+
let configuration = "Release"
115+
#endif
116+
let projectDir = Path.GetDirectoryName(projectPath)
117+
118+
// For net9, create a global.json to pin SDK version
119+
if compilerVersion = "net9" then
120+
createGlobalJson projectDir "9.0.306" |> ignore
121+
122+
let packArgs =
123+
if compilerVersion = "local" then
124+
$"pack \"{projectPath}\" -c {configuration} -p:LoadLocalFSharpBuild=true -p:LocalFSharpCompilerConfiguration={configuration} -o \"{outputDir}\""
125+
else
126+
$"pack \"{projectPath}\" -c {configuration} -o \"{outputDir}\""
127+
128+
try
129+
runDotnetCommand packArgs projectDir compilerVersion
130+
finally
131+
// Clean up global.json after pack
132+
if compilerVersion = "net9" then
133+
deleteGlobalJson projectDir
27134

28135
let runApp appBinaryPath =
29136
Commands.executeProcess "dotnet" appBinaryPath (Path.GetDirectoryName(appBinaryPath))
@@ -38,31 +145,89 @@ type CompilerCompatibilityTests() =
38145
Directory.Delete(objPath, true)
39146

40147
let getAppDllPath () =
41-
// The app is built to artifacts directory due to Directory.Build.props
42-
Path.Combine(__SOURCE_DIRECTORY__, "..", "..", "artifacts", "bin", "CompilerCompatApp", "Release", "net8.0", "CompilerCompatApp.dll")
148+
// The app is built to its local bin directory (not artifacts) due to isolated Directory.Build.props
149+
// Use the same configuration as the test project itself
150+
#if DEBUG
151+
let configuration = "Debug"
152+
#else
153+
let configuration = "Release"
154+
#endif
155+
Path.Combine(appProjectPath, "bin", configuration, "net8.0", "CompilerCompatApp.dll")
43156

44157
[<Theory>]
45158
[<InlineData("local", "local", "Baseline scenario - Both library and app built with local compiler")>]
46159
[<InlineData("latest", "local", "Forward compatibility - Library built with latest SDK, app with local compiler")>]
47160
[<InlineData("local", "latest", "Backward compatibility - Library built with local compiler, app with latest SDK")>]
161+
[<InlineData("net9", "local", "Cross-version compatibility - Library built with .NET 9, app with local compiler")>]
162+
[<InlineData("local", "net9", "Cross-version compatibility - Library built with local compiler, app with .NET 9")>]
48163
member _.``Compiler compatibility test``(libCompilerVersion: string, appCompilerVersion: string, scenarioDescription: string) =
49-
// Clean previous builds
164+
// Clean previous builds (no artifacts directory due to isolated Directory.Build.props)
50165
cleanBinObjDirectories libProjectPath
51166
cleanBinObjDirectories appProjectPath
52167

53-
// Build library with specified compiler version
54-
let libOutput = runDotnetBuild libProjectPath libCompilerVersion
55-
Assert.Contains("CompilerCompatLib -> ", libOutput)
168+
// Create a local NuGet packages directory for this test
169+
let localNuGetDir = Path.Combine(projectsPath, "local-nuget-packages")
170+
if Directory.Exists(localNuGetDir) then Directory.Delete(localNuGetDir, true)
171+
Directory.CreateDirectory(localNuGetDir) |> ignore
172+
173+
// Step 1: Pack the library with the specified compiler version (which will also build it)
174+
let libProjectFile = Path.Combine(libProjectPath, "CompilerCompatLib.fsproj")
175+
176+
let packOutput = packProject libProjectFile libCompilerVersion localNuGetDir
177+
Assert.Contains("Successfully created package", packOutput)
178+
179+
// Verify the nupkg file was created
180+
let nupkgFiles = Directory.GetFiles(localNuGetDir, "CompilerCompatLib.*.nupkg")
181+
Assert.True(nupkgFiles.Length > 0, $"No .nupkg file found in {localNuGetDir}")
56182

57-
// Build app with specified compiler version
58-
let appOutput = runDotnetBuild appProjectPath appCompilerVersion
59-
Assert.Contains("CompilerCompatApp -> ", appOutput)
183+
// Step 2: Configure app to use the local NuGet package
184+
// Create a temporary nuget.config that points to our local package directory
185+
let appNuGetConfig = Path.Combine(appProjectPath, "nuget.config")
186+
let nuGetConfigContent = $"""<?xml version="1.0" encoding="utf-8"?>
187+
<configuration>
188+
<packageSources>
189+
<clear />
190+
<add key="local" value="{localNuGetDir}" />
191+
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
192+
</packageSources>
193+
</configuration>"""
194+
File.WriteAllText(appNuGetConfig, nuGetConfigContent)
60195

61-
// Run app and verify it works
196+
// For net9, create global.json before restore
197+
if appCompilerVersion = "net9" then
198+
createGlobalJson appProjectPath "9.0.306" |> ignore
199+
200+
try
201+
// Step 3: Clear global packages cache to ensure we get the fresh package, then restore the app
202+
let appProjectFile = Path.Combine(appProjectPath, "CompilerCompatApp.fsproj")
203+
let _ = runDotnetCommand "nuget locals global-packages --clear" appProjectPath appCompilerVersion
204+
let restoreOutput = runDotnetCommand $"restore \"{appProjectFile}\" --force --no-cache" appProjectPath appCompilerVersion
205+
// Restore may say "Restore complete", "Restored", or "All projects are up-to-date" depending on state
206+
Assert.True(
207+
restoreOutput.Contains("Restore complete") || restoreOutput.Contains("Restored") || restoreOutput.Contains("up-to-date"),
208+
$"Restore did not complete successfully. Output:\n{restoreOutput}")
209+
210+
// Step 4: Build the app with the specified compiler version
211+
// The app will use the NuGet package we just created and restored
212+
let appOutput = buildProject appProjectFile appCompilerVersion
213+
Assert.Contains("CompilerCompatApp -> ", appOutput)
214+
finally
215+
// Clean up global.json if we created it
216+
if appCompilerVersion = "net9" then
217+
deleteGlobalJson appProjectPath
218+
219+
// Clean up the temporary nuget.config
220+
File.Delete(appNuGetConfig)
221+
222+
// Step 5: Run the app and verify it works
62223
let appDllPath = getAppDllPath()
63224
Assert.True(File.Exists(appDllPath), $"App DLL not found at {appDllPath} for scenario: {scenarioDescription}")
64225

65-
let (exitCode, output, _error) = runApp appDllPath
226+
let (exitCode, output, error) = runApp appDllPath
227+
if exitCode <> 0 then
228+
printfn $"App failed with exit code {exitCode}"
229+
printfn $"Output:\n{output}"
230+
printfn $"Error:\n{error}"
66231
Assert.Equal(0, exitCode)
67232
Assert.Contains("SUCCESS: All compiler compatibility tests passed", output)
68233

@@ -106,12 +271,47 @@ type CompilerCompatibilityTests() =
106271
Assert.True((lines |> Array.exists (fun l -> l.Contains("F# Compiler Path:") && not (l.Contains("Unknown")))),
107272
"F# Compiler Path should be captured from build-time, not show 'Unknown'")
108273

109-
// Validate that local builds have artifacts path and non-local builds don't
274+
// Extract F# Compiler Paths to compare
275+
let extractCompilerPath (sectionHeader: string) =
276+
lines
277+
|> Array.tryFindIndex (fun l -> l.StartsWith(sectionHeader))
278+
|> Option.bind (fun startIdx ->
279+
lines
280+
|> Array.skip (startIdx + 1)
281+
|> Array.tryFind (fun l -> l.Contains("F# Compiler Path:"))
282+
|> Option.map (fun l -> l.Trim()))
283+
284+
let libCompilerPath = extractCompilerPath "Library Build Info:"
285+
let appCompilerPath = extractCompilerPath "Application Build Info:"
286+
287+
// Validate that F# Compiler Path consistency matches compiler version consistency
288+
match libCompilerPath, appCompilerPath with
289+
| Some libPath, Some appPath ->
290+
if libCompilerVersion = appCompilerVersion then
291+
Assert.True((libPath = appPath),
292+
$"Same compiler version ('{libCompilerVersion}') should have same F# Compiler Path, but lib has '{libPath}' and app has '{appPath}'")
293+
else
294+
Assert.True((libPath <> appPath),
295+
$"Different compiler versions (lib='{libCompilerVersion}', app='{appCompilerVersion}') should have different F# Compiler Paths, but both have '{libPath}'")
296+
| _ -> Assert.True(false, "Could not extract F# Compiler Path from output")
297+
298+
// Validate that local builds have artifacts path
299+
// Look for the section header, then check if any subsequent lines contain artifacts
300+
let hasArtifactsInSection (sectionHeader: string) =
301+
lines
302+
|> Array.tryFindIndex (fun (l: string) -> l.StartsWith(sectionHeader))
303+
|> Option.map (fun startIdx ->
304+
lines
305+
|> Array.skip startIdx
306+
|> Array.take (min 10 (lines.Length - startIdx)) // Look in next 10 lines
307+
|> Array.exists (fun l -> l.Contains("artifacts")))
308+
|> Option.defaultValue false
309+
110310
if expectedLibIsLocal then
111-
Assert.True((lines |> Array.exists (fun l -> l.Contains("Library") && l.Contains("artifacts"))),
311+
Assert.True(hasArtifactsInSection "Library Build Info:",
112312
"Local library build should reference artifacts path")
113313
if expectedAppIsLocal then
114-
Assert.True((lines |> Array.exists (fun l -> l.Contains("Application") && l.Contains("artifacts"))),
314+
Assert.True(hasArtifactsInSection "Application Build Info:",
115315
"Local app build should reference artifacts path")
116316

117317
// Ensure build verification section is present

tests/projects/CompilerCompat/CompilerCompatApp/CompilerCompatApp.fsproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
55
<TargetFramework>net8.0</TargetFramework>
6-
<DirectoryBuildPropsPath Condition="'$(LoadLocalFSharpBuild)' == 'True'">../../../../UseLocalCompiler.Directory.Build.props</DirectoryBuildPropsPath>
7-
<DirectoryBuildPropsPath Condition="'$(LoadLocalFSharpBuild)' != 'True'"></DirectoryBuildPropsPath>
86
</PropertyGroup>
97

108
<ItemGroup>
@@ -13,7 +11,9 @@
1311
</ItemGroup>
1412

1513
<ItemGroup>
16-
<ProjectReference Include="../CompilerCompatLib/CompilerCompatLib.fsproj" />
14+
<!-- Reference the library as a NuGet package instead of a project reference -->
15+
<!-- This allows testing cross-compiler scenarios where lib and app use different compilers -->
16+
<PackageReference Include="CompilerCompatLib" Version="1.0.0" />
1717
</ItemGroup>
1818

1919
<Target Name="GenerateAppBuildInfo" BeforeTargets="BeforeCompile">

tests/projects/CompilerCompat/CompilerCompatApp/Program.fs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,19 @@ open System
55
[<EntryPoint>]
66
let main _argv =
77
try
8+
// Helper to get the actual compiler path (prefer dotnetFscCompilerPath when using local build)
9+
let getActualCompilerPath (dotnetPath: string) (fallbackPath: string) =
10+
if dotnetPath <> "N/A" && dotnetPath <> "" then dotnetPath else fallbackPath
11+
812
// Print detailed build information to verify which compiler was used
913
printfn "=== BUILD VERIFICATION ==="
1014
printfn "Library Build Info:"
1115
printfn " SDK Version: %s" LibBuildInfo.sdkVersion
12-
printfn " F# Compiler Path: %s" LibBuildInfo.fsharpCompilerPath
13-
printfn " .NET FSC Compiler Path: %s" LibBuildInfo.dotnetFscCompilerPath
16+
printfn " F# Compiler Path: %s" (getActualCompilerPath LibBuildInfo.dotnetFscCompilerPath LibBuildInfo.fsharpCompilerPath)
1417
printfn " Is Local Build: %b" LibBuildInfo.isLocalBuild
1518
printfn "Application Build Info:"
1619
printfn " SDK Version: %s" AppBuildInfo.sdkVersion
17-
printfn " F# Compiler Path: %s" AppBuildInfo.fsharpCompilerPath
18-
printfn " .NET FSC Compiler Path: %s" AppBuildInfo.dotnetFscCompilerPath
20+
printfn " F# Compiler Path: %s" (getActualCompilerPath AppBuildInfo.dotnetFscCompilerPath AppBuildInfo.fsharpCompilerPath)
1921
printfn " Is Local Build: %b" AppBuildInfo.isLocalBuild
2022
printfn "=========================="
2123

tests/projects/CompilerCompat/CompilerCompatLib/CompilerCompatLib.fsproj

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,29 @@
33
<PropertyGroup>
44
<TargetFramework>net8.0</TargetFramework>
55
<GenerateDocumentationFile>true</GenerateDocumentationFile>
6-
<DirectoryBuildPropsPath Condition="'$(LoadLocalFSharpBuild)' == 'True'">../../../../UseLocalCompiler.Directory.Build.props</DirectoryBuildPropsPath>
7-
<DirectoryBuildPropsPath Condition="'$(LoadLocalFSharpBuild)' != 'True'"></DirectoryBuildPropsPath>
6+
7+
<!-- Package properties for NuGet pack -->
8+
<IsPackable>true</IsPackable>
9+
<PackageId>CompilerCompatLib</PackageId>
10+
<Version>1.0.0</Version>
11+
<Authors>Test</Authors>
12+
<Description>Test library for compiler compatibility tests</Description>
813
</PropertyGroup>
914

1015
<ItemGroup>
1116
<Compile Include="LibBuildInfo.fs" />
1217
<Compile Include="Library.fs" />
1318
</ItemGroup>
1419

20+
<!-- When building with local compiler, include FSharp.Core in the package so consumers can find it -->
21+
<ItemGroup Condition="'$(LoadLocalFSharpBuild)' == 'True'">
22+
<Content Include="$(LocalFSharpCompilerPath)/artifacts/bin/FSharp.Core/$(LocalFSharpCompilerConfiguration)/netstandard2.0/FSharp.Core.dll">
23+
<PackagePath>lib/$(TargetFramework)</PackagePath>
24+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
25+
<Pack>true</Pack>
26+
</Content>
27+
</ItemGroup>
28+
1529
<Target Name="GenerateLibBuildInfo" BeforeTargets="BeforeCompile">
1630
<PropertyGroup>
1731
<IsLocalBuildValue Condition="'$(LoadLocalFSharpBuild)' == 'True'">true</IsLocalBuildValue>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<Project>
2+
<!--
3+
Isolated Directory.Build.props for CompilerCompat test projects.
4+
Prevents import of repository root's Directory.Build.props for isolated build environments.
5+
-->
6+
7+
<PropertyGroup>
8+
<CompilerCompatTestProject>true</CompilerCompatTestProject>
9+
</PropertyGroup>
10+
</Project>

0 commit comments

Comments
 (0)