diff --git a/scripts/dotnet-cli-build/PackageTargets.cs b/scripts/dotnet-cli-build/PackageTargets.cs index e524b1166..86dba2985 100644 --- a/scripts/dotnet-cli-build/PackageTargets.cs +++ b/scripts/dotnet-cli-build/PackageTargets.cs @@ -230,11 +230,11 @@ namespace Microsoft.DotNet.Cli.Build var projectFile = Path.Combine(Dirs.RepoRoot, "src", projectName, "project.json"); dotnet.Pack( - projectFile, - "--no-build", - "--build-base-path", packagingBuildBasePath, - "--output", Dirs.PackagesIntermediate, - "--configuration", configuration, + projectFile, + "--no-build", + "--build-base-path", packagingBuildBasePath, + "--output", Dirs.PackagesIntermediate, + "--configuration", configuration, "--version-suffix", versionSuffix) .Execute() .EnsureSuccessful(); diff --git a/src/Microsoft.DotNet.Cli.Utils/DebugHelper.cs b/src/Microsoft.DotNet.Cli.Utils/DebugHelper.cs index 3539759ef..1675e83ab 100644 --- a/src/Microsoft.DotNet.Cli.Utils/DebugHelper.cs +++ b/src/Microsoft.DotNet.Cli.Utils/DebugHelper.cs @@ -19,7 +19,7 @@ namespace Microsoft.DotNet.Cli.Utils } } - private static void WaitForDebugger() + public static void WaitForDebugger() { Console.WriteLine("Waiting for debugger to attach. Press ENTER to continue"); Console.WriteLine($"Process ID: {Process.GetCurrentProcess().Id}"); diff --git a/src/Microsoft.DotNet.ProjectModel.Workspaces/ProjectJsonWorkspace.cs b/src/Microsoft.DotNet.ProjectModel.Workspaces/ProjectJsonWorkspace.cs index c37b3967b..6d3c1da58 100644 --- a/src/Microsoft.DotNet.ProjectModel.Workspaces/ProjectJsonWorkspace.cs +++ b/src/Microsoft.DotNet.ProjectModel.Workspaces/ProjectJsonWorkspace.cs @@ -17,9 +17,11 @@ using Microsoft.DotNet.ProjectModel.Compilation; using Microsoft.DotNet.ProjectModel.Files; using NuGet.Frameworks; +using RoslynWorkspace = Microsoft.CodeAnalysis.Workspace; + namespace Microsoft.DotNet.ProjectModel.Workspaces { - public class ProjectJsonWorkspace : Workspace + public class ProjectJsonWorkspace : RoslynWorkspace { private Dictionary _cache = new Dictionary(); diff --git a/src/Microsoft.DotNet.ProjectModel.Workspaces/WorkspaceProjectContextExtensions.cs b/src/Microsoft.DotNet.ProjectModel.Workspaces/WorkspaceProjectContextExtensions.cs index 615f4fac2..fd54b39c0 100644 --- a/src/Microsoft.DotNet.ProjectModel.Workspaces/WorkspaceProjectContextExtensions.cs +++ b/src/Microsoft.DotNet.ProjectModel.Workspaces/WorkspaceProjectContextExtensions.cs @@ -1,12 +1,9 @@ -using Microsoft.CodeAnalysis; +using RoslynWorkspace = Microsoft.CodeAnalysis.Workspace; namespace Microsoft.DotNet.ProjectModel.Workspaces { public static class WorkspaceProjectContextExtensions { - public static Workspace CreateWorkspace(this ProjectContext context) - { - return new ProjectJsonWorkspace(context); - } + public static RoslynWorkspace CreateRoslynWorkspace(this ProjectContext context) => new ProjectJsonWorkspace(context); } } \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/BuildWorkspace.cs b/src/Microsoft.DotNet.ProjectModel/BuildWorkspace.cs new file mode 100644 index 000000000..4c633d859 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/BuildWorkspace.cs @@ -0,0 +1,81 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.DotNet.ProjectModel +{ + public class BuildWorkspace : Workspace + { + public BuildWorkspace(ProjectReaderSettings settings) : base(settings) { } + + /// + /// Create an empty using the default + /// + /// + public static BuildWorkspace Create() => Create(versionSuffix: string.Empty); + + /// + /// Create an empty using the default , with the specified Version Suffix + /// + /// The suffix to use to replace any '-*' snapshot tokens in Project versions. + /// + public static BuildWorkspace Create(string versionSuffix) + { + var settings = ProjectReaderSettings.ReadFromEnvironment(); + if (!string.IsNullOrEmpty(versionSuffix)) + { + settings.VersionSuffix = versionSuffix; + } + return new BuildWorkspace(settings); + } + + public ProjectContext GetRuntimeContext(ProjectContext context, IEnumerable runtimeIdentifiers) + { + if (!runtimeIdentifiers.Any()) + { + return context; + } + + var contexts = GetProjectContextCollection(context.ProjectDirectory); + if (contexts == null) + { + return null; + } + + var runtimeContext = runtimeIdentifiers + .Select(r => contexts.GetTarget(context.TargetFramework, r)) + .FirstOrDefault(c => c != null); + + if (runtimeContext == null) + { + if (context.IsPortable) + { + // We're specializing a portable target, so synthesize a runtime target manually + // We don't cache this project context, but we'll still use the cached Project and LockFile + return CreateBaseProjectBuilder(context.ProjectFile) + .WithTargetFramework(context.TargetFramework) + .WithRuntimeIdentifiers(runtimeIdentifiers) + .AsDesignTime() + .Build(); + } + + // We are standalone, but don't support this runtime + var rids = string.Join(", ", runtimeIdentifiers); + throw new InvalidOperationException($"Can not find runtime target for framework '{context.TargetFramework}' compatible with one of the target runtimes: '{rids}'. " + + "Possible causes:" + Environment.NewLine + + "1. The project has not been restored or restore failed - run `dotnet restore`" + Environment.NewLine + + $"2. The project does not list one of '{rids}' in the 'runtimes' section."); + } + + return runtimeContext; + } + + protected override IEnumerable BuildProjectContexts(Project project) + { + return CreateBaseProjectBuilder(project).BuildAllTargets(); + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel/DesignTimeWorkspace.cs b/src/Microsoft.DotNet.ProjectModel/DesignTimeWorkspace.cs new file mode 100644 index 000000000..fe6495174 --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/DesignTimeWorkspace.cs @@ -0,0 +1,148 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.DotNet.ProjectModel.Graph; + +namespace Microsoft.DotNet.ProjectModel +{ + public class DesignTimeWorkspace : Workspace + { + private readonly HashSet _projects = new HashSet(StringComparer.OrdinalIgnoreCase); + + private bool _needRefresh; + + public DesignTimeWorkspace(ProjectReaderSettings settings) : base(settings) { } + + public void AddProject(string path) + { + var projectPath = NormalizeProjectPath(path); + + if (projectPath != null) + { + _needRefresh = _projects.Add(path); + } + } + + public void RemoveProject(string path) + { + _needRefresh = _projects.Remove(path); + } + + /// + /// Refresh all cached projects in the Workspace + /// + public void Refresh() + { + if (!_needRefresh) + { + return; + } + + var basePaths = new List(_projects); + _projects.Clear(); + + foreach (var projectDirectory in basePaths) + { + var project = GetProject(projectDirectory); + if (project == null) + { + continue; + } + + _projects.Add(project.ProjectDirectory); + + foreach (var projectContext in GetProjectContextCollection(project.ProjectDirectory).ProjectContexts) + { + foreach (var reference in GetProjectReferences(projectContext)) + { + var referencedProject = GetProject(reference.Path); + if (referencedProject != null) + { + _projects.Add(referencedProject.ProjectDirectory); + } + } + } + } + + _needRefresh = false; + } + + protected override IEnumerable BuildProjectContexts(Project project) + { + foreach (var framework in project.GetTargetFrameworks()) + { + yield return CreateBaseProjectBuilder(project) + .AsDesignTime() + .WithTargetFramework(framework.FrameworkName) + .Build(); + } + } + + private static List ResolveProjectPath(string projectPath) + { + if (File.Exists(projectPath)) + { + var filename = Path.GetFileName(projectPath); + if (!Project.FileName.Equals(filename, StringComparison.OrdinalIgnoreCase) && + !GlobalSettings.FileName.Equals(filename, StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + projectPath = Path.GetDirectoryName(projectPath); + } + + if (File.Exists(Path.Combine(projectPath, Project.FileName))) + { + return new List { projectPath }; + } + + if (File.Exists(Path.Combine(projectPath, GlobalSettings.FileName))) + { + var root = ProjectRootResolver.ResolveRootDirectory(projectPath); + GlobalSettings globalSettings; + if (GlobalSettings.TryGetGlobalSettings(projectPath, out globalSettings)) + { + return globalSettings.ProjectSearchPaths + .Select(searchPath => Path.Combine(globalSettings.DirectoryPath, searchPath)) + .Where(actualPath => Directory.Exists(actualPath)) + .SelectMany(actualPath => Directory.GetDirectories(actualPath)) + .Where(actualPath => File.Exists(Path.Combine(actualPath, Project.FileName))) + .Select(path => Path.GetFullPath(path)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + } + } + + return null; + } + + private static IEnumerable GetProjectReferences(ProjectContext context) + { + var projectDescriptions = context.LibraryManager + .GetLibraries() + .Where(lib => lib.Identity.Type == LibraryType.Project) + .OfType(); + + foreach (var description in projectDescriptions) + { + if (description.Identity.Name == context.ProjectFile.Name) + { + continue; + } + + // if this is an assembly reference then don't threat it as project reference + if (!string.IsNullOrEmpty(description.TargetFrameworkInfo?.AssemblyPath)) + { + continue; + } + + yield return description; + } + } + } +} diff --git a/src/Microsoft.DotNet.ProjectModel/ProjectContext.cs b/src/Microsoft.DotNet.ProjectModel/ProjectContext.cs index cf7ba5a64..31d3f80fd 100644 --- a/src/Microsoft.DotNet.ProjectModel/ProjectContext.cs +++ b/src/Microsoft.DotNet.ProjectModel/ProjectContext.cs @@ -135,7 +135,7 @@ namespace Microsoft.DotNet.ProjectModel yield return new ProjectContextBuilder() .WithProject(project) .WithTargetFramework(framework.FrameworkName) - .WithReaderSettings(settings) + .WithProjectReaderSettings(settings) .WithRuntimeIdentifiers(runtimeIdentifiers ?? Enumerable.Empty()) .Build(); } @@ -149,7 +149,7 @@ namespace Microsoft.DotNet.ProjectModel var project = ProjectReader.GetProject(projectPath); return new ProjectContextBuilder() - .WithReaderSettings(settings) + .WithProjectReaderSettings(settings) .WithProject(project) .BuildAllTargets(); } diff --git a/src/Microsoft.DotNet.ProjectModel/ProjectContextBuilder.cs b/src/Microsoft.DotNet.ProjectModel/ProjectContextBuilder.cs index 581f4a9e8..f750b0cfa 100644 --- a/src/Microsoft.DotNet.ProjectModel/ProjectContextBuilder.cs +++ b/src/Microsoft.DotNet.ProjectModel/ProjectContextBuilder.cs @@ -3,8 +3,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; +using System.Reflection; using System.Text; using Microsoft.DotNet.InternalAbstractions; using Microsoft.DotNet.ProjectModel.Graph; @@ -15,12 +17,13 @@ namespace Microsoft.DotNet.ProjectModel { public class ProjectContextBuilder { + // Note: When adding a property, make sure to add it to Clone below. You'll also need to update the CloneTest in + // Microsoft.DotNet.ProjectModel.Tests.ProjectContextBuilderTests + private Project Project { get; set; } private LockFile LockFile { get; set; } - private GlobalSettings GlobalSettings { get; set; } - private NuGetFramework TargetFramework { get; set; } private IEnumerable RuntimeIdentifiers { get; set; } = Enumerable.Empty(); @@ -39,7 +42,7 @@ namespace Microsoft.DotNet.ProjectModel private Func LockFileResolver { get; set; } - private ProjectReaderSettings Settings { get; set; } = ProjectReaderSettings.ReadFromEnvironment(); + private ProjectReaderSettings ProjectReaderSettings { get; set; } = ProjectReaderSettings.ReadFromEnvironment(); public ProjectContextBuilder() { @@ -47,6 +50,28 @@ namespace Microsoft.DotNet.ProjectModel LockFileResolver = ResolveLockFile; } + public ProjectContextBuilder Clone() + { + var builder = new ProjectContextBuilder() + .WithLockFile(LockFile) + .WithProject(Project) + .WithProjectDirectory(ProjectDirectory) + .WithTargetFramework(TargetFramework) + .WithRuntimeIdentifiers(RuntimeIdentifiers) + .WithReferenceAssembliesPath(ReferenceAssembliesPath) + .WithPackagesDirectory(PackagesDirectory) + .WithRootDirectory(RootDirectory) + .WithProjectResolver(ProjectResolver) + .WithLockFileResolver(LockFileResolver) + .WithProjectReaderSettings(ProjectReaderSettings); + if(IsDesignTime) + { + builder.AsDesignTime(); + } + + return builder; + } + public ProjectContextBuilder WithLockFile(LockFile lockFile) { LockFile = lockFile; @@ -113,9 +138,9 @@ namespace Microsoft.DotNet.ProjectModel return this; } - public ProjectContextBuilder WithReaderSettings(ProjectReaderSettings settings) + public ProjectContextBuilder WithProjectReaderSettings(ProjectReaderSettings projectReaderSettings) { - Settings = settings; + ProjectReaderSettings = projectReaderSettings; return this; } @@ -125,13 +150,20 @@ namespace Microsoft.DotNet.ProjectModel return this; } + /// + /// Produce all targets found in the lock file associated with this builder. + /// Returns an empty enumerable if there is no lock file + /// (making this unsuitable for scenarios where the lock file may not be present, + /// such as at design-time) + /// + /// public IEnumerable BuildAllTargets() { ProjectDirectory = Project?.ProjectDirectory ?? ProjectDirectory; EnsureProjectLoaded(); LockFile = LockFile ?? LockFileResolver(ProjectDirectory); - if (LockFile != null && LockFile.Targets.Any()) + if (LockFile != null) { var deduper = new HashSet(); foreach (var target in LockFile.Targets) @@ -139,15 +171,9 @@ namespace Microsoft.DotNet.ProjectModel var id = $"{target.TargetFramework}/{target.RuntimeIdentifier}"; if (deduper.Add(id)) { - var builder = new ProjectContextBuilder() - .WithProject(Project) - .WithLockFile(LockFile) + var builder = Clone() .WithTargetFramework(target.TargetFramework) .WithRuntimeIdentifiers(new[] { target.RuntimeIdentifier }); - if (IsDesignTime) - { - builder.AsDesignTime(); - } yield return builder.Build(); } @@ -155,8 +181,7 @@ namespace Microsoft.DotNet.ProjectModel } else { - // Build a context for each framework. It won't be fully valid, since it won't have resolved data or runtime data, but the diagnostics will show that - // (Project Model Server needs this) + // Build a context for each framework. It won't be fully valid, since it won't have resolved data or runtime data, but the diagnostics will show that. foreach (var framework in Project.GetTargetFrameworks()) { var builder = new ProjectContextBuilder() @@ -177,19 +202,15 @@ namespace Microsoft.DotNet.ProjectModel ProjectDirectory = Project?.ProjectDirectory ?? ProjectDirectory; - if (GlobalSettings == null && ProjectDirectory != null) + GlobalSettings globalSettings = null; + if (ProjectDirectory != null) { RootDirectory = ProjectRootResolver.ResolveRootDirectory(ProjectDirectory); - - GlobalSettings globalSettings; - if (GlobalSettings.TryGetGlobalSettings(RootDirectory, out globalSettings)) - { - GlobalSettings = globalSettings; - } + GlobalSettings.TryGetGlobalSettings(RootDirectory, out globalSettings); } - RootDirectory = GlobalSettings?.DirectoryPath ?? RootDirectory; - PackagesDirectory = PackagesDirectory ?? PackageDependencyProvider.ResolvePackagesPath(RootDirectory, GlobalSettings); + RootDirectory = globalSettings?.DirectoryPath ?? RootDirectory; + PackagesDirectory = PackagesDirectory ?? PackageDependencyProvider.ResolvePackagesPath(RootDirectory, globalSettings); FrameworkReferenceResolver frameworkReferenceResolver; if (string.IsNullOrEmpty(ReferenceAssembliesPath)) @@ -343,7 +364,7 @@ namespace Microsoft.DotNet.ProjectModel var libraryManager = new LibraryManager(libraries.Values.ToList(), diagnostics, Project?.ProjectFilePath); return new ProjectContext( - GlobalSettings, + globalSettings, mainProject, platformLibrary, TargetFramework, @@ -570,7 +591,7 @@ namespace Microsoft.DotNet.ProjectModel private Project ResolveProject(string projectDirectory) { Project project; - if (ProjectReader.TryGetProject(projectDirectory, out project, settings: Settings)) + if (ProjectReader.TryGetProject(projectDirectory, out project, settings: ProjectReaderSettings)) { return project; } diff --git a/src/Microsoft.DotNet.ProjectModel/ProjectContextCollection.cs b/src/Microsoft.DotNet.ProjectModel/ProjectContextCollection.cs index 2467cb45d..ab5e78239 100644 --- a/src/Microsoft.DotNet.ProjectModel/ProjectContextCollection.cs +++ b/src/Microsoft.DotNet.ProjectModel/ProjectContextCollection.cs @@ -13,8 +13,14 @@ namespace Microsoft.DotNet.ProjectModel { public Project Project { get; set; } + /// + /// Gets all the ProjectContexts in this collection + /// public List ProjectContexts { get; } = new List(); + /// + /// Gets the ProjectContexts in this collection which are not runtime-specific (i.e. the ones used for compilation) + /// public IEnumerable FrameworkOnlyContexts => ProjectContexts.Where(c => string.IsNullOrEmpty(c.RuntimeIdentifier)); public List ProjectDiagnostics { get; } = new List(); diff --git a/src/Microsoft.DotNet.ProjectModel/Workspace.cs b/src/Microsoft.DotNet.ProjectModel/Workspace.cs new file mode 100644 index 000000000..2314eea8f --- /dev/null +++ b/src/Microsoft.DotNet.ProjectModel/Workspace.cs @@ -0,0 +1,300 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.DotNet.ProjectModel.Graph; +using Microsoft.DotNet.ProjectModel.Utilities; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.ProjectModel +{ + /// + /// Represents a cache of Projects, LockFiles, and ProjectContexts + /// + public abstract class Workspace + { + // key: project directory + private readonly ConcurrentDictionary> _projectsCache + = new ConcurrentDictionary>(); + + // key: project directory + private readonly ConcurrentDictionary> _lockFileCache + = new ConcurrentDictionary>(); + + // key: project directory, target framework + private readonly ConcurrentDictionary _projectContextsCache + = new ConcurrentDictionary(); + + private readonly ProjectReaderSettings _settings; + private readonly LockFileReader _lockFileReader; + + protected Workspace(ProjectReaderSettings settings) + { + _settings = settings; + _lockFileReader = new LockFileReader(); + } + + public ProjectContext GetProjectContext(string projectPath, NuGetFramework framework) + { + var contexts = GetProjectContextCollection(projectPath); + if (contexts == null) + { + return null; + } + + return contexts + .ProjectContexts + .FirstOrDefault(c => Equals(c.TargetFramework, framework) && string.IsNullOrEmpty(c.RuntimeIdentifier)); + } + + public ProjectContextCollection GetProjectContextCollection(string projectPath) + { + var normalizedPath = NormalizeProjectPath(projectPath); + if (normalizedPath == null) + { + return null; + } + + return _projectContextsCache.AddOrUpdate( + normalizedPath, + key => AddProjectContextEntry(key, null), + (key, oldEntry) => AddProjectContextEntry(key, oldEntry)); + } + + public Project GetProject(string projectDirectory) => GetProjectCore(projectDirectory)?.Model; + + private LockFile GetLockFile(string projectDirectory) + { + var normalizedPath = NormalizeProjectPath(projectDirectory); + if (normalizedPath == null) + { + return null; + } + + return _lockFileCache.AddOrUpdate( + normalizedPath, + key => AddLockFileEntry(key, null), + (key, oldEntry) => AddLockFileEntry(key, oldEntry)).Model; + } + + + private FileModelEntry GetProjectCore(string projectDirectory) + { + var normalizedPath = NormalizeProjectPath(projectDirectory); + if (normalizedPath == null) + { + return null; + } + + return _projectsCache.AddOrUpdate( + normalizedPath, + key => AddProjectEntry(key, null), + (key, oldEntry) => AddProjectEntry(key, oldEntry)); + } + + + protected static string NormalizeProjectPath(string path) + { + if (File.Exists(path) && + string.Equals(Path.GetFileName(path), Project.FileName, StringComparison.OrdinalIgnoreCase)) + { + return Path.GetDirectoryName(Path.GetFullPath(path)); + } + else if (Directory.Exists(path) && + File.Exists(Path.Combine(path, Project.FileName))) + { + return Path.GetFullPath(path); + } + + return null; + } + + private FileModelEntry AddProjectEntry(string projectDirectory, FileModelEntry currentEntry) + { + if (currentEntry == null) + { + currentEntry = new FileModelEntry(); + } + else if (!File.Exists(Path.Combine(projectDirectory, Project.FileName))) + { + // project was deleted + currentEntry.Reset(); + return currentEntry; + } + + if (currentEntry.IsInvalid) + { + Project project; + if (!ProjectReader.TryGetProject(projectDirectory, out project, _settings)) + { + currentEntry.Reset(); + } + else + { + currentEntry.Diagnostics.AddRange(project.Diagnostics); + currentEntry.Model = project; + currentEntry.FilePath = project.ProjectFilePath; + currentEntry.UpdateLastWriteTimeUtc(); + } + } + + return currentEntry; + } + + private FileModelEntry AddLockFileEntry(string projectDirectory, FileModelEntry currentEntry) + { + if (currentEntry == null) + { + currentEntry = new FileModelEntry(); + } + + if (currentEntry.IsInvalid) + { + currentEntry.Reset(); + + if (!File.Exists(Path.Combine(projectDirectory, LockFile.FileName))) + { + return currentEntry; + } + else + { + currentEntry.FilePath = Path.Combine(projectDirectory, LockFile.FileName); + + using (var fs = ResilientFileStreamOpener.OpenFile(currentEntry.FilePath, retry: 2)) + { + try + { + currentEntry.Model = _lockFileReader.ReadLockFile(currentEntry.FilePath, fs, designTime: true); + currentEntry.UpdateLastWriteTimeUtc(); + } + catch (FileFormatException ex) + { + throw ex.WithFilePath(currentEntry.FilePath); + } + catch (Exception ex) + { + throw FileFormatException.Create(ex, currentEntry.FilePath); + } + } + } + } + + return currentEntry; + } + + private ProjectContextCollection AddProjectContextEntry(string projectDirectory, + ProjectContextCollection currentEntry) + { + if (currentEntry == null) + { + // new entry required + currentEntry = new ProjectContextCollection(); + } + + var projectEntry = GetProjectCore(projectDirectory); + + if (projectEntry?.Model == null) + { + // project doesn't exist anymore + currentEntry.Reset(); + return currentEntry; + } + + var project = projectEntry.Model; + if (currentEntry.HasChanged) + { + currentEntry.Reset(); + + var contexts = BuildProjectContexts(project); + + currentEntry.ProjectContexts.AddRange(contexts); + + currentEntry.Project = project; + currentEntry.ProjectFilePath = project.ProjectFilePath; + currentEntry.LastProjectFileWriteTimeUtc = File.GetLastWriteTimeUtc(currentEntry.ProjectFilePath); + + var lockFilePath = Path.Combine(project.ProjectDirectory, LockFile.FileName); + if (File.Exists(lockFilePath)) + { + currentEntry.LockFilePath = lockFilePath; + currentEntry.LastLockFileWriteTimeUtc = File.GetLastWriteTimeUtc(lockFilePath); + } + + currentEntry.ProjectDiagnostics.AddRange(projectEntry.Diagnostics); + } + + return currentEntry; + } + + protected abstract IEnumerable BuildProjectContexts(Project project); + + /// + /// Creates a ProjectContextBuilder configured to use the Workspace caches. + /// + /// + protected ProjectContextBuilder CreateBaseProjectBuilder() + { + return new ProjectContextBuilder() + .WithProjectReaderSettings(_settings) + .WithProjectResolver(path => GetProjectCore(path)?.Model) + .WithLockFileResolver(path => GetLockFile(path)); + } + + /// + /// Creates a ProjectContextBuilder configured to use the Workspace caches, and the specified root project. + /// + /// The root project + /// + protected ProjectContextBuilder CreateBaseProjectBuilder(Project root) + { + return CreateBaseProjectBuilder().WithProject(root); + } + + protected class FileModelEntry where TModel : class + { + private DateTime _lastWriteTimeUtc; + + public TModel Model { get; set; } + + public string FilePath { get; set; } + + public List Diagnostics { get; } = new List(); + + public void UpdateLastWriteTimeUtc() + { + _lastWriteTimeUtc = File.GetLastWriteTimeUtc(FilePath); + } + + public bool IsInvalid + { + get + { + if (Model == null) + { + return true; + } + + if (!File.Exists(FilePath)) + { + return true; + } + + return _lastWriteTimeUtc < File.GetLastWriteTimeUtc(FilePath); + } + } + + public void Reset() + { + Model = null; + FilePath = null; + Diagnostics.Clear(); + _lastWriteTimeUtc = DateTime.MinValue; + } + } + + } +} diff --git a/src/Microsoft.DotNet.ProjectModel/WorkspaceContext.cs b/src/Microsoft.DotNet.ProjectModel/WorkspaceContext.cs deleted file mode 100644 index 97436bf89..000000000 --- a/src/Microsoft.DotNet.ProjectModel/WorkspaceContext.cs +++ /dev/null @@ -1,507 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Microsoft.DotNet.ProjectModel.Graph; -using Microsoft.DotNet.ProjectModel.Utilities; -using NuGet.Frameworks; - -namespace Microsoft.DotNet.ProjectModel -{ - public class WorkspaceContext - { - // key: project directory - private readonly ConcurrentDictionary> _projectsCache - = new ConcurrentDictionary>(); - - // key: project directory - private readonly ConcurrentDictionary> _lockFileCache - = new ConcurrentDictionary>(); - - // key: project directory, target framework - private readonly ConcurrentDictionary _projectContextsCache - = new ConcurrentDictionary(); - - private readonly HashSet _projects = new HashSet(StringComparer.OrdinalIgnoreCase); - - private bool _needRefresh; - private readonly ProjectReaderSettings _settings; - private readonly bool _designTime; - private readonly LockFileReader _lockFileReader; - - private WorkspaceContext(IEnumerable projectPaths, ProjectReaderSettings settings, bool designTime) - { - _settings = settings; - _designTime = designTime; - _lockFileReader = new LockFileReader(); - - foreach (var path in projectPaths) - { - AddProject(path); - } - - Refresh(); - } - - /// - /// Create a WorkspaceContext from a given path. - /// - /// If the given path points to a global.json, all the projects found under the search paths - /// are added to the WorkspaceContext. - /// - /// If the given path points to a project.json, all the projects it referenced as well as itself - /// are added to the WorkspaceContext. - /// - /// If no path is provided, the workspace context is created empty and projects must be manually added - /// to it using . - /// - public static WorkspaceContext CreateFrom(string projectPath, bool designTime) - { - var projectPaths = ResolveProjectPath(projectPath); - if (projectPaths == null || !projectPaths.Any()) - { - return new WorkspaceContext(Enumerable.Empty(), ProjectReaderSettings.ReadFromEnvironment(), designTime); - } - - var context = new WorkspaceContext(projectPaths, ProjectReaderSettings.ReadFromEnvironment(), designTime); - return context; - } - - /// - /// Create an empty using the default - /// - /// A boolean indicating if the workspace should be created in Design-Time mode - /// - public static WorkspaceContext Create(bool designTime) => Create(ProjectReaderSettings.ReadFromEnvironment(), designTime); - - /// - /// Create an empty using the default , with the specified Version Suffix - /// - /// The suffix to use to replace any '-*' snapshot tokens in Project versions. - /// A boolean indicating if the workspace should be created in Design-Time mode - /// - public static WorkspaceContext Create(string versionSuffix, bool designTime) - { - var settings = ProjectReaderSettings.ReadFromEnvironment(); - if (!string.IsNullOrEmpty(versionSuffix)) - { - settings.VersionSuffix = versionSuffix; - } - return Create(settings, designTime); - } - - /// - /// Create an empty using the provided . - /// - /// The settings to use when reading projects - /// A boolean indicating if the workspace should be created in Design-Time mode - /// - public static WorkspaceContext Create(ProjectReaderSettings settings, bool designTime) - { - return new WorkspaceContext(Enumerable.Empty(), settings, designTime); - } - - public void AddProject(string path) - { - var projectPath = ProjectPathHelper.NormalizeProjectDirectoryPath(path); - - if (projectPath != null) - { - _needRefresh = _projects.Add(path); - } - } - - public void RemoveProject(string path) - { - _needRefresh = _projects.Remove(path); - } - - public IReadOnlyList GetAllProjects() - { - Refresh(); - return _projects.ToList().AsReadOnly(); - } - - /// - /// Refresh the WorkspaceContext to update projects collection - /// - public void Refresh() - { - if (!_needRefresh) - { - return; - } - - var basePaths = new List(_projects); - _projects.Clear(); - - foreach (var projectDirectory in basePaths) - { - var project = GetProjectCore(projectDirectory).Model; - if (project == null) - { - continue; - } - - _projects.Add(project.ProjectDirectory); - - foreach (var projectContext in GetProjectContexts(project.ProjectDirectory)) - { - foreach (var reference in GetProjectReferences(projectContext)) - { - var referencedProject = GetProjectCore(reference.Path).Model; - if (referencedProject != null) - { - _projects.Add(referencedProject.ProjectDirectory); - } - } - } - } - - _needRefresh = false; - } - - public IReadOnlyList GetProjectContexts(string projectPath) - { - return (IReadOnlyList)GetProjectContextCollection(projectPath)?.ProjectContexts.AsReadOnly() ?? - EmptyArray.Value; - } - - public ProjectContext GetProjectContext(string projectPath, NuGetFramework framework) - { - var contexts = GetProjectContextCollection(projectPath); - if (contexts == null) - { - return null; - } - - return contexts - .ProjectContexts - .FirstOrDefault(c => Equals(c.TargetFramework, framework) && string.IsNullOrEmpty(c.RuntimeIdentifier)); - } - - public ProjectContext GetRuntimeContext(ProjectContext context, IEnumerable runtimeIdentifiers) - { - if(!runtimeIdentifiers.Any()) - { - return context; - } - - var contexts = GetProjectContextCollection(context.ProjectDirectory); - if (contexts == null) - { - return null; - } - - var runtimeContext = runtimeIdentifiers - .Select(r => contexts.GetTarget(context.TargetFramework, r)) - .FirstOrDefault(c => c != null); - - if (runtimeContext == null) - { - if (context.IsPortable) - { - // We're specializing a portable target, so synthesize a runtime target manually - // We don't cache this project context, but we'll still use the cached Project and LockFile - return InitializeProjectContextBuilder(context.ProjectFile) - .WithTargetFramework(context.TargetFramework) - .WithRuntimeIdentifiers(runtimeIdentifiers) - .Build(); - } - - // We are standalone, but don't support this runtime - var rids = string.Join(", ", runtimeIdentifiers); - throw new InvalidOperationException($"Can not find runtime target for framework '{context.TargetFramework}' compatible with one of the target runtimes: '{rids}'. " + - "Possible causes:" + Environment.NewLine + - "1. The project has not been restored or restore failed - run `dotnet restore`" + Environment.NewLine + - $"2. The project does not list one of '{rids}' in the 'runtimes' section."); - } - - return runtimeContext; - } - - public Project GetProject(string projectDirectory) => GetProjectCore(projectDirectory)?.Model; - - public ProjectContextCollection GetProjectContextCollection(string projectPath) - { - var normalizedPath = ProjectPathHelper.NormalizeProjectDirectoryPath(projectPath); - if (normalizedPath == null) - { - return null; - } - - return _projectContextsCache.AddOrUpdate( - normalizedPath, - key => AddProjectContextEntry(key, null), - (key, oldEntry) => AddProjectContextEntry(key, oldEntry)); - } - - private FileModelEntry GetProjectCore(string projectDirectory) - { - var normalizedPath = ProjectPathHelper.NormalizeProjectDirectoryPath(projectDirectory); - if (normalizedPath == null) - { - return null; - } - - return _projectsCache.AddOrUpdate( - normalizedPath, - key => AddProjectEntry(key, null), - (key, oldEntry) => AddProjectEntry(key, oldEntry)); - } - - private LockFile GetLockFile(string projectDirectory) - { - var normalizedPath = ProjectPathHelper.NormalizeProjectDirectoryPath(projectDirectory); - if (normalizedPath == null) - { - return null; - } - - return _lockFileCache.AddOrUpdate( - normalizedPath, - key => AddLockFileEntry(key, null), - (key, oldEntry) => AddLockFileEntry(key, oldEntry)).Model; - } - - private FileModelEntry AddProjectEntry(string projectDirectory, FileModelEntry currentEntry) - { - if (currentEntry == null) - { - currentEntry = new FileModelEntry(); - } - else if (!File.Exists(Path.Combine(projectDirectory, Project.FileName))) - { - // project was deleted - currentEntry.Reset(); - return currentEntry; - } - - if (currentEntry.IsInvalid) - { - Project project; - if (!ProjectReader.TryGetProject(projectDirectory, out project, _settings)) - { - currentEntry.Reset(); - } - else - { - currentEntry.Model = project; - currentEntry.Diagnostics.AddRange(project.Diagnostics); - currentEntry.FilePath = project.ProjectFilePath; - currentEntry.UpdateLastWriteTimeUtc(); - } - } - - return currentEntry; - } - - private FileModelEntry AddLockFileEntry(string projectDirectory, FileModelEntry currentEntry) - { - if (currentEntry == null) - { - currentEntry = new FileModelEntry(); - } - - if (currentEntry.IsInvalid) - { - currentEntry.Reset(); - - if (!File.Exists(Path.Combine(projectDirectory, LockFile.FileName))) - { - return currentEntry; - } - else - { - currentEntry.FilePath = Path.Combine(projectDirectory, LockFile.FileName); - - using (var fs = ResilientFileStreamOpener.OpenFile(currentEntry.FilePath, retry: 2)) - { - try - { - currentEntry.Model = _lockFileReader.ReadLockFile(currentEntry.FilePath, fs, designTime: true); - currentEntry.UpdateLastWriteTimeUtc(); - } - catch (FileFormatException ex) - { - throw ex.WithFilePath(currentEntry.FilePath); - } - catch (Exception ex) - { - throw FileFormatException.Create(ex, currentEntry.FilePath); - } - } - } - } - - return currentEntry; - } - - private ProjectContextCollection AddProjectContextEntry(string projectDirectory, - ProjectContextCollection currentEntry) - { - if (currentEntry == null) - { - // new entry required - currentEntry = new ProjectContextCollection(); - } - - var projectEntry = GetProjectCore(projectDirectory); - - if (projectEntry?.Model == null) - { - // project doesn't exist anymore - currentEntry.Reset(); - return currentEntry; - } - - var project = projectEntry.Model; - if (currentEntry.HasChanged) - { - currentEntry.Reset(); - - var builder = InitializeProjectContextBuilder(project); - - currentEntry.ProjectContexts.AddRange(builder.BuildAllTargets()); - - currentEntry.Project = project; - currentEntry.ProjectFilePath = project.ProjectFilePath; - currentEntry.LastProjectFileWriteTimeUtc = File.GetLastWriteTimeUtc(currentEntry.ProjectFilePath); - - var lockFilePath = Path.Combine(project.ProjectDirectory, LockFile.FileName); - if (File.Exists(lockFilePath)) - { - currentEntry.LockFilePath = lockFilePath; - currentEntry.LastLockFileWriteTimeUtc = File.GetLastWriteTimeUtc(lockFilePath); - } - - currentEntry.ProjectDiagnostics.AddRange(projectEntry.Diagnostics); - } - - return currentEntry; - } - - private ProjectContextBuilder InitializeProjectContextBuilder(Project project) - { - var builder = new ProjectContextBuilder() - .WithProjectResolver(path => GetProjectCore(path)?.Model) - .WithLockFileResolver(path => GetLockFile(path)) - .WithProject(project); - if (_designTime) - { - builder.AsDesignTime(); - } - - return builder; - } - - private class FileModelEntry where TModel : class - { - private DateTime _lastWriteTimeUtc; - - public TModel Model { get; set; } - - public string FilePath { get; set; } - - public List Diagnostics { get; } = new List(); - - public void UpdateLastWriteTimeUtc() - { - _lastWriteTimeUtc = File.GetLastWriteTimeUtc(FilePath); - } - - public bool IsInvalid - { - get - { - if (Model == null) - { - return true; - } - - if (!File.Exists(FilePath)) - { - return true; - } - - return _lastWriteTimeUtc < File.GetLastWriteTimeUtc(FilePath); - } - } - - public void Reset() - { - Model = null; - FilePath = null; - Diagnostics.Clear(); - _lastWriteTimeUtc = DateTime.MinValue; - } - } - - private static List ResolveProjectPath(string projectPath) - { - if (File.Exists(projectPath)) - { - var filename = Path.GetFileName(projectPath); - if (!Project.FileName.Equals(filename, StringComparison.OrdinalIgnoreCase) && - !GlobalSettings.FileName.Equals(filename, StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - projectPath = Path.GetDirectoryName(projectPath); - } - - if (File.Exists(Path.Combine(projectPath, Project.FileName))) - { - return new List { projectPath }; - } - - if (File.Exists(Path.Combine(projectPath, GlobalSettings.FileName))) - { - var root = ProjectRootResolver.ResolveRootDirectory(projectPath); - GlobalSettings globalSettings; - if (GlobalSettings.TryGetGlobalSettings(projectPath, out globalSettings)) - { - return globalSettings.ProjectSearchPaths - .Select(searchPath => Path.Combine(globalSettings.DirectoryPath, searchPath)) - .Where(actualPath => Directory.Exists(actualPath)) - .SelectMany(actualPath => Directory.GetDirectories(actualPath)) - .Where(actualPath => File.Exists(Path.Combine(actualPath, Project.FileName))) - .Select(path => Path.GetFullPath(path)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToList(); - } - } - - return null; - } - - private static IEnumerable GetProjectReferences(ProjectContext context) - { - var projectDescriptions = context.LibraryManager - .GetLibraries() - .Where(lib => lib.Identity.Type == LibraryType.Project) - .OfType(); - - foreach (var description in projectDescriptions) - { - if (description.Identity.Name == context.ProjectFile.Name) - { - continue; - } - - // if this is an assembly reference then don't threat it as project reference - if (!string.IsNullOrEmpty(description.TargetFrameworkInfo?.AssemblyPath)) - { - continue; - } - - yield return description; - } - } - } -} diff --git a/src/dotnet/commands/dotnet-build/BuildCommandApp.cs b/src/dotnet/commands/dotnet-build/BuildCommandApp.cs index 758ce8420..8048c3cc2 100644 --- a/src/dotnet/commands/dotnet-build/BuildCommandApp.cs +++ b/src/dotnet/commands/dotnet-build/BuildCommandApp.cs @@ -45,14 +45,14 @@ namespace Microsoft.DotNet.Tools.Compiler public bool ShouldNotUseIncrementality { get; set; } public bool ShouldSkipDependencies { get; set; } - public WorkspaceContext Workspace { get; private set; } + public BuildWorkspace Workspace { get; private set; } // workaround: CommandLineApplication is internal therefore I cannot make _app protected so baseclasses can add their own params private readonly Dictionary baseClassOptions; public BuildCommandApp(string name, string fullName, string description) : this(name, fullName, description, workspace: null) { } - public BuildCommandApp(string name, string fullName, string description, WorkspaceContext workspace) + public BuildCommandApp(string name, string fullName, string description, BuildWorkspace workspace) { Workspace = workspace; _app = new CommandLineApplication @@ -108,13 +108,7 @@ namespace Microsoft.DotNet.Tools.Compiler // Set defaults based on the environment if (Workspace == null) { - var settings = ProjectReaderSettings.ReadFromEnvironment(); - - if (!string.IsNullOrEmpty(VersionSuffixValue)) - { - settings.VersionSuffix = VersionSuffixValue; - } - Workspace = WorkspaceContext.Create(settings, designTime: false); + Workspace = BuildWorkspace.Create(VersionSuffixValue); } var files = new ProjectGlobbingResolver().Resolve(_projectArgument.Values); diff --git a/src/dotnet/commands/dotnet-build/CompilerIOManager.cs b/src/dotnet/commands/dotnet-build/CompilerIOManager.cs index 5dfcc52da..2d2b49d48 100644 --- a/src/dotnet/commands/dotnet-build/CompilerIOManager.cs +++ b/src/dotnet/commands/dotnet-build/CompilerIOManager.cs @@ -4,12 +4,11 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.IO; using System.Linq; -using Microsoft.DotNet.Tools.Compiler; -using Microsoft.DotNet.ProjectModel; -using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Compiler.Common; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Tools.Compiler; namespace Microsoft.DotNet.Tools.Build { @@ -19,21 +18,21 @@ namespace Microsoft.DotNet.Tools.Build private readonly string _outputPath; private readonly string _buildBasePath; private readonly IList _runtimes; - private readonly WorkspaceContext _workspace; + private readonly BuildWorkspace _workspace; private readonly ConcurrentDictionary _cache; public CompilerIOManager(string configuration, string outputPath, string buildBasePath, IEnumerable runtimes, - WorkspaceContext workspace) + BuildWorkspace workspace) { _configuration = configuration; _outputPath = outputPath; _buildBasePath = buildBasePath; _runtimes = runtimes.ToList(); _workspace = workspace; - + _cache = new ConcurrentDictionary(); } diff --git a/src/dotnet/commands/dotnet-build/Program.cs b/src/dotnet/commands/dotnet-build/Program.cs index 1f02e06fa..132804192 100644 --- a/src/dotnet/commands/dotnet-build/Program.cs +++ b/src/dotnet/commands/dotnet-build/Program.cs @@ -4,11 +4,10 @@ using System; using System.Collections.Generic; using System.Linq; -using System.IO; using System.Threading.Tasks; +using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.ProjectModel; using Microsoft.DotNet.Tools.Compiler; -using Microsoft.DotNet.Cli.Utils; using NuGet.Frameworks; namespace Microsoft.DotNet.Tools.Build @@ -17,7 +16,7 @@ namespace Microsoft.DotNet.Tools.Build { public static int Run(string[] args) => Run(args, null); - public static int Run(string[] args, WorkspaceContext workspace) + public static int Run(string[] args, BuildWorkspace workspace) { DebugHelper.HandleDebugSwitch(ref args); diff --git a/src/dotnet/commands/dotnet-pack/BuildProjectCommand.cs b/src/dotnet/commands/dotnet-pack/BuildProjectCommand.cs index 3492bcda7..27c786fa7 100644 --- a/src/dotnet/commands/dotnet-pack/BuildProjectCommand.cs +++ b/src/dotnet/commands/dotnet-pack/BuildProjectCommand.cs @@ -15,18 +15,18 @@ namespace Microsoft.DotNet.Tools.Pack private readonly string _buildBasePath; private readonly string _configuration; - private readonly string _versionSuffix; + private readonly BuildWorkspace _workspace; public BuildProjectCommand( Project project, string buildBasePath, string configuration, - string versionSuffix) + BuildWorkspace workspace) { _project = project; _buildBasePath = buildBasePath; _configuration = configuration; - _versionSuffix = versionSuffix; + _workspace = workspace; } public int Execute() @@ -37,11 +37,8 @@ namespace Microsoft.DotNet.Tools.Pack argsBuilder.Add("--configuration"); argsBuilder.Add($"{_configuration}"); - if (!string.IsNullOrEmpty(_versionSuffix)) - { - argsBuilder.Add("--version-suffix"); - argsBuilder.Add(_versionSuffix); - } + // Passing the Workspace along will flow the version suffix, + // so we don't need to pass it as an argument. if (!string.IsNullOrEmpty(_buildBasePath)) { @@ -51,7 +48,7 @@ namespace Microsoft.DotNet.Tools.Pack argsBuilder.Add($"{_project.ProjectFilePath}"); - var result = Build.BuildCommand.Run(argsBuilder.ToArray()); + var result = Build.BuildCommand.Run(argsBuilder.ToArray(), _workspace); return result; } diff --git a/src/dotnet/commands/dotnet-pack/PackagesGenerator.cs b/src/dotnet/commands/dotnet-pack/PackagesGenerator.cs index 5858b6ae8..9a9c01fae 100644 --- a/src/dotnet/commands/dotnet-pack/PackagesGenerator.cs +++ b/src/dotnet/commands/dotnet-pack/PackagesGenerator.cs @@ -31,7 +31,7 @@ namespace Microsoft.DotNet.Tools.Pack var packDiagnostics = new List(); var mainPackageGenerator = new PackageGenerator(project, _configuration, _artifactPathsCalculator); - var symbolsPackageGenerator = + var symbolsPackageGenerator = new SymbolPackageGenerator(project, _configuration, _artifactPathsCalculator); return mainPackageGenerator.BuildPackage(_contexts, packDiagnostics) && diff --git a/src/dotnet/commands/dotnet-pack/Program.cs b/src/dotnet/commands/dotnet-pack/Program.cs index 4f0bfd723..8919dfbc1 100644 --- a/src/dotnet/commands/dotnet-pack/Program.cs +++ b/src/dotnet/commands/dotnet-pack/Program.cs @@ -51,19 +51,13 @@ namespace Microsoft.DotNet.Tools.Compiler } // Set defaults based on the environment - var settings = ProjectReaderSettings.ReadFromEnvironment(); - var versionSuffixValue = versionSuffix.Value(); - - if (!string.IsNullOrEmpty(versionSuffixValue)) - { - settings.VersionSuffix = versionSuffixValue; - } + var workspace = BuildWorkspace.Create(versionSuffix.Value()); var configValue = configuration.Value() ?? Cli.Utils.Constants.DefaultConfiguration; var outputValue = output.Value(); var buildBasePathValue = buildBasePath.Value(); - var contexts = ProjectContext.CreateContextForEachFramework(pathValue, settings); + var contexts = workspace.GetProjectContextCollection(pathValue).FrameworkOnlyContexts; var project = contexts.First().ProjectFile; var artifactPathsCalculator = new ArtifactPathsCalculator(project, buildBasePathValue, outputValue, configValue); @@ -72,7 +66,7 @@ namespace Microsoft.DotNet.Tools.Compiler int buildResult = 0; if (!noBuild.HasValue()) { - var buildProjectCommand = new BuildProjectCommand(project, buildBasePathValue, configValue, versionSuffixValue); + var buildProjectCommand = new BuildProjectCommand(project, buildBasePathValue, configValue, workspace); buildResult = buildProjectCommand.Execute(); } diff --git a/src/dotnet/commands/dotnet-projectmodel-server/ConnectionContext.cs b/src/dotnet/commands/dotnet-projectmodel-server/ConnectionContext.cs index ee196488a..235db0530 100644 --- a/src/dotnet/commands/dotnet-projectmodel-server/ConnectionContext.cs +++ b/src/dotnet/commands/dotnet-projectmodel-server/ConnectionContext.cs @@ -16,7 +16,7 @@ namespace Microsoft.DotNet.ProjectModel.Server public ConnectionContext(Socket acceptedSocket, string hostName, ProtocolManager protocolManager, - WorkspaceContext workspaceContext, + DesignTimeWorkspace workspaceContext, IDictionary projects) { _hostName = hostName; @@ -59,4 +59,4 @@ namespace Microsoft.DotNet.ProjectModel.Server return _queue.Send(message); } } -} \ No newline at end of file +} diff --git a/src/dotnet/commands/dotnet-projectmodel-server/InternalModels/ProjectSnapshot.cs b/src/dotnet/commands/dotnet-projectmodel-server/InternalModels/ProjectSnapshot.cs index f376bd7a2..0944a7e80 100644 --- a/src/dotnet/commands/dotnet-projectmodel-server/InternalModels/ProjectSnapshot.cs +++ b/src/dotnet/commands/dotnet-projectmodel-server/InternalModels/ProjectSnapshot.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using Microsoft.DotNet.ProjectModel.Server.Helpers; using Microsoft.DotNet.ProjectModel.Server.Models; @@ -20,7 +19,7 @@ namespace Microsoft.DotNet.ProjectModel.Server public ErrorMessage GlobalErrorMessage { get; set; } public Dictionary ProjectContexts { get; } = new Dictionary(); - public static ProjectSnapshot Create(string projectDirectory, string configuration, WorkspaceContext workspaceContext, IReadOnlyList projectSearchPaths) + public static ProjectSnapshot Create(string projectDirectory, string configuration, DesignTimeWorkspace workspaceContext, IReadOnlyList projectSearchPaths) { var projectContextsCollection = workspaceContext.GetProjectContextCollection(projectDirectory); if (!projectContextsCollection.ProjectContexts.Any()) diff --git a/src/dotnet/commands/dotnet-projectmodel-server/Program.cs b/src/dotnet/commands/dotnet-projectmodel-server/Program.cs index 29e1d780b..3211ed15d 100644 --- a/src/dotnet/commands/dotnet-projectmodel-server/Program.cs +++ b/src/dotnet/commands/dotnet-projectmodel-server/Program.cs @@ -14,7 +14,7 @@ namespace Microsoft.DotNet.ProjectModel.Server public class ProjectModelServerCommand { private readonly Dictionary _projects; - private readonly WorkspaceContext _workspaceContext; + private readonly DesignTimeWorkspace _workspaceContext; private readonly ProtocolManager _protocolManager; private readonly string _hostName; private readonly int _port; @@ -25,7 +25,7 @@ namespace Microsoft.DotNet.ProjectModel.Server _port = port; _hostName = hostName; _protocolManager = new ProtocolManager(maxVersion: 4); - _workspaceContext = WorkspaceContext.Create(designTime: true); + _workspaceContext = new DesignTimeWorkspace(ProjectReaderSettings.ReadFromEnvironment()); _projects = new Dictionary(); } diff --git a/src/dotnet/commands/dotnet-projectmodel-server/ProjectManager.cs b/src/dotnet/commands/dotnet-projectmodel-server/ProjectManager.cs index c46400c61..d28651a89 100644 --- a/src/dotnet/commands/dotnet-projectmodel-server/ProjectManager.cs +++ b/src/dotnet/commands/dotnet-projectmodel-server/ProjectManager.cs @@ -31,7 +31,7 @@ namespace Microsoft.DotNet.ProjectModel.Server private ProjectSnapshot _local = new ProjectSnapshot(); private ProjectSnapshot _remote = new ProjectSnapshot(); - private readonly WorkspaceContext _workspaceContext; + private readonly DesignTimeWorkspace _workspaceContext; private int? _contextProtocolVersion; private readonly List> _messengers; @@ -42,7 +42,7 @@ namespace Microsoft.DotNet.ProjectModel.Server public ProjectManager( int contextId, - WorkspaceContext workspaceContext, + DesignTimeWorkspace workspaceContext, ProtocolManager protocolManager) { Id = contextId; diff --git a/src/dotnet/commands/dotnet-publish/Program.cs b/src/dotnet/commands/dotnet-publish/Program.cs index 7d0a07804..3763b080a 100644 --- a/src/dotnet/commands/dotnet-publish/Program.cs +++ b/src/dotnet/commands/dotnet-publish/Program.cs @@ -45,7 +45,7 @@ namespace Microsoft.DotNet.Tools.Publish publish.VersionSuffix = versionSuffix.Value(); publish.ShouldBuild = !noBuild.HasValue(); - publish.Workspace = WorkspaceContext.Create(versionSuffix.Value(), designTime: false); + publish.Workspace = BuildWorkspace.Create(versionSuffix.Value()); if (string.IsNullOrEmpty(publish.ProjectPath)) { diff --git a/src/dotnet/commands/dotnet-publish/PublishCommand.cs b/src/dotnet/commands/dotnet-publish/PublishCommand.cs index cccfee24e..b6f7ed541 100644 --- a/src/dotnet/commands/dotnet-publish/PublishCommand.cs +++ b/src/dotnet/commands/dotnet-publish/PublishCommand.cs @@ -31,7 +31,7 @@ namespace Microsoft.DotNet.Tools.Publish public string Runtime { get; set; } public bool NativeSubdirectories { get; set; } public NuGetFramework NugetFramework { get; set; } - public WorkspaceContext Workspace { get; set; } + public BuildWorkspace Workspace { get; set; } public IList ProjectContexts { get; set; } public string VersionSuffix { get; set; } public int NumberOfProjects { get; private set; } @@ -253,7 +253,7 @@ namespace Microsoft.DotNet.Tools.Publish args.Add(buildBasePath); } - var result = Build.BuildCommand.Run(args.ToArray()); + var result = Build.BuildCommand.Run(args.ToArray(), Workspace); return result == 0; } diff --git a/src/dotnet/commands/dotnet-run/RunCommand.cs b/src/dotnet/commands/dotnet-run/RunCommand.cs index c323ea85a..5b68e6cb4 100644 --- a/src/dotnet/commands/dotnet-run/RunCommand.cs +++ b/src/dotnet/commands/dotnet-run/RunCommand.cs @@ -29,7 +29,7 @@ namespace Microsoft.DotNet.Tools.Run ProjectContext _context; List _args; - private WorkspaceContext _workspace; + private BuildWorkspace _workspace; public int Start() { @@ -113,7 +113,7 @@ namespace Microsoft.DotNet.Tools.Run private int RunExecutable() { // Set up the workspace - _workspace = WorkspaceContext.Create(ProjectReaderSettings.ReadFromEnvironment(), designTime: false); + _workspace = new BuildWorkspace(ProjectReaderSettings.ReadFromEnvironment()); CalculateDefaultsForNonAssigned(); @@ -125,7 +125,7 @@ namespace Microsoft.DotNet.Tools.Run $"--configuration", Configuration, $"{_context.ProjectFile.ProjectDirectory}" - }); + }, _workspace); if (result != 0) { diff --git a/src/dotnet/commands/dotnet-test/BaseDotnetTestRunner.cs b/src/dotnet/commands/dotnet-test/BaseDotnetTestRunner.cs index be013d38e..64b2069ec 100644 --- a/src/dotnet/commands/dotnet-test/BaseDotnetTestRunner.cs +++ b/src/dotnet/commands/dotnet-test/BaseDotnetTestRunner.cs @@ -8,26 +8,26 @@ namespace Microsoft.DotNet.Tools.Test { public abstract class BaseDotnetTestRunner : IDotnetTestRunner { - public int RunTests(ProjectContext projectContext, DotnetTestParams dotnetTestParams) + public int RunTests(ProjectContext projectContext, DotnetTestParams dotnetTestParams, BuildWorkspace workspace) { - var result = BuildTestProject(projectContext, dotnetTestParams); + var result = BuildTestProject(projectContext, dotnetTestParams, workspace); return result == 0 ? DoRunTests(projectContext, dotnetTestParams) : result; } internal abstract int DoRunTests(ProjectContext projectContext, DotnetTestParams dotnetTestParams); - private int BuildTestProject(ProjectContext projectContext, DotnetTestParams dotnetTestParams) + private int BuildTestProject(ProjectContext projectContext, DotnetTestParams dotnetTestParams, BuildWorkspace workspace) { if (dotnetTestParams.NoBuild) { return 0; } - return DoBuildTestProject(projectContext, dotnetTestParams); + return DoBuildTestProject(projectContext, dotnetTestParams, workspace); } - private int DoBuildTestProject(ProjectContext projectContext, DotnetTestParams dotnetTestParams) + private int DoBuildTestProject(ProjectContext projectContext, DotnetTestParams dotnetTestParams, BuildWorkspace workspace) { var strings = new List { @@ -59,7 +59,7 @@ namespace Microsoft.DotNet.Tools.Test strings.Add(projectContext.RuntimeIdentifier); } - var result = Build.BuildCommand.Run(strings.ToArray()); + var result = Build.BuildCommand.Run(strings.ToArray(), workspace); return result; } diff --git a/src/dotnet/commands/dotnet-test/DotnetTestParams.cs b/src/dotnet/commands/dotnet-test/DotnetTestParams.cs index a06c1ab56..d53a986b6 100644 --- a/src/dotnet/commands/dotnet-test/DotnetTestParams.cs +++ b/src/dotnet/commands/dotnet-test/DotnetTestParams.cs @@ -41,6 +41,8 @@ namespace Microsoft.DotNet.Tools.Test public NuGetFramework Framework { get; set; } + public string UnparsedFramework { get; set; } + public List RemainingArguments { get; set; } public bool NoBuild { get; set; } @@ -97,6 +99,7 @@ namespace Microsoft.DotNet.Tools.Test Port = port; } + UnparsedFramework = _frameworkOption.Value(); if (_frameworkOption.HasValue()) { Framework = NuGetFramework.Parse(_frameworkOption.Value()); diff --git a/src/dotnet/commands/dotnet-test/IDotnetTestRunner.cs b/src/dotnet/commands/dotnet-test/IDotnetTestRunner.cs index 1fe290429..1466eb32d 100644 --- a/src/dotnet/commands/dotnet-test/IDotnetTestRunner.cs +++ b/src/dotnet/commands/dotnet-test/IDotnetTestRunner.cs @@ -8,6 +8,6 @@ namespace Microsoft.DotNet.Tools.Test { public interface IDotnetTestRunner { - int RunTests(ProjectContext projectContext, DotnetTestParams dotnetTestParams); + int RunTests(ProjectContext projectContext, DotnetTestParams dotnetTestParams, BuildWorkspace workspace); } } diff --git a/src/dotnet/commands/dotnet-test/Program.cs b/src/dotnet/commands/dotnet-test/Program.cs index 9a0204abe..6d6df9c7a 100644 --- a/src/dotnet/commands/dotnet-test/Program.cs +++ b/src/dotnet/commands/dotnet-test/Program.cs @@ -48,12 +48,19 @@ namespace Microsoft.DotNet.Tools.Test var exitCode = 0; // Create a workspace - var workspace = WorkspaceContext.Create(ProjectReaderSettings.ReadFromEnvironment(), designTime: false); + var workspace = new BuildWorkspace(ProjectReaderSettings.ReadFromEnvironment()); if (dotnetTestParams.Framework != null) { - var projectContext = ProjectContext.Create(projectPath, dotnetTestParams.Framework, runtimeIdentifiers); - exitCode = RunTest(projectContext, dotnetTestParams); + var projectContext = workspace.GetProjectContext(projectPath, dotnetTestParams.Framework); + if (projectContext == null) + { + Reporter.Error.WriteLine($"Project '{projectPath}' does not support framework: {dotnetTestParams.UnparsedFramework}"); + return 1; + } + projectContext = workspace.GetRuntimeContext(projectContext, runtimeIdentifiers); + + exitCode = RunTest(projectContext, dotnetTestParams, workspace); } else { @@ -66,7 +73,7 @@ namespace Microsoft.DotNet.Tools.Test // Execute for all TFMs the project targets. foreach (var projectContext in projectContexts) { - var result = RunTest(projectContext, dotnetTestParams); + var result = RunTest(projectContext, dotnetTestParams, workspace); if (result == 0) { summary.Passed++; @@ -134,11 +141,11 @@ namespace Microsoft.DotNet.Tools.Test } } - private int RunTest(ProjectContext projectContext, DotnetTestParams dotnetTestParams) + private int RunTest(ProjectContext projectContext, DotnetTestParams dotnetTestParams, BuildWorkspace workspace) { var testRunner = projectContext.ProjectFile.TestRunner; var dotnetTestRunner = _dotnetTestRunnerFactory.Create(dotnetTestParams.Port); - return dotnetTestRunner.RunTests(projectContext, dotnetTestParams); + return dotnetTestRunner.RunTests(projectContext, dotnetTestParams, workspace); } private static string GetProjectPath(string projectPath) diff --git a/test/Microsoft.DotNet.ProjectModel.Tests/ProjectContextBuilderTests.cs b/test/Microsoft.DotNet.ProjectModel.Tests/ProjectContextBuilderTests.cs new file mode 100644 index 000000000..5d873a625 --- /dev/null +++ b/test/Microsoft.DotNet.ProjectModel.Tests/ProjectContextBuilderTests.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using FluentAssertions; +using Microsoft.DotNet.ProjectModel.Graph; +using NuGet.Frameworks; +using Xunit; + +namespace Microsoft.DotNet.ProjectModel.Tests +{ + public class ProjectContextBuilderTests + { + private static readonly HashSet KnownProperties = new HashSet(StringComparer.Ordinal) { + "Project", + "LockFile", + "TargetFramework", + "RuntimeIdentifiers", + "RootDirectory", + "ProjectDirectory", + "PackagesDirectory", + "ReferenceAssembliesPath", + "IsDesignTime", + "ProjectResolver", + "LockFileResolver", + "ProjectReaderSettings" + }; + + // This test ensures that Clone is always kept up-to-date to avoid hard-to-debug errors + // because someone added a property but didn't put it in Clone. + [Fact] + public void CloneTest() + { + // Initialize a test instance that we're going to clone. Make sure all properties are initialized here. + var initialBuilder = new ProjectContextBuilder() + .WithProject(new Project()) + .WithLockFile(new LockFile("abc")) + .WithTargetFramework(FrameworkConstants.CommonFrameworks.NetStandard10) + .WithRuntimeIdentifiers(new[] { "win7-x64", "osx.10.10-x64" }) + .WithRootDirectory("C:\\The\\Root") + .WithProjectDirectory("/where/the/project/at") + .WithPackagesDirectory("D:\\My\\Awesome\\NuGet\\Packages") + .WithReferenceAssembliesPath("/these/are/the/reference/assemblies") + .WithProjectResolver(_ => new Project()) + .WithLockFileResolver(_ => new LockFile("def")) + .WithProjectReaderSettings(new ProjectReaderSettings()); + + // Clone the builder + var clonedBuilder = initialBuilder.Clone(); + + // Compare all the properties. This is a shallow clone, so they should all be exactly ReferenceEqual + foreach(var prop in typeof(ProjectContextBuilder).GetTypeInfo().DeclaredProperties) + { + KnownProperties.Remove(prop.Name).Should().BeTrue(because: $"{prop.Name} should be listed in the known properties to ensure it is properly tested."); + + if (prop.PropertyType.GetTypeInfo().IsValueType) + { + // Can't use reference equality on value types + prop.GetValue(clonedBuilder).Should().Be(prop.GetValue(initialBuilder), because: $"clone should have duplicated the {prop.Name} property"); + } + else + { + prop.GetValue(clonedBuilder).Should().BeSameAs(prop.GetValue(initialBuilder), because: $"clone should have duplicated the {prop.Name} property"); + } + } + + KnownProperties.Should().BeEmpty(because: "all properties should have been checked by the CloneTest"); + } + } +} diff --git a/test/dotnet-build.Tests/IncrementalTests.cs b/test/dotnet-build.Tests/IncrementalTests.cs index bd3ad8b69..2f278a375 100644 --- a/test/dotnet-build.Tests/IncrementalTests.cs +++ b/test/dotnet-build.Tests/IncrementalTests.cs @@ -9,6 +9,7 @@ using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Tools.Test.Utilities; using Xunit; using Microsoft.DotNet.TestFramework; +using System.Diagnostics; namespace Microsoft.DotNet.Tools.Builder.Tests { diff --git a/test/dotnet-compile.UnitTests/GivenACompilationDriver.cs b/test/dotnet-compile.UnitTests/GivenACompilationDriver.cs index d9f1de1d0..6b26d73ae 100644 --- a/test/dotnet-compile.UnitTests/GivenACompilationDriver.cs +++ b/test/dotnet-compile.UnitTests/GivenACompilationDriver.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.IO; using FluentAssertions; using Microsoft.DotNet.ProjectModel; -using Microsoft.DotNet.Tools.Compiler; using Moq; using NuGet.Frameworks; using Xunit; @@ -20,7 +19,7 @@ namespace Microsoft.DotNet.Tools.Compiler.Tests private Mock _nativeCompilerMock; private List _contexts; private BuildCommandApp _args; - private readonly WorkspaceContext _workspace; + private readonly BuildWorkspace _workspace; public GivenACompilationDriverController() { @@ -35,13 +34,13 @@ namespace Microsoft.DotNet.Tools.Compiler.Tests .Compile(It.IsAny(), It.IsAny())) .Returns(true); - _workspace = WorkspaceContext.Create(ProjectReaderSettings.ReadFromEnvironment(), designTime: false); + _workspace = new BuildWorkspace(ProjectReaderSettings.ReadFromEnvironment()); _contexts = new List { _workspace.GetProjectContext(_projectJson, NuGetFramework.Parse("netcoreapp1.0")) }; - _args = new BuildCommandApp("dotnet compile", ".NET Compiler", "Compiler for the .NET Platform", WorkspaceContext.Create(designTime: false)); + _args = new BuildCommandApp("dotnet compile", ".NET Compiler", "Compiler for the .NET Platform", _workspace); } [Fact] diff --git a/test/dotnet-compile.UnitTests/GivenThatICareAboutScriptVariablesFromAManagedCompiler.cs b/test/dotnet-compile.UnitTests/GivenThatICareAboutScriptVariablesFromAManagedCompiler.cs index bd0a9090a..c3c0be8af 100644 --- a/test/dotnet-compile.UnitTests/GivenThatICareAboutScriptVariablesFromAManagedCompiler.cs +++ b/test/dotnet-compile.UnitTests/GivenThatICareAboutScriptVariablesFromAManagedCompiler.cs @@ -187,7 +187,7 @@ namespace Microsoft.DotNet.Tools.Compiler.Tests It.IsAny())) .Returns(command.Object); - var _args = new BuildCommandApp("dotnet compile", ".NET Compiler", "Compiler for the .NET Platform", WorkspaceContext.Create(designTime: false)); + var _args = new BuildCommandApp("dotnet compile", ".NET Compiler", "Compiler for the .NET Platform", new BuildWorkspace(new ProjectReaderSettings())); _args.ConfigValue = ConfigValue; PreCompileScriptVariables = new Dictionary(); @@ -218,7 +218,7 @@ namespace Microsoft.DotNet.Tools.Compiler.Tests rids.Add(rid); } - var workspace = WorkspaceContext.Create(ProjectReaderSettings.ReadFromEnvironment(), designTime: false); + var workspace = new BuildWorkspace(new ProjectReaderSettings()); var context = workspace.GetRuntimeContext(workspace.GetProjectContext(projectJson, TestAssetFramework), rids); context = workspace.GetRuntimeContext(context, rids); managedCompiler.Compile(context, _args); diff --git a/test/dotnet-projectmodel-server.Tests/DthTestClient.cs b/test/dotnet-projectmodel-server.Tests/DthTestClient.cs index 81def3636..f7d230fe0 100644 --- a/test/dotnet-projectmodel-server.Tests/DthTestClient.cs +++ b/test/dotnet-projectmodel-server.Tests/DthTestClient.cs @@ -53,12 +53,12 @@ namespace Microsoft.DotNet.ProjectModel.Server.Tests Task.Run(() => ReadMessage(_readCancellationToken.Token), _readCancellationToken.Token); } - public void SendPayLoad(Project project, string messageType) + public void SendPayload(Project project, string messageType) { - SendPayLoad(project.ProjectDirectory, messageType); + SendPayload(project.ProjectDirectory, messageType); } - public void SendPayLoad(string projectPath, string messageType) + public void SendPayload(string projectPath, string messageType) { int contextId; if (!_projectContexts.TryGetValue(projectPath, out contextId)) @@ -66,15 +66,15 @@ namespace Microsoft.DotNet.ProjectModel.Server.Tests Assert.True(false, $"Unable to resolve context for {projectPath}"); } - SendPayLoad(contextId, messageType); + SendPayload(contextId, messageType); } - public void SendPayLoad(int contextId, string messageType) + public void SendPayload(int contextId, string messageType) { - SendPayLoad(contextId, messageType, new { }); + SendPayload(contextId, messageType, new { }); } - public void SendPayLoad(int contextId, string messageType, object payload) + public void SendPayload(int contextId, string messageType, object payload) { lock (_writer) { @@ -94,7 +94,7 @@ namespace Microsoft.DotNet.ProjectModel.Server.Tests var contextId = _nextContextId++; _projectContexts[projectPath] = contextId; - SendPayLoad(contextId, MessageTypes.Initialize, new { ProjectFolder = projectPath }); + SendPayload(contextId, MessageTypes.Initialize, new { ProjectFolder = projectPath }); return contextId; } @@ -104,7 +104,7 @@ namespace Microsoft.DotNet.ProjectModel.Server.Tests var contextId = _nextContextId++; _projectContexts[projectPath] = contextId; - SendPayLoad(contextId, MessageTypes.Initialize, new { ProjectFolder = projectPath, Version = protocolVersion }); + SendPayload(contextId, MessageTypes.Initialize, new { ProjectFolder = projectPath, Version = protocolVersion }); return contextId; } @@ -114,14 +114,14 @@ namespace Microsoft.DotNet.ProjectModel.Server.Tests var contextId = _nextContextId++; _projectContexts[projectPath] = contextId; - SendPayLoad(contextId, MessageTypes.Initialize, new { ProjectFolder = projectPath, Version = protocolVersion, Configuration = configuration }); + SendPayload(contextId, MessageTypes.Initialize, new { ProjectFolder = projectPath, Version = protocolVersion, Configuration = configuration }); return contextId; } public void SetProtocolVersion(int version) { - SendPayLoad(0, MessageTypes.ProtocolVersion, new { Version = version }); + SendPayload(0, MessageTypes.ProtocolVersion, new { Version = version }); } public List DrainMessage(int count) diff --git a/test/dotnet-projectmodel-server.Tests/DthTests.cs b/test/dotnet-projectmodel-server.Tests/DthTests.cs index 707b2096b..3af722a35 100644 --- a/test/dotnet-projectmodel-server.Tests/DthTests.cs +++ b/test/dotnet-projectmodel-server.Tests/DthTests.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.ProjectModel.Graph; using Microsoft.DotNet.TestFramework; using Microsoft.DotNet.Tools.Test.Utilities; @@ -260,7 +261,7 @@ namespace Microsoft.DotNet.ProjectModel.Server.Tests Path.Combine(projectPath, "home", GlobalSettings.FileName), JsonConvert.SerializeObject(new { project = new string[] { "src" } })); - client.SendPayLoad(testProject, "RefreshDependencies"); + client.SendPayload(testProject, "RefreshDependencies"); client.DrainTillFirst("ProjectInformation") .RetrievePayloadAs() @@ -379,7 +380,7 @@ namespace Microsoft.DotNet.ProjectModel.Server.Tests messages.ContainsMessage(MessageTypes.Error); File.WriteAllText(projectFile, content); - client.SendPayLoad(testProject, MessageTypes.FilesChanged); + client.SendPayload(testProject, MessageTypes.FilesChanged); var clearError = client.DrainTillFirst(MessageTypes.Error); clearError.Payload.AsJObject().AssertProperty("Message", null as string); } @@ -548,7 +549,7 @@ namespace Microsoft.DotNet.ProjectModel.Server.Tests File.WriteAllText(projectFilePath, JsonConvert.SerializeObject(projectJson)); - client.SendPayLoad(projectPath, MessageTypes.RefreshDependencies); + client.SendPayload(projectPath, MessageTypes.RefreshDependencies); var afterDependencies = client.DrainTillFirst(MessageTypes.Dependencies); afterDependencies.RetrieveDependency(appName) @@ -597,7 +598,7 @@ namespace Microsoft.DotNet.ProjectModel.Server.Tests File.WriteAllText(projectFilePath, JsonConvert.SerializeObject(projectJson)); - client.SendPayLoad(projectPath, MessageTypes.RefreshDependencies); + client.SendPayload(projectPath, MessageTypes.RefreshDependencies); var afterDependencies = client.DrainTillFirst(MessageTypes.Dependencies); afterDependencies.RetrieveDependency("MainApp") @@ -625,6 +626,82 @@ namespace Microsoft.DotNet.ProjectModel.Server.Tests } } + [Fact] + public void TestTargetFrameworkChange() + { + using (var server = new DthTestServer()) + using (var client = new DthTestClient(server)) + { + var testProject = _testAssetsManager.CreateTestInstance("EmptyLibrary") + .WithLockFiles() + .TestRoot; + + // initialize the project and drain all messages (7 message for project with one framework) + client.Initialize(testProject); + client.DrainAllMessages(); + + // update the target framework from netstandard1.3 to netstandard 1.5 so as to invalidate all + // dependencies + var projectJsonPath = Path.Combine(testProject, "project.json"); + File.WriteAllText(projectJsonPath, + File.ReadAllText(projectJsonPath).Replace("netstandard1.3", "netstandard1.5")); + + // send files change request to server to prompt update + client.SendPayload(testProject, MessageTypes.FilesChanged); + + // assert project information is updated + client.DrainTillFirst(MessageTypes.ProjectInformation) + .RetrievePayloadAs() + .RetrievePropertyAs("Frameworks") + .AssertJArrayCount(1) + .RetrieveArraryElementAs(0) + .AssertProperty("ShortName", "netstandard1.5"); + + // the NETStandard.Library dependency should turn unresolved + var dependencies = client.DrainTillFirst(MessageTypes.Dependencies); + + dependencies.RetrievePayloadAs() + .RetrievePropertyAs("Framework") + .AssertProperty("ShortName", "netstandard1.5"); + + dependencies.RetrieveDependency("NETStandard.Library") + .RetrievePropertyAs("Errors") + .AssertJArrayCount(1) + .RetrieveArraryElementAs(0) + .AssertProperty("ErrorCode", "NU1001"); + + // warning for project.json and project.lock.json out of sync + var diagnostics = client.DrainTillFirst(MessageTypes.DependencyDiagnostics); + + diagnostics.RetrievePayloadAs() + .RetrievePropertyAs("Framework") + .AssertProperty("ShortName", "netstandard1.5"); + + diagnostics.RetrievePayloadAs() + .RetrievePropertyAs("Warnings") + .AssertJArrayCount(1) + .RetrieveArraryElementAs(0) + .AssertProperty("ErrorCode", "NU1006"); + + // restore again + var restoreCommand = new RestoreCommand(); + restoreCommand.WorkingDirectory = testProject; + restoreCommand.Execute().Should().Pass(); + + client.SendPayload(testProject, MessageTypes.RefreshDependencies); + + client.DrainTillFirst(MessageTypes.Dependencies) + .RetrieveDependency("NETStandard.Library") + .RetrievePropertyAs("Errors") + .AssertJArrayCount(0); + + client.DrainTillFirst(MessageTypes.DependencyDiagnostics) + .RetrievePayloadAs() + .RetrievePropertyAs("Warnings") + .AssertJArrayCount(0); + } + } + private static string NormalizePathString(string original) { return original.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar); diff --git a/test/dotnet-test.UnitTests/GivenATestCommand.cs b/test/dotnet-test.UnitTests/GivenATestCommand.cs index 846920de2..3a1c7598a 100644 --- a/test/dotnet-test.UnitTests/GivenATestCommand.cs +++ b/test/dotnet-test.UnitTests/GivenATestCommand.cs @@ -29,7 +29,7 @@ namespace Microsoft.Dotnet.Tools.Test.Tests { _dotnetTestRunnerMock = new Mock(); _dotnetTestRunnerMock - .Setup(d => d.RunTests(It.IsAny(), It.IsAny())) + .Setup(d => d.RunTests(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(0); _dotnetTestRunnerFactoryMock = new Mock(); @@ -62,7 +62,7 @@ namespace Microsoft.Dotnet.Tools.Test.Tests var result = _testCommand.DoRun(new[] { ProjectJsonPath, "-f", "netcoreapp1.0" }); _dotnetTestRunnerMock.Verify( - d => d.RunTests(It.IsAny(), It.IsAny()), + d => d.RunTests(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } }