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 sealed 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);
    }
}