From 3c6fec96533109f88010db06464380c4bebc8b2f Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sun, 21 Apr 2024 14:10:41 -0500 Subject: [PATCH] Allow loading of projects that are missing imports. (#206) ## The Problem To load project files with missing Imports, we need to supply one or two ProjectLoadSettings flags to everywhere a project file could be parsed. These flags are `ProjectLoadSettings.IgnoreMissingImports` and `ProjectLoadSettings.IgnoreInvalidImports`. Ideally we need to find everywhere that a project would be loaded (so anywhere `Project(...)` or `ProjectInstance(...)` occur) and make sure these flags are applied. ## Phase 1 - protecting the main constructors For our purposes there are two main places that `ProjectLoadSettings` can be specified * when the `Project` constructor is called in the `WorkspaceLoader` * as part of the `buildParameters` for the `WorkspaceLoaderViaProjectGraph` Applying these changes to the existing call-sites was fairly easy - there were something like 6 places that Projects/Project Instances were directly created. Here we come to our first hurdle - the `ProjectInstance` constructor doesn't surface `ProjectLoadSettings` in any way. So step one was to turn any sort of `ProjectInstance` creation into a two-phase operation: * create the `Project` with appropriate load settings * create the `ProjectInstance` from that `Project` This got us most of the way through the tests. ## Phase 2 - TFM detection With one hurdle - the way we detected the TFM for a project involved loading a `ProjectInstance` directly and reading properties from it, and this turned out to be error prone because of the reasons mentioned above - `ProjectInstance` doesn't have `ProjectLoadSettings`. So we needed to create a `Project` to get the `ProjectInstance` from, as described above. ## Phase 3 - ProjectCollection management The above fix worked for more tests, but others still failed because the 'same project' was being created (and implicitly assigned to the global `ProjectCollection`) multiple times - a big no-no for `ProjectCollections`. So this required two changes: * create and manage a `ProjectCollection` as a 'container' for a given call to `LoadProjects` - this allows us to cache the evaluations and design-time builds over the course of a single call to `LoadProjects` while also not cluttering/clobbering the global default `ProjectCollection` * implement a function that safely retrieves an existing `Project` from the `ProjectCollection` if one exists for the same project path + global properties - if not, a new `Project` is created. With these two changes, all the tests (including the new tests) became green. --- .github/workflows/build.yml | 6 - .vscode/launch.json | 1 - src/Ionide.ProjInfo/Library.fs | 273 +++++++++++++----- test/Ionide.ProjInfo.Tests/TestAssets.fs | 12 +- test/Ionide.ProjInfo.Tests/Tests.fs | 74 ++++- test/examples/missing-import/Program.fs | 3 + .../missing-import/missing-import.fsproj | 14 + 7 files changed, 289 insertions(+), 94 deletions(-) create mode 100644 test/examples/missing-import/Program.fs create mode 100644 test/examples/missing-import/missing-import.fsproj diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 27c48cb3..6b63052f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -74,12 +74,6 @@ jobs: env: BuildNet7: ${{ matrix.build_net7 }} - - name: Upload NuGet packages - uses: actions/upload-artifact@v2 - with: - name: packages - path: src/**/*.nupkg - - name: Archive test results uses: actions/upload-artifact@v3 diff --git a/.vscode/launch.json b/.vscode/launch.json index a8f09603..099a5617 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -39,7 +39,6 @@ "name": "Ionide.ProjInfo.Tests", "type": "coreclr", "request": "launch", - "preLaunchTask": "build", "program": "${workspaceFolder}/test/Ionide.ProjInfo.Tests/bin/Debug/${input:tfm}/Ionide.ProjInfo.Tests.dll", "args": [ "--filter", diff --git a/src/Ionide.ProjInfo/Library.fs b/src/Ionide.ProjInfo/Library.fs index 96806644..ad683f7b 100644 --- a/src/Ionide.ProjInfo/Library.fs +++ b/src/Ionide.ProjInfo/Library.fs @@ -331,16 +331,10 @@ module ProjectLoader = type LoadedProject = internal LoadedProject of ProjectInstance - [] - type ProjectLoadingStatus = - private - | Success of LoadedProject - | Error of string - - let internal msBuildLogger = lazy (LogProvider.getLoggerByName "MsBuild") //lazy because dotnet test wont pickup our logger otherwise + let internal projectLoaderLogger = lazy (LogProvider.getLoggerByName "ProjectLoader") let msBuildToLogProvider () = - let msBuildLogger = msBuildLogger.Value + let msBuildLogger = LogProvider.getLoggerByName "MsBuild" { new ILogger with member this.Initialize(eventSource: IEventSource) : unit = @@ -373,7 +367,6 @@ module ProjectLoader = and set (v: LoggerVerbosity): unit = () } - let internal stringWriterLogger (writer: StringWriter) = { new ILogger with member this.Initialize(eventSource: IEventSource) : unit = @@ -392,9 +385,99 @@ module ProjectLoader = with set (v: LoggerVerbosity): unit = () } - let getTfm (path: string) readingProps isLegacyFrameworkProj = - let pi = ProjectInstance(path, globalProperties = readingProps, toolsVersion = null) + let mergeGlobalProperties (collection: ProjectCollection) (otherProperties: IDictionary) = + let combined = Dictionary(collection.GlobalProperties) + + for kvp in otherProperties do + combined.Add(kvp.Key, kvp.Value) + + combined + + type ProjectAlreadyLoaded(projectPath: string, collection: ProjectCollection, properties: IDictionary, innerException) = + inherit System.Exception("", innerException) + let mutable _message = null + + override this.Message = + match _message with + | null -> + // if the project is already loaded throw a nicer message + let message = System.Text.StringBuilder() + + message + .AppendLine($"The project '{projectPath}' already exists in the project collection with the same global properties.") + .AppendLine("The global properties requested were:") + |> ignore + + for (KeyValue(k, v)) in properties do + message.AppendLine($" {k} = {v}") + |> ignore + + message.AppendLine() + |> ignore + message.AppendLine("There are projects of the following properties already in the collection:") + |> ignore + + for project in collection.GetLoadedProjects(projectPath) do + message.AppendLine($"Evaluation #{project.LastEvaluationId}") + |> ignore + + for (KeyValue(k, v)) in project.GlobalProperties do + message.AppendLine($" {k} = {v}") + |> ignore + + message.AppendLine() + |> ignore + + _message <- message.ToString() + | _ -> () + + _message + + // it's _super_ important that the 'same' project (path + properties) is only created once in a project collection, so we have to check on this here + let findOrCreateMatchingProject path (collection: ProjectCollection) globalProps = + let createNewProject properties = + try + Project( + projectFile = path, + projectCollection = collection, + globalProperties = properties, + toolsVersion = null, + loadSettings = + (ProjectLoadSettings.IgnoreMissingImports + ||| ProjectLoadSettings.IgnoreInvalidImports) + ) + with :? System.InvalidOperationException as ex -> + raise (ProjectAlreadyLoaded(path, collection, properties, ex)) + + let hasSameGlobalProperties (globalProps: IDictionary) (incomingProject: Project) = + if + incomingProject.GlobalProperties.Count + <> globalProps.Count + then + false + else + globalProps + |> Seq.forall (fun (KeyValue(k, v)) -> + incomingProject.GlobalProperties.ContainsKey k + && incomingProject.GlobalProperties.[k] = v + ) + + lock + (collection) + (fun _ -> + match collection.GetLoadedProjects(path) with + | null -> createNewProject globalProps + | existingProjects when existingProjects.Count = 0 -> createNewProject globalProps + | existingProjects -> + let totalGlobalProps = mergeGlobalProperties collection globalProps + + existingProjects + |> Seq.tryFind (hasSameGlobalProperties totalGlobalProps) + |> Option.defaultWith (fun _ -> createNewProject globalProps) + ) + + let getTfm (pi: ProjectInstance) isLegacyFrameworkProj = let tfm = pi.GetPropertyValue( if isLegacyFrameworkProj then @@ -415,6 +498,11 @@ module ProjectLoader = else Some tfm + let loadProjectAndGetTFM (path: string) projectCollection readingProps isLegacyFrameworkProj = + let project = findOrCreateMatchingProject path projectCollection readingProps + let pi = project.CreateProjectInstance() + getTfm pi isLegacyFrameworkProj + let createLoggers (paths: string seq) (binaryLogs: BinaryLogGeneration) (sw: StringWriter) = let swLogger = stringWriterLogger (sw) let msBuildLogger = msBuildToLogProvider () @@ -443,8 +531,8 @@ module ProjectLoader = yield! loggers ] - let getGlobalProps (path: string) (tfm: string option) (globalProperties: (string * string) list) = - dict [ + let getGlobalProps (tfm: string option) (globalProperties: (string * string) list) (propsSetFromParentCollection: Set) = + [ "ProvideCommandLineArgs", "true" "DesignTimeBuild", "true" "SkipCompilerExecution", "true" @@ -459,6 +547,9 @@ module ProjectLoader = "DotnetProjInfo", "true" yield! globalProperties ] + |> List.filter (fun (ourProp, _) -> not (propsSetFromParentCollection.Contains ourProp)) + |> dict + /// /// These are a list of build targets that are run during a design-time build (mostly). @@ -505,7 +596,7 @@ module ProjectLoader = Init.setupForLegacyFramework msbuildBinaryDir | _ -> () - let loadProject (path: string) (binaryLogs: BinaryLogGeneration) globalProperties = + let loadProject (path: string) (binaryLogs: BinaryLogGeneration) (projectCollection: ProjectCollection) = try let isLegacyFrameworkProjFile = if @@ -520,33 +611,39 @@ module ProjectLoader = else false - let readingProps = getGlobalProps path None globalProperties + let collectionProps = + projectCollection.GlobalProperties.Keys + |> Set.ofSeq + + let readingProps = getGlobalProps None [] collectionProps if isLegacyFrameworkProjFile then setLegacyMsbuildProperties isLegacyFrameworkProjFile - let tfm = getTfm path readingProps isLegacyFrameworkProjFile - - let globalProperties = getGlobalProps path tfm globalProperties - - use pc = new ProjectCollection(globalProperties) - - let pi = pc.LoadProject(path, globalProperties, toolsVersion = null) + let tfm = loadProjectAndGetTFM path projectCollection readingProps isLegacyFrameworkProjFile + let globalProperties = getGlobalProps tfm [] collectionProps + let project = findOrCreateMatchingProject path projectCollection globalProperties use sw = new StringWriter() let loggers = createLoggers [ path ] binaryLogs sw - let pi = pi.CreateProjectInstance() + let pi = project.CreateProjectInstance() let build = pi.Build(designTimeBuildTargets isLegacyFrameworkProjFile, loggers) if build then - ProjectLoadingStatus.Success(LoadedProject pi) + Ok(LoadedProject pi) else - ProjectLoadingStatus.Error(sw.ToString()) + Error(sw.ToString()) with exc -> - ProjectLoadingStatus.Error(exc.Message) + projectLoaderLogger.Value.error ( + Log.setMessage "Generic error while loading project {path}" + >> Log.addExn exc + >> Log.addContextDestructured "path" path + ) + + Error(exc.Message) let getFscArgs (LoadedProject project) = project.Items @@ -766,11 +863,21 @@ module ProjectLoader = LoadTime = DateTime.Now TargetPath = props - |> Seq.tryPick (fun n -> if n.Name = "TargetPath" then Some n.Value else None) + |> Seq.tryPick (fun n -> + if n.Name = "TargetPath" then + Some n.Value + else + None + ) |> Option.defaultValue "" TargetRefPath = props - |> Seq.tryPick (fun n -> if n.Name = "TargetRefPath" then Some n.Value else None) + |> Seq.tryPick (fun n -> + if n.Name = "TargetRefPath" then + Some n.Value + else + None + ) ProjectOutputType = outputType ProjectSdkInfo = sdkInfo Items = compileItems @@ -833,21 +940,6 @@ module ProjectLoader = Result.Ok proj - /// - /// Main entry point for project loading. - /// - /// Full path to the `.fsproj` file - /// describes if and how to generate MsBuild binary logs - /// The global properties to use (e.g. Configuration=Release). Some additional global properties are pre-set by the tool - /// List of additional MsBuild properties that you want to obtain. - /// Returns the record instance representing the loaded project or string containing error message - let getProjectInfo (path: string) (globalProperties: (string * string) list) (binaryLogs: BinaryLogGeneration) (customProperties: string list) : Result = - let loadedProject = loadProject path binaryLogs globalProperties - - match loadedProject with - | ProjectLoadingStatus.Success project -> getLoadedProjectInfo path customProperties project - | ProjectLoadingStatus.Error e -> Result.Error e - /// A type that turns project files or solution files into deconstructed options. /// Use this in conjunction with the other ProjInfo libraries to turn these options into /// ones compatible for use with FCS directly. @@ -894,10 +986,20 @@ module WorkspaceLoaderViaProjectGraph = type WorkspaceLoaderViaProjectGraph private (toolsPath, ?globalProperties: (string * string) list) = let (ToolsPath toolsPath) = toolsPath - let globalProperties = defaultArg globalProperties [] let logger = LogProvider.getLoggerFor () let loadingNotification = new Event() + let projectCollection () = + new ProjectCollection( + globalProperties = dict (defaultArg globalProperties []), + loggers = null, + remoteLoggers = null, + toolsetDefinitionLocations = ToolsetDefinitionLocations.Local, + loadProjectsReadOnly = true, + maxNodeCount = Environment.ProcessorCount, + onlyLogCriticalEvents = false + ) + let handleProjectGraphFailures f = try f () @@ -915,16 +1017,29 @@ type WorkspaceLoaderViaProjectGraph private (toolsPath, ?globalProperties: (stri None - let projectInstanceFactory projectPath (_globalProperties: IDictionary) (projectCollection: ProjectCollection) = - let tfm = ProjectLoader.getTfm projectPath (dict globalProperties) false - //let globalProperties = globalProperties |> Seq.toList |> List.map (fun (KeyValue(k,v)) -> (k,v)) - let globalProperties = ProjectLoader.getGlobalProps projectPath tfm globalProperties - ProjectInstance(projectPath, globalProperties, toolsVersion = null, projectCollection = projectCollection) + let projectInstanceFactory projectPath (globalProperties: IDictionary) (projectCollection: ProjectCollection) = + let loadedProject = ProjectLoader.findOrCreateMatchingProject projectPath projectCollection globalProperties + + let projInstance = loadedProject.CreateProjectInstance() + let tfm = ProjectLoader.getTfm projInstance false + + let ourGlobalProperties = + ProjectLoader.getGlobalProps + tfm + [] + (globalProperties.Keys + |> Set.ofSeq + |> Set.union (Set.ofSeq projectCollection.GlobalProperties.Keys)) + + let tfm_specific_project = ProjectLoader.findOrCreateMatchingProject projectPath projectCollection ourGlobalProperties + tfm_specific_project.CreateProjectInstance() let projectGraphProjects (paths: string seq) = handleProjectGraphFailures <| fun () -> + use per_request_collection = projectCollection () + paths |> Seq.iter (fun p -> loadingNotification.Trigger(WorkspaceProjectState.Loading p)) @@ -935,7 +1050,7 @@ type WorkspaceLoaderViaProjectGraph private (toolsPath, ?globalProperties: (stri with | [ x ] -> let g: ProjectGraph = - ProjectGraph(x, projectCollection = ProjectCollection.GlobalProjectCollection, projectInstanceFactory = projectInstanceFactory) + ProjectGraph(x, projectCollection = per_request_collection, projectInstanceFactory = projectInstanceFactory) // When giving ProjectGraph a singular project, g.EntryPointNodes only contains that project. // To get it to build the Graph with all the dependencies we need to look at all the ProjectNodes // and tell the graph to use all as potentially an entrypoint @@ -943,7 +1058,7 @@ type WorkspaceLoaderViaProjectGraph private (toolsPath, ?globalProperties: (stri g.ProjectNodes |> Seq.map (fun pn -> ProjectGraphEntryPoint pn.ProjectInstance.FullPath) - ProjectGraph(nodes, projectCollection = ProjectCollection.GlobalProjectCollection, projectInstanceFactory = projectInstanceFactory) + ProjectGraph(nodes, projectCollection = per_request_collection, projectInstanceFactory = projectInstanceFactory) | xs -> let entryPoints = @@ -951,7 +1066,7 @@ type WorkspaceLoaderViaProjectGraph private (toolsPath, ?globalProperties: (stri |> Seq.map ProjectGraphEntryPoint |> List.ofSeq - ProjectGraph(entryPoints, projectCollection = ProjectCollection.GlobalProjectCollection, projectInstanceFactory = projectInstanceFactory) + ProjectGraph(entryPoints, projectCollection = per_request_collection, projectInstanceFactory = projectInstanceFactory) graph @@ -1030,6 +1145,8 @@ type WorkspaceLoaderViaProjectGraph private (toolsPath, ?globalProperties: (stri buildParameters.ProjectLoadSettings <- ProjectLoadSettings.RecordEvaluatedItemElements ||| ProjectLoadSettings.ProfileEvaluation + ||| ProjectLoadSettings.IgnoreMissingImports + ||| ProjectLoadSettings.IgnoreInvalidImports buildParameters.LogInitialPropertiesAndItems <- true bm.BeginBuild(buildParameters) @@ -1160,7 +1277,18 @@ type WorkspaceLoaderViaProjectGraph private (toolsPath, ?globalProperties: (stri WorkspaceLoaderViaProjectGraph(toolsPath, ?globalProperties = globalProperties) :> IWorkspaceLoader type WorkspaceLoader private (toolsPath: ToolsPath, ?globalProperties: (string * string) list) = - let globalProperties = defaultArg globalProperties [] + + let projectCollection () = + new ProjectCollection( + globalProperties = dict (defaultArg globalProperties []), + loggers = null, + remoteLoggers = null, + toolsetDefinitionLocations = ToolsetDefinitionLocations.Local, + maxNodeCount = Environment.ProcessorCount, + onlyLogCriticalEvents = false, + loadProjectsReadOnly = true + ) + let loadingNotification = new Event() interface IWorkspaceLoader with @@ -1170,6 +1298,7 @@ type WorkspaceLoader private (toolsPath: ToolsPath, ?globalProperties: (string * override __.LoadProjects(projects: string list, customProperties, binaryLogs) = let cache = Dictionary() + use per_request_collection = projectCollection () let getAllKnown () = cache @@ -1177,11 +1306,26 @@ type WorkspaceLoader private (toolsPath: ToolsPath, ?globalProperties: (string * |> Seq.toList let rec loadProject p = - let res = ProjectLoader.getProjectInfo p globalProperties binaryLogs customProperties - match res with + match ProjectLoader.loadProject p binaryLogs per_request_collection with + | Error msg when msg.Contains "The project file could not be loaded." -> + loadingNotification.Trigger(WorkspaceProjectState.Failed(p, ProjectNotFound(p))) + [], None + | Error msg when msg.Contains "not restored" -> + loadingNotification.Trigger(WorkspaceProjectState.Failed(p, ProjectNotRestored(p))) + [], None + | Error msg when msg.Contains "The operation cannot be completed because a build is already in progress." -> + //Try to load project again + Threading.Thread.Sleep(50) + loadProject p + | Error msg -> + loadingNotification.Trigger(WorkspaceProjectState.Failed(p, GenericError(p, msg))) + [], None | Ok project -> - try + let mappedProjectInfo = ProjectLoader.getLoadedProjectInfo p customProperties project + + match mappedProjectInfo with + | Ok project -> cache.Add(p, project) let lst = @@ -1196,22 +1340,9 @@ type WorkspaceLoader private (toolsPath: ToolsPath, ?globalProperties: (string * let info = Some project lst, info - with exc -> - loadingNotification.Trigger(WorkspaceProjectState.Failed(p, GenericError(p, exc.Message))) + | Error msg -> + loadingNotification.Trigger(WorkspaceProjectState.Failed(p, GenericError(p, msg))) [], None - | Error msg when msg.Contains "The project file could not be loaded." -> - loadingNotification.Trigger(WorkspaceProjectState.Failed(p, ProjectNotFound(p))) - [], None - | Error msg when msg.Contains "not restored" -> - loadingNotification.Trigger(WorkspaceProjectState.Failed(p, ProjectNotRestored(p))) - [], None - | Error msg when msg.Contains "The operation cannot be completed because a build is already in progress." -> - //Try to load project again - Threading.Thread.Sleep(50) - loadProject p - | Error msg -> - loadingNotification.Trigger(WorkspaceProjectState.Failed(p, GenericError(p, msg))) - [], None let rec loadProjectList (projectList: string list) = for p in projectList do diff --git a/test/Ionide.ProjInfo.Tests/TestAssets.fs b/test/Ionide.ProjInfo.Tests/TestAssets.fs index c80e531e..cdcfbaae 100644 --- a/test/Ionide.ProjInfo.Tests/TestAssets.fs +++ b/test/Ionide.ProjInfo.Tests/TestAssets.fs @@ -300,7 +300,13 @@ let ``NetSDK library referencing ProduceReferenceAssembly library`` = { "l2" / "l2.fsproj" TargetFrameworks = Map.ofList [ "netstandard2.0", sourceFiles [ "Library.fs" ] ] - ProjectReferences = [ - ``NetSDK library with ProduceReferenceAssembly`` - ] + ProjectReferences = [ ``NetSDK library with ProduceReferenceAssembly`` ] +} + +let ``Console app with missing direct Import`` = { + ProjDir = "missing-import" + AssemblyName = "missing-import" + ProjectFile = "missing-import.fsproj" + TargetFrameworks = Map.ofList [ "net6.0", sourceFiles [ "Program.fs" ] ] + ProjectReferences = [] } diff --git a/test/Ionide.ProjInfo.Tests/Tests.fs b/test/Ionide.ProjInfo.Tests/Tests.fs index 0a1f1237..24691586 100644 --- a/test/Ionide.ProjInfo.Tests/Tests.fs +++ b/test/Ionide.ProjInfo.Tests/Tests.fs @@ -38,8 +38,7 @@ let pathForProject (test: TestAssetProjInfo) = pathForTestAssets test / test.ProjectFile -let implAssemblyForProject (test: TestAssetProjInfo) = - $"{test.AssemblyName}.dll" +let implAssemblyForProject (test: TestAssetProjInfo) = $"{test.AssemblyName}.dll" let refAssemblyForProject (test: TestAssetProjInfo) = Path.Combine("ref", implAssemblyForProject test) @@ -366,7 +365,7 @@ let testLegacyFrameworkMultiProject toolsPath workspaceLoader isRelease (workspa let testSample2 toolsPath workspaceLoader isRelease (workspaceFactory: ToolsPath * (string * string) list -> IWorkspaceLoader) = testCase |> withLog - (sprintf "can load sample2 - %s - isRelease is %b" workspaceLoader isRelease) + (sprintf "can load sample2 - isRelease is %b - %s" isRelease workspaceLoader) (fun logger fs -> let testDir = inDir fs "load_sample2" copyDirFromAssets fs ``sample2 NetSdk library``.ProjDir testDir @@ -1765,12 +1764,14 @@ let testLoadProject toolsPath = ] |> checkExitCodeZero - let projResult = ProjectLoader.getProjectInfo projPath [] BinaryLogGeneration.Off [] + let collection = new Microsoft.Build.Evaluation.ProjectCollection() - match projResult with - | Result.Ok proj -> Expect.equal proj.ProjectFileName projPath "project file names" + match ProjectLoader.loadProject projPath BinaryLogGeneration.Off collection with | Result.Error err -> failwith $"{err}" - + | Result.Ok proj -> + match ProjectLoader.getLoadedProjectInfo projPath [] proj with + | Result.Ok proj -> Expect.equal proj.ProjectFileName projPath "project file names" + | Result.Error err -> failwith $"{err}" ) let testProjectSystem toolsPath workspaceLoader workspaceFactory = @@ -2067,7 +2068,7 @@ let addFileToProject (projPath: string) fileName = let loadProjfileFromDiskTests toolsPath workspaceLoader (workspaceFactory: ToolsPath -> IWorkspaceLoader) = testCase |> withLog - $"can load project from disk everytime {workspaceLoader}" + $"can load project from disk everytime - {workspaceLoader}" (fun logger fs -> let loader = workspaceFactory toolsPath @@ -2157,14 +2158,17 @@ let referenceAssemblySupportTest toolsPath prefix (workspaceFactory: ToolsPath - |> Seq.toList Expect.hasLength parsed 2 "Should have loaded the F# lib and the referenced F# lib" - let fsharpProject = parsed |> Seq.find (fun p -> Path.GetFileName(p.ProjectFileName) = Path.GetFileName(childProj.ProjectFile)) + + let fsharpProject = + parsed + |> Seq.find (fun p -> Path.GetFileName(p.ProjectFileName) = Path.GetFileName(childProj.ProjectFile)) + let mapped = FCS.mapToFSharpProjectOptions fsharpProject parsed let referencedProjects = mapped.ReferencedProjects Expect.hasLength referencedProjects 1 "Should have a reference to the F# ProjectReference lib" match referencedProjects[0] with - | FSharpReferencedProject.FSharpReference(targetPath, _) -> - Expect.stringContains targetPath (refAssemblyForProject parentProj) "Should have found the ref assembly for the F# lib" + | FSharpReferencedProject.FSharpReference(targetPath, _) -> Expect.stringContains targetPath (refAssemblyForProject parentProj) "Should have found the ref assembly for the F# lib" | _ -> failwith "Should have found a F# reference" ) @@ -2180,6 +2184,47 @@ let testProjectLoadBadData = Expect.isNone proj.Response "should have loaded, detected bad data, and defaulted to empty" ) +let canLoadMissingImports toolsPath loaderType (workspaceFactory: ToolsPath -> IWorkspaceLoader) = + testCase + $"Can load projects with missing Imports - {loaderType}" + (fun () -> + let proj = ``Console app with missing direct Import`` + let projPath = pathForProject proj + + let loader = workspaceFactory toolsPath + let logger = Log.create (sprintf "Test '%s'" $"Can load projects with missing Imports - {loaderType}") + + loader.Notifications.Add( + function + | WorkspaceProjectState.Failed(projPath, errors) -> + logger.error ( + Message.eventX "Failed to load project {project} with {errors}" + >> Message.setField "project" projPath + >> Message.setField "errors" errors + ) + | WorkspaceProjectState.Loading p -> + logger.info ( + Message.eventX "Loading project {project}" + >> Message.setField "project" p + ) + | WorkspaceProjectState.Loaded(p, knownProjects, fromCache) -> + logger.info ( + Message.eventX "Loaded project {project}(fromCache: {fromCache})" + >> Message.setField "project" p + >> Message.setField "fromCache" fromCache + ) + ) + + let parsed = + loader.LoadProjects [ projPath ] + |> Seq.toList + + Expect.equal parsed.Length 1 "Should have loaded the project" + let parsed = parsed[0] + Expect.equal 3 parsed.SourceFiles.Length "Should have Program.fs, AssemblyInfo, and AssemblyAttributes" + Expect.stringEnds parsed.SourceFiles[2] "Program.fs" "Filename should be Program.fs" + ) + let tests toolsPath = let testSample3WorkspaceLoaderExpected = [ ExpectNotification.loading "c1.fsproj" @@ -2297,7 +2342,10 @@ let tests toolsPath = expensiveTests toolsPath WorkspaceLoader.Create csharpLibTest toolsPath WorkspaceLoader.Create - referenceAssemblySupportTest toolsPath (nameof(WorkspaceLoader)) WorkspaceLoader.Create - referenceAssemblySupportTest toolsPath (nameof(WorkspaceLoaderViaProjectGraph)) WorkspaceLoaderViaProjectGraph.Create + referenceAssemblySupportTest toolsPath (nameof (WorkspaceLoader)) WorkspaceLoader.Create + referenceAssemblySupportTest toolsPath (nameof (WorkspaceLoaderViaProjectGraph)) WorkspaceLoaderViaProjectGraph.Create + // tests that cover our ability to handle missing imports + canLoadMissingImports toolsPath (nameof (WorkspaceLoader)) WorkspaceLoader.Create + canLoadMissingImports toolsPath (nameof (WorkspaceLoaderViaProjectGraph)) WorkspaceLoaderViaProjectGraph.Create ] diff --git a/test/examples/missing-import/Program.fs b/test/examples/missing-import/Program.fs new file mode 100644 index 00000000..561c2d4a --- /dev/null +++ b/test/examples/missing-import/Program.fs @@ -0,0 +1,3 @@ +[] +let main args = + 1 diff --git a/test/examples/missing-import/missing-import.fsproj b/test/examples/missing-import/missing-import.fsproj new file mode 100644 index 00000000..90a1e9b2 --- /dev/null +++ b/test/examples/missing-import/missing-import.fsproj @@ -0,0 +1,14 @@ + + + + net6.0 + missing_import + + + + + + + + +