File: WorkspaceTests\AdhocWorkspaceTests.cs
Web Access
Project: src\src\Workspaces\CoreTest\Microsoft.CodeAnalysis.Workspaces.UnitTests.csproj (Microsoft.CodeAnalysis.Workspaces.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#nullable disable
 
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
using CS = Microsoft.CodeAnalysis.CSharp;
 
namespace Microsoft.CodeAnalysis.UnitTests
{
    [UseExportProvider]
    [Trait(Traits.Feature, Traits.Features.Workspace)]
    public partial class AdhocWorkspaceTests : TestBase
    {
        [Fact]
        public void TestAddProject_ProjectInfo()
        {
            var info = ProjectInfo.Create(
                ProjectId.CreateNewId(),
                version: VersionStamp.Default,
                name: "TestProject",
                assemblyName: "TestProject.dll",
                language: LanguageNames.CSharp);
 
            using var ws = new AdhocWorkspace();
            var project = ws.AddProject(info);
            Assert.Equal(project, ws.CurrentSolution.Projects.FirstOrDefault());
            Assert.Equal(info.Name, project.Name);
            Assert.Equal(info.Id, project.Id);
            Assert.Equal(info.AssemblyName, project.AssemblyName);
            Assert.Equal(info.Language, project.Language);
        }
 
        [Fact]
        public void TestAddProject_NameAndLanguage()
        {
            using var ws = new AdhocWorkspace();
            var project = ws.AddProject("TestProject", LanguageNames.CSharp);
            Assert.Same(project, ws.CurrentSolution.Projects.FirstOrDefault());
            Assert.Equal("TestProject", project.Name);
            Assert.Equal(LanguageNames.CSharp, project.Language);
        }
 
        [Fact]
        public void TestAddDocument_DocumentInfo()
        {
            using var ws = new AdhocWorkspace();
            var project = ws.AddProject("TestProject", LanguageNames.CSharp);
            var info = DocumentInfo.Create(DocumentId.CreateNewId(project.Id), "code.cs");
            var doc = ws.AddDocument(info);
 
            Assert.Equal(ws.CurrentSolution.GetDocument(info.Id), doc);
            Assert.Equal(info.Name, doc.Name);
        }
 
        [Fact]
        public async Task TestAddDocument_NameAndTextAsync()
        {
            using var ws = new AdhocWorkspace();
            var project = ws.AddProject("TestProject", LanguageNames.CSharp);
            var name = "code.cs";
            var source = "class C {}";
            var doc = ws.AddDocument(project.Id, name, SourceText.From(source));
 
            Assert.Equal(name, doc.Name);
            Assert.Equal(source, (await doc.GetTextAsync()).ToString());
        }
 
        [Fact]
        public void TestAddSolution_SolutionInfo()
        {
            using var ws = new AdhocWorkspace();
            var pinfo = ProjectInfo.Create(
ProjectId.CreateNewId(),
version: VersionStamp.Default,
name: "TestProject",
assemblyName: "TestProject.dll",
language: LanguageNames.CSharp);
 
            var sinfo = SolutionInfo.Create(SolutionId.CreateNewId(), VersionStamp.Default, projects: [pinfo]);
 
            var solution = ws.AddSolution(sinfo);
 
            Assert.Same(ws.CurrentSolution, solution);
            Assert.Equal(solution.Id, sinfo.Id);
 
            Assert.Equal(sinfo.Projects.Count, solution.ProjectIds.Count);
            var project = solution.Projects.FirstOrDefault();
            Assert.NotNull(project);
            Assert.Equal(pinfo.Name, project.Name);
            Assert.Equal(pinfo.Id, project.Id);
            Assert.Equal(pinfo.AssemblyName, project.AssemblyName);
            Assert.Equal(pinfo.Language, project.Language);
        }
 
        [Fact]
        public void TestAddProjects()
        {
            var id1 = ProjectId.CreateNewId();
            var info1 = ProjectInfo.Create(
                id1,
                version: VersionStamp.Default,
                name: "TestProject1",
                assemblyName: "TestProject1.dll",
                language: LanguageNames.CSharp);
 
            var id2 = ProjectId.CreateNewId();
            var info2 = ProjectInfo.Create(
                id2,
                version: VersionStamp.Default,
                name: "TestProject2",
                assemblyName: "TestProject2.dll",
                language: LanguageNames.VisualBasic,
                projectReferences: [new ProjectReference(id1)]);
 
            using var ws = new AdhocWorkspace();
            ws.AddProjects([info1, info2]);
            var solution = ws.CurrentSolution;
            Assert.Equal(2, solution.ProjectIds.Count);
 
            var project1 = solution.GetProject(id1);
            Assert.Equal(info1.Name, project1.Name);
            Assert.Equal(info1.Id, project1.Id);
            Assert.Equal(info1.AssemblyName, project1.AssemblyName);
            Assert.Equal(info1.Language, project1.Language);
 
            var project2 = solution.GetProject(id2);
            Assert.Equal(info2.Name, project2.Name);
            Assert.Equal(info2.Id, project2.Id);
            Assert.Equal(info2.AssemblyName, project2.AssemblyName);
            Assert.Equal(info2.Language, project2.Language);
            Assert.Equal(1, project2.ProjectReferences.Count());
            Assert.Equal(id1, project2.ProjectReferences.First().ProjectId);
        }
 
        [Fact]
        public void TestAddProject_TryApplyChanges()
        {
            using var ws = new AdhocWorkspace();
            var pid = ProjectId.CreateNewId();
 
            var docInfo = DocumentInfo.Create(
                            DocumentId.CreateNewId(pid),
                            "MyDoc.cs",
                            loader: TextLoader.From(TextAndVersion.Create(SourceText.From(""), VersionStamp.Create())));
 
            var projInfo = ProjectInfo.Create(
                    pid,
                    VersionStamp.Create(),
                    "NewProject",
                    "NewProject.dll",
                    LanguageNames.CSharp,
                    documents: [docInfo]);
 
            var newSolution = ws.CurrentSolution.AddProject(projInfo);
 
            Assert.Equal(0, ws.CurrentSolution.Projects.Count());
 
            var result = ws.TryApplyChanges(newSolution);
            Assert.True(result);
 
            Assert.Equal(1, ws.CurrentSolution.Projects.Count());
            var proj = ws.CurrentSolution.Projects.First();
 
            Assert.Equal("NewProject", proj.Name);
            Assert.Equal("NewProject.dll", proj.AssemblyName);
            Assert.Equal(LanguageNames.CSharp, proj.Language);
            Assert.Equal(1, proj.Documents.Count());
 
            var doc = proj.Documents.First();
            Assert.Equal("MyDoc.cs", doc.Name);
        }
 
        [Fact]
        public void TestRemoveProject_TryApplyChanges()
        {
            var pid = ProjectId.CreateNewId();
            var info = ProjectInfo.Create(
                pid,
                version: VersionStamp.Default,
                name: "TestProject",
                assemblyName: "TestProject.dll",
                language: LanguageNames.CSharp);
 
            using var ws = new AdhocWorkspace();
            ws.AddProject(info);
 
            Assert.Equal(1, ws.CurrentSolution.Projects.Count());
 
            var newSolution = ws.CurrentSolution.RemoveProject(pid);
            Assert.Equal(0, newSolution.Projects.Count());
 
            var result = ws.TryApplyChanges(newSolution);
            Assert.True(result);
 
            Assert.Equal(0, ws.CurrentSolution.Projects.Count());
        }
 
        [Fact]
        public void TestOpenCloseDocument()
        {
            var pid = ProjectId.CreateNewId();
            var text = SourceText.From("public class C { }");
            var version = VersionStamp.Create();
            var docInfo = DocumentInfo.Create(DocumentId.CreateNewId(pid), "c.cs", loader: TextLoader.From(TextAndVersion.Create(text, version)));
            var projInfo = ProjectInfo.Create(
                pid,
                version: VersionStamp.Default,
                name: "TestProject",
                assemblyName: "TestProject.dll",
                language: LanguageNames.CSharp,
                documents: [docInfo]);
 
            using var ws = new AdhocWorkspace();
            ws.AddProject(projInfo);
            var doc = ws.CurrentSolution.GetDocument(docInfo.Id);
            Assert.False(doc.TryGetText(out var currentText));
 
            ws.OpenDocument(docInfo.Id);
 
            doc = ws.CurrentSolution.GetDocument(docInfo.Id);
            Assert.True(doc.TryGetText(out currentText));
            Assert.True(doc.TryGetTextVersion(out var currentVersion));
            Assert.Same(text, currentText);
            Assert.Equal(version, currentVersion);
 
            ws.CloseDocument(docInfo.Id);
 
            doc = ws.CurrentSolution.GetDocument(docInfo.Id);
            Assert.False(doc.TryGetText(out currentText));
        }
 
        [Fact]
        public void TestOpenCloseAdditionalDocument()
        {
            var pid = ProjectId.CreateNewId();
            var text = SourceText.From("public class C { }");
            var version = VersionStamp.Create();
            var docInfo = DocumentInfo.Create(DocumentId.CreateNewId(pid), "c.cs", loader: TextLoader.From(TextAndVersion.Create(text, version)));
            var projInfo = ProjectInfo.Create(
                pid,
                version: VersionStamp.Default,
                name: "TestProject",
                assemblyName: "TestProject.dll",
                language: LanguageNames.CSharp,
                additionalDocuments: [docInfo]);
 
            using var ws = new AdhocWorkspace();
            ws.AddProject(projInfo);
            var doc = ws.CurrentSolution.GetAdditionalDocument(docInfo.Id);
            Assert.False(doc.TryGetText(out var currentText));
 
            ws.OpenAdditionalDocument(docInfo.Id);
 
            doc = ws.CurrentSolution.GetAdditionalDocument(docInfo.Id);
            Assert.True(doc.TryGetText(out currentText));
            Assert.True(doc.TryGetTextVersion(out var currentVersion));
            Assert.Same(text, currentText);
            Assert.Equal(version, currentVersion);
 
            ws.CloseAdditionalDocument(docInfo.Id);
 
            doc = ws.CurrentSolution.GetAdditionalDocument(docInfo.Id);
            Assert.False(doc.TryGetText(out currentText));
        }
 
        [Fact]
        public void TestOpenCloseAnnalyzerConfigDocument()
        {
            var pid = ProjectId.CreateNewId();
            var text = SourceText.From("public class C { }");
            var version = VersionStamp.Create();
            var analyzerConfigDocFilePath = PathUtilities.CombineAbsoluteAndRelativePaths(Temp.CreateDirectory().Path, ".editorconfig");
            var docInfo = DocumentInfo.Create(
                    DocumentId.CreateNewId(pid),
                    name: ".editorconfig",
                    loader: TextLoader.From(TextAndVersion.Create(text, version, analyzerConfigDocFilePath)),
                    filePath: analyzerConfigDocFilePath);
            var projInfo = ProjectInfo.Create(
                pid,
                version: VersionStamp.Default,
                name: "TestProject",
                assemblyName: "TestProject.dll",
                language: LanguageNames.CSharp)
                .WithAnalyzerConfigDocuments([docInfo]);
 
            using var ws = new AdhocWorkspace();
            ws.AddProject(projInfo);
            var doc = ws.CurrentSolution.GetAnalyzerConfigDocument(docInfo.Id);
            Assert.False(doc.TryGetText(out var currentText));
 
            ws.OpenAnalyzerConfigDocument(docInfo.Id);
 
            doc = ws.CurrentSolution.GetAnalyzerConfigDocument(docInfo.Id);
            Assert.True(doc.TryGetText(out currentText));
            Assert.True(doc.TryGetTextVersion(out var currentVersion));
            Assert.Same(text, currentText);
            Assert.Equal(version, currentVersion);
 
            ws.CloseAnalyzerConfigDocument(docInfo.Id);
 
            doc = ws.CurrentSolution.GetAnalyzerConfigDocument(docInfo.Id);
            Assert.False(doc.TryGetText(out currentText));
        }
 
        [Fact]
        public async Task TestUpdatedDocumentHasTextVersionAsync()
        {
            var pid = ProjectId.CreateNewId();
            var text = SourceText.From("public class C { }");
            var version = VersionStamp.Create();
            var docInfo = DocumentInfo.Create(DocumentId.CreateNewId(pid), "c.cs", loader: TextLoader.From(TextAndVersion.Create(text, version)));
            var projInfo = ProjectInfo.Create(
                pid,
                version: VersionStamp.Default,
                name: "TestProject",
                assemblyName: "TestProject.dll",
                language: LanguageNames.CSharp,
                documents: [docInfo]);
 
            using var ws = new AdhocWorkspace();
            ws.AddProject(projInfo);
            var doc = ws.CurrentSolution.GetDocument(docInfo.Id);
            Assert.False(doc.TryGetText(out var currentText));
            Assert.False(doc.TryGetTextVersion(out var currentVersion));
 
            // cause text to load and show that TryGet now works for text and version
            currentText = await doc.GetTextAsync();
            Assert.True(doc.TryGetText(out currentText));
            Assert.True(doc.TryGetTextVersion(out currentVersion));
            Assert.Equal(version, currentVersion);
 
            // change document
            var root = await doc.GetSyntaxRootAsync();
            var newRoot = root.WithAdditionalAnnotations(new SyntaxAnnotation());
            Assert.NotSame(root, newRoot);
            var newDoc = doc.WithSyntaxRoot(newRoot);
            Assert.NotSame(doc, newDoc);
 
            // text is now unavailable since it must be constructed from tree
            Assert.False(newDoc.TryGetText(out currentText));
 
            // version is available because it is cached
            Assert.True(newDoc.TryGetTextVersion(out currentVersion));
 
            // access it the hard way
            var actualVersion = await newDoc.GetTextVersionAsync();
 
            // version is the same 
            Assert.Equal(currentVersion, actualVersion);
 
            // accessing text version did not cause text to be constructed.
            Assert.False(newDoc.TryGetText(out currentText));
 
            // now access text directly (force it to be constructed)
            var actualText = await newDoc.GetTextAsync();
            actualVersion = await newDoc.GetTextVersionAsync();
 
            // prove constructing text did not introduce a new version
            Assert.Equal(currentVersion, actualVersion);
        }
 
        [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/1174396")]
        public async Task TestUpdateCSharpLanguageVersionAsync()
        {
            using var ws = new AdhocWorkspace();
            var projid = ws.AddProject("TestProject", LanguageNames.CSharp).Id;
            var docid1 = ws.AddDocument(projid, "A.cs", SourceText.From("public class A { }")).Id;
            var docid2 = ws.AddDocument(projid, "B.cs", SourceText.From("public class B { }")).Id;
 
            var pws = new WorkspaceWithPartialSemantics(ws.CurrentSolution);
            var proj = pws.CurrentSolution.GetProject(projid);
            var comp = await proj.GetCompilationAsync();
 
            // change language version
            var parseOptions = proj.ParseOptions as CS.CSharpParseOptions;
            pws.SetParseOptions(projid, parseOptions.WithLanguageVersion(CS.LanguageVersion.CSharp3));
 
            // get partial semantics doc
            var frozen = pws.CurrentSolution.GetDocument(docid1).WithFrozenPartialSemantics(CancellationToken.None);
        }
 
        public class WorkspaceWithPartialSemantics : Workspace
        {
            public WorkspaceWithPartialSemantics(Solution solution)
                : base(solution.Workspace.Services.HostServices, solution.Workspace.Kind)
            {
                this.SetCurrentSolutionEx(solution);
            }
 
            protected internal override bool PartialSemanticsEnabled
            {
                get { return true; }
            }
 
            public void SetParseOptions(ProjectId id, ParseOptions options)
                => base.OnParseOptionsChanged(id, options);
        }
 
        [Fact]
        public async Task TestChangeDocumentName_TryApplyChanges()
        {
            using var ws = new AdhocWorkspace();
            var projectId = ws.AddProject("TestProject", LanguageNames.CSharp).Id;
            var originalDoc = ws.AddDocument(projectId, "TestDocument", SourceText.From(""));
            Assert.Equal("TestDocument", originalDoc.Name);
 
            var newName = "ChangedName";
            var changedDoc = originalDoc.WithName(newName);
            Assert.Equal(newName, changedDoc.Name);
 
            var tcs = new TaskCompletionSource<bool>();
            ws.WorkspaceChanged += (s, args) =>
            {
                if (args.Kind == WorkspaceChangeKind.DocumentInfoChanged
                    && args.DocumentId == originalDoc.Id)
                {
                    tcs.SetResult(true);
                }
            };
 
            Assert.True(ws.TryApplyChanges(changedDoc.Project.Solution));
 
            var appliedDoc = ws.CurrentSolution.GetDocument(originalDoc.Id);
            Assert.Equal(newName, appliedDoc.Name);
 
            await Task.WhenAny(tcs.Task, Task.Delay(1000));
            Assert.True(tcs.Task.IsCompleted && tcs.Task.Result);
        }
 
        [Fact]
        public async Task TestChangeDocumentFolders_TryApplyChanges()
        {
            using var ws = new AdhocWorkspace();
            var projectId = ws.AddProject("TestProject", LanguageNames.CSharp).Id;
            var originalDoc = ws.AddDocument(projectId, "TestDocument", SourceText.From(""));
 
            Assert.Equal(0, originalDoc.Folders.Count);
 
            var changedDoc = originalDoc.WithFolders(["A", "B"]);
            Assert.Equal(2, changedDoc.Folders.Count);
            Assert.Equal("A", changedDoc.Folders[0]);
            Assert.Equal("B", changedDoc.Folders[1]);
 
            var tcs = new TaskCompletionSource<bool>();
            ws.WorkspaceChanged += (s, args) =>
            {
                if (args.Kind == WorkspaceChangeKind.DocumentInfoChanged
                    && args.DocumentId == originalDoc.Id)
                {
                    tcs.SetResult(true);
                }
            };
 
            Assert.True(ws.TryApplyChanges(changedDoc.Project.Solution));
 
            var appliedDoc = ws.CurrentSolution.GetDocument(originalDoc.Id);
            Assert.Equal(2, appliedDoc.Folders.Count);
            Assert.Equal("A", appliedDoc.Folders[0]);
            Assert.Equal("B", appliedDoc.Folders[1]);
 
            await Task.WhenAny(tcs.Task, Task.Delay(1000));
            Assert.True(tcs.Task.IsCompleted && tcs.Task.Result);
        }
 
        [Fact]
        public async Task TestChangeDocumentFilePath_TryApplyChanges()
        {
            using var ws = new AdhocWorkspace();
            var projectId = ws.AddProject("TestProject", LanguageNames.CSharp).Id;
 
            var originalDoc = ws.AddDocument(projectId, "TestDocument", SourceText.From(""));
            Assert.Null(originalDoc.FilePath);
 
            var newPath = @"\goo\TestDocument.cs";
            var changedDoc = originalDoc.WithFilePath(newPath);
            Assert.Equal(newPath, changedDoc.FilePath);
 
            var tcs = new TaskCompletionSource<bool>();
            ws.WorkspaceChanged += (s, args) =>
            {
                if (args.Kind == WorkspaceChangeKind.DocumentInfoChanged
                    && args.DocumentId == originalDoc.Id)
                {
                    tcs.SetResult(true);
                }
            };
 
            Assert.True(ws.TryApplyChanges(changedDoc.Project.Solution));
 
            var appliedDoc = ws.CurrentSolution.GetDocument(originalDoc.Id);
            Assert.Equal(newPath, appliedDoc.FilePath);
 
            await Task.WhenAny(tcs.Task, Task.Delay(1000));
            Assert.True(tcs.Task.IsCompleted && tcs.Task.Result);
        }
 
        [Fact]
        public async Task TestChangeDocumentSourceCodeKind_TryApplyChanges()
        {
            using var ws = new AdhocWorkspace();
            var projectId = ws.AddProject("TestProject", LanguageNames.CSharp).Id;
 
            var originalDoc = ws.AddDocument(projectId, "TestDocument", SourceText.From(""));
            Assert.Equal(SourceCodeKind.Regular, originalDoc.SourceCodeKind);
 
            var changedDoc = originalDoc.WithSourceCodeKind(SourceCodeKind.Script);
            Assert.Equal(SourceCodeKind.Script, changedDoc.SourceCodeKind);
 
            var tcs = new TaskCompletionSource<bool>();
            ws.WorkspaceChanged += (s, args) =>
            {
                if (args.Kind == WorkspaceChangeKind.DocumentInfoChanged
                    && args.DocumentId == originalDoc.Id)
                {
                    tcs.SetResult(true);
                }
            };
 
            Assert.True(ws.TryApplyChanges(changedDoc.Project.Solution));
 
            var appliedDoc = ws.CurrentSolution.GetDocument(originalDoc.Id);
            Assert.Equal(SourceCodeKind.Script, appliedDoc.SourceCodeKind);
 
            await Task.WhenAny(tcs.Task, Task.Delay(1000));
            Assert.True(tcs.Task.IsCompleted && tcs.Task.Result);
        }
 
        [Fact]
        public void TestChangeDocumentInfo_TryApplyChanges()
        {
            using var ws = new AdhocWorkspace();
            var projectId = ws.AddProject("TestProject", LanguageNames.CSharp).Id;
 
            var originalDoc = ws.AddDocument(projectId, "TestDocument", SourceText.From(""));
            Assert.Equal("TestDocument", originalDoc.Name);
            Assert.Equal(0, originalDoc.Folders.Count);
            Assert.Null(originalDoc.FilePath);
 
            var newName = "ChangedName";
            var newPath = @"\A\B\ChangedName.cs";
            var changedDoc = originalDoc.WithName(newName).WithFolders(["A", "B"]).WithFilePath(newPath);
 
            Assert.Equal(newName, changedDoc.Name);
            Assert.Equal(2, changedDoc.Folders.Count);
            Assert.Equal("A", changedDoc.Folders[0]);
            Assert.Equal("B", changedDoc.Folders[1]);
            Assert.Equal(newPath, changedDoc.FilePath);
 
            Assert.True(ws.TryApplyChanges(changedDoc.Project.Solution));
 
            var appliedDoc = ws.CurrentSolution.GetDocument(originalDoc.Id);
            Assert.Equal(newName, appliedDoc.Name);
            Assert.Equal(2, appliedDoc.Folders.Count);
            Assert.Equal("A", appliedDoc.Folders[0]);
            Assert.Equal("B", appliedDoc.Folders[1]);
            Assert.Equal(newPath, appliedDoc.FilePath);
        }
 
        [Fact]
        public void TestDefaultDocumentTextDifferencingService()
        {
            using var ws = new AdhocWorkspace();
            var service = ws.Services.GetService<IDocumentTextDifferencingService>();
            Assert.NotNull(service);
            Assert.Equal(typeof(DefaultDocumentTextDifferencingService), service.GetType());
        }
 
        [Fact, WorkItem("https://github.com/dotnet/roslyn/pull/67142")]
        public void TestNotGCRootedOnConstruction()
        {
            var composition = FeaturesTestCompositions.Features;
            var exportProvider = composition.ExportProviderFactory.CreateExportProvider();
            var adhocWorkspaceReference = ObjectReference.CreateFromFactory(
                static composition => new AdhocWorkspace(composition.GetHostServices()),
                composition);
 
            // Verify the GC can reclaim member for a workspace which has not been disposed.
            adhocWorkspaceReference.AssertReleased();
 
            // Keep the export provider alive longer than the workspace to further ensure that the workspace is not GC
            // rooted within the export provider instance.
            GC.KeepAlive(exportProvider);
        }
    }
}