From 33e8be6361e01516e758fd723dba99faed12d685 Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Tue, 30 Jan 2024 17:17:56 +0000 Subject: [PATCH] Run TransparentCompiler unit tests with local response files. (#16609) --- .../FSharpChecker/TransparentCompiler.fs | 45 +++++++- .../ProjectGeneration.fs | 100 ++++++++++++++++-- 2 files changed, 134 insertions(+), 11 deletions(-) diff --git a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs index 60c7e8be881..90dc85c4a33 100644 --- a/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs +++ b/tests/FSharp.Compiler.ComponentTests/FSharpChecker/TransparentCompiler.fs @@ -494,7 +494,7 @@ let fuzzingTest seed (project: SyntheticProject) = task { let checker = builder.Checker // Force creation and caching of options - do! SaveAndCheckProject project checker |> Async.Ignore + do! SaveAndCheckProject project checker false |> Async.Ignore let projectAgent = MailboxProcessor.Start(fun (inbox: MailboxProcessor) -> let rec loop project = @@ -800,4 +800,45 @@ module Stuff = //Assert.Equal(hash, hash2) - () \ No newline at end of file + () + +/// Update these paths to a local response file with compiler arguments of existing F# projects. +/// References projects are expected to have been built. +let localResponseFiles = + [| + @"C:\Projects\fantomas\src\Fantomas.Core.Tests\Fantomas.Core.Tests.rsp" + |] + |> Array.collect (fun f -> + [| + [| true :> obj; f:> obj |] + [| false :> obj; f :> obj|] + |] + ) + +// Uncomment this attribute if you want run this test against local response files. +// [] +[] +let ``TypeCheck last file in project with transparent compiler`` useTransparentCompiler responseFile = + let responseFile = FileInfo responseFile + let syntheticProject = mkSyntheticProjectForResponseFile responseFile + + let workflow = + ProjectWorkflowBuilder( + syntheticProject, + isExistingProject = true, + useTransparentCompiler = useTransparentCompiler + ) + + let lastFile = + syntheticProject.SourceFiles + |> List.tryLast + |> Option.map (fun sf -> sf.Id) + + match lastFile with + | None -> failwithf "Last file of project could not be found" + | Some lastFile -> + + workflow { + clearCache + checkFile lastFile expectOk + } diff --git a/tests/FSharp.Test.Utilities/ProjectGeneration.fs b/tests/FSharp.Test.Utilities/ProjectGeneration.fs index 54c00ebe544..22dcc12c184 100644 --- a/tests/FSharp.Test.Utilities/ProjectGeneration.fs +++ b/tests/FSharp.Test.Utilities/ProjectGeneration.fs @@ -193,9 +193,13 @@ type SyntheticSourceFile = Source: string ExtraSource: string EntryPoint: bool + /// Indicates whether this is an existing F# file on disk. + IsPhysicalFile: bool } - member this.FileName = $"File{this.Id}.fs" + member this.FileName = + if this.IsPhysicalFile then $"%s{this.Id}.fs" else $"File%s{this.Id}.fs" + member this.SignatureFileName = $"{this.FileName}i" member this.TypeName = $"T{this.Id}V_{this.PublicVersion}" member this.ModuleName = $"Module{this.Id}" @@ -216,7 +220,8 @@ let sourceFile fileId deps = HasErrors = false Source = "" ExtraSource = "" - EntryPoint = false } + EntryPoint = false + IsPhysicalFile = false } let OptionsCache = ConcurrentDictionary() @@ -468,6 +473,76 @@ let private writeFile (p: SyntheticProject) (f: SyntheticSourceFile) = let content = renderSourceFile p f writeFileIfChanged fileName content +/// Creates a SyntheticProject from the compiler arguments found in the response file. +let mkSyntheticProjectForResponseFile (responseFile: FileInfo) : SyntheticProject = + if not responseFile.Exists then + failwith $"%s{responseFile.FullName} does not exist" + + let compilerArgs = File.ReadAllLines responseFile.FullName + + let fsharpFileExtensions = set [| ".fs" ; ".fsi" ; ".fsx" |] + + let isFSharpFile (file : string) = + Set.exists (fun (ext : string) -> file.EndsWith (ext, StringComparison.Ordinal)) fsharpFileExtensions + + let fsharpFiles = + compilerArgs + |> Array.choose (fun (line : string) -> + if not (isFSharpFile line) then + None + else + + let fullPath = Path.Combine (responseFile.DirectoryName, line) + if not (File.Exists fullPath) then + None + else + Some fullPath + ) + |> Array.toList + + let signatureFiles, implementationFiles = + fsharpFiles |> List.partition (fun path -> path.EndsWith ".fsi") + + let signatureFiles = set signatureFiles + + let sourceFiles = + implementationFiles + |> List.map (fun implPath -> + let id = + let fileNameWithoutExtension = Path.GetFileNameWithoutExtension implPath + let directoryOfFile = FileInfo(implPath).DirectoryName + let relativeUri = Uri(responseFile.FullName).MakeRelativeUri(Uri(directoryOfFile)) + let relativeFolderPath = Uri.UnescapeDataString(relativeUri.ToString()).Replace('/', Path.DirectorySeparatorChar) + Path.Combine(relativeFolderPath, fileNameWithoutExtension) + + { + Id = id + PublicVersion = 1 + InternalVersion = 1 + DependsOn = [] + FunctionName = "f" + SignatureFile = + let sigPath = $"%s{implPath}i" in + if signatureFiles.Contains sigPath then Custom(File.ReadAllText sigPath) else No + HasErrors = false + Source = File.ReadAllText implPath + ExtraSource = "" + EntryPoint = false + IsPhysicalFile = true + } + ) + + let otherOptions = + compilerArgs + |> Array.filter (fun line -> not (isFSharpFile line)) + |> Array.toList + + { SyntheticProject.Create(Path.GetFileNameWithoutExtension responseFile.Name) with + ProjectDir = responseFile.DirectoryName + SourceFiles = sourceFiles + OtherOptions = otherOptions + AutoAddModules = false + } [] module ProjectOperations = @@ -792,12 +867,14 @@ type WorkflowContext = Signatures: Map Cursor: FSharpSymbolUse option } -let SaveAndCheckProject project checker = +let SaveAndCheckProject project checker isExistingProject = async { use _ = Activity.start "SaveAndCheckProject" [ Activity.Tags.project, project.Name ] - do! saveProject project true checker + // Don't save the project if it is a real world project that exists on disk. + if not isExistingProject then + do! saveProject project true checker let options = project.GetProjectOptions checker let! snapshot = FSharpProjectSnapshot.FromOptions(options, getFileSnapshot project) @@ -834,13 +911,15 @@ type ProjectWorkflowBuilder ?useSyntaxTreeCache, ?useTransparentCompiler, ?runTimeout, - ?autoStart + ?autoStart, + ?isExistingProject ) = let useTransparentCompiler = defaultArg useTransparentCompiler FSharp.Compiler.CompilerConfig.FSharpExperimentalFeaturesEnabledAutomatically let useGetSource = not useTransparentCompiler && defaultArg useGetSource false let useChangeNotifications = not useTransparentCompiler && defaultArg useChangeNotifications false let autoStart = defaultArg autoStart true + let isExistingProject = defaultArg isExistingProject false let mutable latestProject = initialProject let mutable activity = None @@ -874,7 +953,7 @@ type ProjectWorkflowBuilder let getInitialContext() = match initialContext with | Some ctx -> async.Return ctx - | None -> SaveAndCheckProject initialProject checker + | None -> SaveAndCheckProject initialProject checker isExistingProject /// Creates a ProjectWorkflowBuilder which will already have the project /// saved and checked so time won't be spent on that. @@ -915,7 +994,7 @@ type ProjectWorkflowBuilder try Async.RunSynchronously(workflow, timeout = defaultArg runTimeout 600_000) finally - if initialContext.IsNone then + if initialContext.IsNone && not isExistingProject then this.DeleteProjectDir() activity |> Option.iter (fun x -> x.Dispose()) tracerProvider |> Option.iter (fun x -> @@ -1021,10 +1100,13 @@ type ProjectWorkflowBuilder async { let! ctx = workflow - use _ = + use activity = Activity.start "ProjectWorkflowBuilder.CheckFile" [ Activity.Tags.project, initialProject.Name; "fileId", fileId ] - let! results = checkFile fileId ctx.Project checker + let! results = + checkFile fileId ctx.Project checker + + activity.Dispose() let oldSignature = ctx.Signatures[fileId] let newSignature = getSignature results