// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.DotNet.ProjectModel.Graph; using Microsoft.DotNet.TestFramework; using Microsoft.DotNet.Tools.Test.Utilities; using Microsoft.Extensions.Logging; using Microsoft.Extensions.PlatformAbstractions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Xunit; namespace Microsoft.DotNet.ProjectModel.Server.Tests { public class DthTests : TestBase { private readonly TestAssetsManager _testAssetsManager; private readonly ILoggerFactory _loggerFactory; public DthTests() { _loggerFactory = new LoggerFactory(); var testVerbose = Environment.GetEnvironmentVariable("DOTNET_TEST_VERBOSE"); if (testVerbose == "2") { _loggerFactory.AddConsole(LogLevel.Trace); } else if (testVerbose == "1") { _loggerFactory.AddConsole(LogLevel.Information); } else if (testVerbose == "0") { _loggerFactory.AddConsole(LogLevel.Warning); } else { _loggerFactory.AddConsole(LogLevel.Error); } _testAssetsManager = new TestAssetsManager( Path.Combine(RepoRoot, "TestAssets", "ProjectModelServer", "DthTestProjects", "src")); } [Fact] public void DthStartup_GetProjectInformation() { var projectPath = Path.Combine(_testAssetsManager.AssetsRoot, "EmptyConsoleApp"); Assert.NotNull(projectPath); using (var server = new DthTestServer(_loggerFactory)) using (var client = new DthTestClient(server, _loggerFactory)) { client.Initialize(projectPath); var projectInformation = client.DrainTillFirst(MessageTypes.ProjectInformation) .EnsureSource(server, client) .RetrievePayloadAs() .AssertProperty("Name", "EmptyConsoleApp"); projectInformation.RetrievePropertyAs("Configurations") .AssertJArrayCount(2) .AssertJArrayContains("Debug") .AssertJArrayContains("Release"); var frameworkShortNames = projectInformation.RetrievePropertyAs("Frameworks") .AssertJArrayCount(2) .Select(f => f["ShortName"].Value()); Assert.Contains("netstandardapp1.5", frameworkShortNames); Assert.Contains("dnx451", frameworkShortNames); } } [Theory] [InlineData(4, 4)] [InlineData(5, 4)] [InlineData(3, 3)] public void DthStartup_ProtocolNegotiation(int requestVersion, int expectVersion) { using (var server = new DthTestServer(_loggerFactory)) using (var client = new DthTestClient(server, _loggerFactory)) { client.SetProtocolVersion(requestVersion); var response = client.DrainTillFirst(MessageTypes.ProtocolVersion, TimeSpan.FromDays(1)); response.EnsureSource(server, client); Assert.Equal(expectVersion, response.Payload["Version"]?.Value()); } } [Fact] public void DthStartup_ProtocolNegotiation_ZeroIsNoAllowed() { using (var server = new DthTestServer(_loggerFactory)) using (var client = new DthTestClient(server, _loggerFactory)) { client.SetProtocolVersion(0); Assert.Throws(() => { client.DrainTillFirst(MessageTypes.ProtocolVersion, timeout: TimeSpan.FromSeconds(1)); }); } } [Theory] [InlineData("Project", "UnresolvedProjectSample", "EmptyLibrary", "Project")] [InlineData("Package", "UnresolvedPackageSample", "NoSuchPackage", null)] [InlineData("Package", "IncompatiblePackageSample", "Microsoft.Web.Administration", "Package")] public void DthCompilation_Initialize_UnresolvedDependency(string referenceType, string testProjectName, string expectedUnresolvedDependency, string expectedUnresolvedType) { if (PlatformServices.Default.Runtime.OperatingSystemPlatform == Platform.Linux) { Console.WriteLine("Test is skipped on Linux"); return; } var projectPath = Path.Combine(_testAssetsManager.AssetsRoot, testProjectName); Assert.NotNull(projectPath); using (var server = new DthTestServer(_loggerFactory)) using (var client = new DthTestClient(server, _loggerFactory)) { client.Initialize(projectPath); var referencesMessage = client.DrainTillFirst(MessageTypes.References, TimeSpan.FromDays(1)) .EnsureSource(server, client); if (referenceType == "Project") { var expectedUnresolvedProjectPath = Path.Combine(Path.GetDirectoryName(projectPath), expectedUnresolvedDependency, Project.FileName); referencesMessage.RetrievePayloadAs() .RetrievePropertyAs("ProjectReferences") .AssertJArrayCount(1) .RetrieveArraryElementAs(0) .AssertProperty("Name", expectedUnresolvedDependency) .AssertProperty("Path", expectedUnresolvedProjectPath) .AssertProperty("MSBuildProjectPath", prop => !prop.HasValues); } else if (referenceType == "Package") { referencesMessage.RetrievePayloadAs() .RetrievePropertyAs("ProjectReferences") .AssertJArrayCount(0); } var unresolveDependency = client.DrainTillFirst(MessageTypes.Dependencies) .EnsureSource(server, client) .RetrieveDependency(expectedUnresolvedDependency); unresolveDependency.AssertProperty("Name", expectedUnresolvedDependency) .AssertProperty("DisplayName", expectedUnresolvedDependency) .AssertProperty("Resolved", false) .AssertProperty("Type", expectedUnresolvedType); if (expectedUnresolvedType == "Project") { unresolveDependency.AssertProperty("Path", Path.Combine(Path.GetDirectoryName(projectPath), expectedUnresolvedDependency, Project.FileName)); } else { Assert.False(unresolveDependency["Path"].HasValues); } } } [Fact] public void DthNegative_BrokenProjectPathInLockFile() { using (var server = new DthTestServer(_loggerFactory)) using (var client = new DthTestClient(server, _loggerFactory)) { // After restore the project is copied to another place so that // the relative path in project lock file is invalid. var movedProjectPath = _testAssetsManager.CreateTestInstance("BrokenProjectPathSample") .WithLockFiles() .TestRoot; client.Initialize(movedProjectPath); client.DrainTillFirst("DependencyDiagnostics") .RetrieveDependencyDiagnosticsCollection() .RetrieveDependencyDiagnosticsErrorAt(0) .AssertProperty("FormattedMessage", message => message.Contains("error NU1002")) .RetrievePropertyAs("Source") .AssertProperty("Name", "EmptyLibrary"); client.DrainTillFirst("Dependencies") .RetrieveDependency("EmptyLibrary") .AssertProperty("Errors", errorsArray => errorsArray.Count == 1) .AssertProperty("Warnings", warningsArray => warningsArray.Count == 0) .AssertProperty("Name", "EmptyLibrary") .AssertProperty("Resolved", false); } } [Fact(Skip = "Require dotnet restore integration test")] public void DthDependencies_UpdateGlobalJson_RefreshDependencies() { var assets = new TestAssetsManager(Path.Combine(AppContext.BaseDirectory, "TestAssets", "ProjectModelServer")); var projectPath = assets.CreateTestInstance("DthUpdateSearchPathSample").WithLockFiles().TestRoot; Assert.True(Directory.Exists(projectPath)); using (var server = new DthTestServer(_loggerFactory)) using (var client = new DthTestClient(server, _loggerFactory)) { var testProject = Path.Combine(projectPath, "home", "src", "MainProject"); client.Initialize(testProject); client.DrainTillFirst("ProjectInformation") .RetrievePayloadAs() .RetrievePropertyAs("ProjectSearchPaths") .AssertJArrayCount(2); client.DrainTillFirst("DependencyDiagnostics") .RetrievePayloadAs() .AssertProperty("Errors", array => array.Count == 0) .AssertProperty("Warnings", array => array.Count == 0); client.DrainTillFirst("Dependencies") .RetrieveDependency("Newtonsoft.Json") .AssertProperty("Type", "Project") .AssertProperty("Resolved", true) .AssertProperty("Errors", array => array.Count == 0, _ => "Dependency shouldn't contain any error."); // Overwrite the global.json to remove search path to ext File.WriteAllText( Path.Combine(projectPath, "home", GlobalSettings.FileName), JsonConvert.SerializeObject(new { project = new string[] { "src" } })); client.SendPayLoad(testProject, "RefreshDependencies"); client.DrainTillFirst("ProjectInformation") .RetrievePayloadAs() .RetrievePropertyAs("ProjectSearchPaths") .AssertJArrayCount(1) .AssertJArrayElement(0, Path.Combine(projectPath, "home", "src")); client.DrainTillFirst("DependencyDiagnostics") .RetrieveDependencyDiagnosticsCollection() .RetrieveDependencyDiagnosticsErrorAt(0) .AssertProperty("ErrorCode", "NU1010"); client.DrainTillFirst("Dependencies") .RetrieveDependency("Newtonsoft.Json") .AssertProperty("Type", "") .AssertProperty("Resolved", false) .RetrievePropertyAs("Errors") .AssertJArrayCount(1) .RetrieveArraryElementAs(0) .AssertProperty("ErrorCode", "NU1010"); } } [Fact] public void DthStartup_OpenProjectBeforeRestore() { var projectPath = _testAssetsManager.CreateTestInstance("EmptyConsoleApp").TestRoot; using (var server = new DthTestServer(_loggerFactory)) using (var client = new DthTestClient(server, _loggerFactory)) { client.Initialize(projectPath); var messages = client.DrainAllMessages(); Assert.False(messages.Any(msg => msg.MessageType == MessageTypes.Error)); var dependencyDiagnostics = messages.Where(msg => msg.MessageType == MessageTypes.DependencyDiagnostics); Assert.Equal(2, dependencyDiagnostics.Count()); foreach (var message in dependencyDiagnostics) { message.RetrievePayloadAs() .RetrievePropertyAs("Errors") .AssertJArrayContains(error => error["ErrorCode"].Value() == ErrorCodes.NU1009); } } } [Fact] public void InvalidProjectJson() { var testAssetsPath = Path.Combine(RepoRoot, "TestAssets", "ProjectModelServer"); var assetsManager = new TestAssetsManager(testAssetsPath); var testSource = assetsManager.CreateTestInstance("IncorrectProjectJson").TestRoot; using (var server = new DthTestServer(_loggerFactory)) using (var client = new DthTestClient(server, _loggerFactory)) { client.Initialize(Path.Combine(_testAssetsManager.AssetsRoot, "EmptyLibrary")); client.Initialize(testSource); // Error for invalid project.json var messages = client.DrainAllMessages(); messages.Single(msg => msg.MessageType == MessageTypes.Error) .Payload.AsJObject() .AssertProperty("Path", v => v.Contains("IncorrectProjectJson")); // Successfully initialize the other project messages.Single(msg => msg.MessageType == MessageTypes.ProjectInformation) .Payload.AsJObject() .AssertProperty("Name", v => string.Equals(v, "EmptyLibrary", StringComparison.Ordinal)); // Successfully initialize another project afterwards client.Initialize(Path.Combine(_testAssetsManager.AssetsRoot, "EmptyConsoleApp")); messages = client.DrainAllMessages(); messages.Single(msg => msg.MessageType == MessageTypes.ProjectInformation) .Payload.AsJObject() .AssertProperty("Name", v => string.Equals(v, "EmptyConsoleApp", StringComparison.Ordinal)); } } [Fact] public void InvalidGlobalJson() { var testAssetsPath = Path.Combine(RepoRoot, "TestAssets", "ProjectModelServer"); var assetsManager = new TestAssetsManager(testAssetsPath); var testSource = assetsManager.CreateTestInstance("IncorrectGlobalJson"); using (var server = new DthTestServer(_loggerFactory)) using (var client = new DthTestClient(server, _loggerFactory)) { client.Initialize(Path.Combine(testSource.TestRoot, "src", "Project1")); var messages = client.DrainAllMessages(); messages.ContainsMessage(MessageTypes.Error) .Single().Payload.AsJObject() .AssertProperty("Path", v => v.Contains("InvalidGlobalJson")); } } [Fact] public void RecoverFromGlobalError() { var testProject = _testAssetsManager.CreateTestInstance("EmptyConsoleApp") .WithLockFiles() .TestRoot; using (var server = new DthTestServer(_loggerFactory)) using (var client = new DthTestClient(server, _loggerFactory)) { var projectFile = Path.Combine(testProject, Project.FileName); var content = File.ReadAllText(projectFile); File.WriteAllText(projectFile, content + "}"); client.Initialize(testProject); var messages = client.DrainAllMessages(); messages.ContainsMessage(MessageTypes.Error); File.WriteAllText(projectFile, content); client.SendPayLoad(testProject, MessageTypes.FilesChanged); var clearError = client.DrainTillFirst(MessageTypes.Error); clearError.Payload.AsJObject().AssertProperty("Message", null as string); } } [Theory] [InlineData(500, true)] [InlineData(3000, false)] public void WaitForLockFileReleased(int occupyFileFor, bool expectSuccess) { var testProject = _testAssetsManager.CreateTestInstance("EmptyConsoleApp") .WithLockFiles() .TestRoot; using (var server = new DthTestServer(_loggerFactory)) using (var client = new DthTestClient(server, _loggerFactory)) { var lockFilePath = Path.Combine(testProject, LockFile.FileName); var lockFileContent = File.ReadAllText(lockFilePath); var fs = new FileStream(lockFilePath, FileMode.Create, FileAccess.Write, FileShare.None); // Test the platform // A sharing violation is expected in following code. Otherwise the FileSteam is not implemented correctly. Assert.ThrowsAny(() => { new FileStream(lockFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); }); var task = Task.Run(() => { // WorkspaceContext will try to open the lock file for 3 times with 500 ms interval in between. Thread.Sleep(occupyFileFor); fs.Dispose(); }); client.Initialize(testProject); var messages = client.DrainAllMessages(); if (expectSuccess) { messages.AssertDoesNotContain(MessageTypes.Error); } else { messages.ContainsMessage(MessageTypes.Error); } } } [Fact] public void MSBuildReferenceTest() { var testProject = Path.Combine(RepoRoot, "TestAssets", "ProjectModelServer", "MSBuildReferencesProjects", "BasicCase01", "src", "ConsoleApp13"); using (var server = new DthTestServer(_loggerFactory)) using (var client = new DthTestClient(server, _loggerFactory)) { client.Initialize(testProject); var messages = client.DrainAllMessages(); var classLibraries = new HashSet(new string[] { "ClassLibrary1", "ClassLibrary2", "ClassLibrary3" }); var dependencies = messages.RetrieveSingleMessage(MessageTypes.Dependencies); foreach (var each in classLibraries) { dependencies.RetrieveDependency(each) .AssertProperty("Type", LibraryType.MSBuildProject.ToString()) .AssertProperty("MSBuildProjectPath", AssertPathsEqual(Path.Combine("..", "..", each, $"{each}.csproj"))) .AssertProperty("Resolved", true) .AssertProperty("Name", each) .AssertProperty("Errors", array => array.Count == 0) .AssertProperty("Warnings", array => array.Count == 0); } var references = messages.RetrieveSingleMessage(MessageTypes.References) .RetrievePayloadAs(); var projectReferences = references.RetrievePropertyAs("ProjectReferences"); Assert.Equal(3, projectReferences.Count); for (int i = 0; i < 3; ++i) { var projectRef = projectReferences.RetrieveArraryElementAs(i); var name = projectRef["Name"].Value(); Assert.True(classLibraries.Contains(name)); projectRef.AssertProperty("Path", path => path.Contains(Path.Combine("BasicCase01", name))) .AssertProperty("MSBuildProjectPath", AssertPathsEqual(Path.Combine("..", "..", name, $"{name}.csproj"))); } var fileReferences = references.RetrievePropertyAs("FileReferences") .Select(each => each.Value()) .ToArray(); Assert.Equal(7, fileReferences.Length); foreach (var each in classLibraries) { fileReferences.Contains(Path.Combine("BasicCase01", "ClassLibrary1", "bin", "Debug", $"{each}.dll")); } } } private static Func AssertPathsEqual(string expectedString) { return (string t) => { if (t.Contains('/')) { t = t.Replace('/', Path.DirectorySeparatorChar); } else if (t.Contains('\\')) { t = t.Replace('\\', Path.DirectorySeparatorChar); } return string.Equals(t, expectedString); }; } } }