@@ -2,6 +2,7 @@ module FSharp.Compiler.ComponentTests.CompilerCompatibilityTests
22
33
44open System.IO
5+ open System.Diagnostics
56open Xunit
67open 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}:\n Command: dotnet {command}\n Stdout:\n {stdout}\n Stderr:\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
0 commit comments