File: BackEnd\Lookup_Tests.cs
Web Access
Project: ..\..\..\src\Build.UnitTests\Microsoft.Build.Engine.UnitTests.csproj (Microsoft.Build.Engine.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Build.BackEnd;
using Microsoft.Build.Collections;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Shouldly;
using Xunit;
 
#nullable disable
 
namespace Microsoft.Build.UnitTests.BackEnd
{
    public class Lookup_Tests
    {
        /// <summary>
        /// Primary group contains an item for a type and secondary does;
        /// primary item should be returned instead of the secondary item.
        /// </summary>
        [Fact]
        public void SecondaryItemShadowedByPrimaryItem()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            table1.Add(new ProjectItemInstance(project, "i1", "a1", project.FullPath));
            table1.Add(new ProjectItemInstance(project, "i2", "a%3b1", project.FullPath));
            Lookup lookup = LookupHelpers.CreateLookup(table1);
 
            lookup.EnterScope("x");
            lookup.PopulateWithItem(new ProjectItemInstance(project, "i1", "a2", project.FullPath));
            lookup.PopulateWithItem(new ProjectItemInstance(project, "i2", "a%282", project.FullPath));
 
            // Should return the item from the primary, not the secondary table
            Assert.Equal("a2", lookup.GetItems("i1").First().EvaluatedInclude);
            Assert.Equal("a(2", lookup.GetItems("i2").First().EvaluatedInclude);
        }
 
        /// <summary>
        /// Primary group does not contain an item for a type but secondary does;
        /// secondary item should be returned.
        /// </summary>
        [Fact]
        public void SecondaryItemNotShadowedByPrimaryItem()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            table1.Add(new ProjectItemInstance(project, "i1", "a1", project.FullPath));
            table1.Add(new ProjectItemInstance(project, "i2", "a%3b1", project.FullPath));
            Lookup lookup = LookupHelpers.CreateLookup(table1);
 
            lookup.EnterScope("x");
 
            // Should return item from the secondary table.
            Assert.Equal("a1", lookup.GetItems("i1").First().EvaluatedInclude);
            Assert.Equal("a;1", lookup.GetItems("i2").First().EvaluatedInclude);
        }
 
        /// <summary>
        /// No items of that type: should return empty group rather than null
        /// </summary>
        [Fact]
        public void UnknownItemType()
        {
            Lookup lookup = LookupHelpers.CreateEmptyLookup();
 
            lookup.EnterScope("x"); // Doesn't matter really
 
            Assert.Empty(lookup.GetItems("i1"));
        }
 
        /// <summary>
        /// Adds accumulate as we lookup in the tables
        /// </summary>
        [Fact]
        public void AddsAreCombinedWithPopulates()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            // One item in the project
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            table1.Add(new ProjectItemInstance(project, "i1", "a1", project.FullPath));
            Lookup lookup = LookupHelpers.CreateLookup(table1);
 
            // We see the one item
            Assert.Equal("a1", lookup.GetItems("i1").First().EvaluatedInclude);
            Assert.Single(lookup.GetItems("i1"));
 
            // One item in the project
            Assert.Equal("a1", table1["i1"].First().EvaluatedInclude);
            Assert.Single(table1["i1"]);
 
            // Start a target
            Lookup.Scope enteredScope = lookup.EnterScope("x");
 
            // We see the one item
            Assert.Equal("a1", lookup.GetItems("i1").First().EvaluatedInclude);
            Assert.Single(lookup.GetItems("i1"));
 
            // One item in the project
            Assert.Equal("a1", table1["i1"].First().EvaluatedInclude);
            Assert.Single(table1["i1"]);
 
            // Start a task (eg) and add a new item
            Lookup.Scope enteredScope2 = lookup.EnterScope("x");
            lookup.AddNewItem(new ProjectItemInstance(project, "i1", "a2", project.FullPath));
 
            // Now we see two items
            Assert.Equal("a1", lookup.GetItems("i1").First().EvaluatedInclude);
            Assert.Equal("a2", lookup.GetItems("i1").ElementAt(1).EvaluatedInclude);
            Assert.Equal(2, lookup.GetItems("i1").Count);
 
            // But there's still one item in the project
            Assert.Equal("a1", table1["i1"].First().EvaluatedInclude);
            Assert.Single(table1["i1"]);
 
            // Finish the task
            enteredScope2.LeaveScope();
 
            // We still see two items
            Assert.Equal("a1", lookup.GetItems("i1").First().EvaluatedInclude);
            Assert.Equal("a2", lookup.GetItems("i1").ElementAt(1).EvaluatedInclude);
            Assert.Equal(2, lookup.GetItems("i1").Count);
 
            // But there's still one item in the project
            Assert.Equal("a1", table1["i1"].First().EvaluatedInclude);
            Assert.Single(table1["i1"]);
 
            // Finish the target
            enteredScope.LeaveScope();
 
            // We still see two items
            Assert.Equal("a1", lookup.GetItems("i1").First().EvaluatedInclude);
            Assert.Equal("a2", lookup.GetItems("i1").ElementAt(1).EvaluatedInclude);
            Assert.Equal(2, lookup.GetItems("i1").Count);
 
            // And now the items have gotten put into the global group
            Assert.Equal("a1", table1["i1"].First().EvaluatedInclude);
            Assert.Equal("a2", table1["i1"].ElementAt(1).EvaluatedInclude);
            Assert.Equal(2, table1["i1"].Count);
        }
 
        /// <summary>
        /// Adds when duplicate removal is enabled removes only duplicates.  Tests only item specs, not metadata differences
        /// </summary>
        [Fact]
        public void AddsWithDuplicateRemovalItemSpecsOnly()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            // One item in the project
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            table1.Add(new ProjectItemInstance(project, "i1", "a1", project.FullPath));
 
            // Add an existing duplicate
            table1.Add(new ProjectItemInstance(project, "i1", "a1", project.FullPath));
            Lookup lookup = LookupHelpers.CreateLookup(table1);
 
            var scope = lookup.EnterScope("test");
 
            // This one should not get added
            ProjectItemInstance[] newItems = new ProjectItemInstance[]
            {
                new ProjectItemInstance(project, "i1", "a1", project.FullPath), // Should not get added
                new ProjectItemInstance(project, "i1", "a2", project.FullPath), // Should get added
            };
 
            // Perform the addition
            lookup.AddNewItemsOfItemType("i1", newItems, doNotAddDuplicates: true);
 
            var group = lookup.GetItems("i1");
 
            // We should have the original two duplicates plus one new addition.
            Assert.Equal(3, group.Count);
 
            // Only two of the items should have the 'a1' include.
            Assert.Equal(2, group.Where(item => item.EvaluatedInclude == "a1").Count());
            // And ensure the other item got added.
            Assert.Single(group, item => item.EvaluatedInclude == "a2");
 
            scope.LeaveScope();
 
            group = lookup.GetItems("i1");
 
            // We should have the original two duplicates plus one new addition.
            Assert.Equal(3, group.Count);
 
            // Only two of the items should have the 'a1' include.
            Assert.Equal(2, group.Where(item => item.EvaluatedInclude == "a1").Count());
            // And ensure the other item got added.
            Assert.Single(group, item => item.EvaluatedInclude == "a2");
        }
 
        /// <summary>
        /// Adds when duplicate removal is enabled removes only duplicates.  Tests only item specs, not metadata differences
        /// </summary>
        [Fact]
        public void AddsWithDuplicateRemovalWithMetadata()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
 
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
 
            // Two items, differ only by metadata
            table1.Add(new ProjectItemInstance(project, "i1", "a1", new KeyValuePair<string, string>[] { new KeyValuePair<string, string>("m1", "m1") }, project.FullPath));
            table1.Add(new ProjectItemInstance(project, "i1", "a1", new KeyValuePair<string, string>[] { new KeyValuePair<string, string>("m1", "m2") }, project.FullPath));
            Lookup lookup = LookupHelpers.CreateLookup(table1);
 
            var scope = lookup.EnterScope("test");
 
            // This one should not get added
            ProjectItemInstance[] newItems = new ProjectItemInstance[]
            {
                new ProjectItemInstance(project, "i1", "a1", project.FullPath), // Should get added
                new ProjectItemInstance(project, "i1", "a2", new KeyValuePair<string, string>[] { new KeyValuePair<string, string>( "m1", "m1") }, project.FullPath), // Should get added
                new ProjectItemInstance(project, "i1", "a1", new KeyValuePair<string, string>[] { new KeyValuePair<string, string>( "m1", "m1") }, project.FullPath), // Should not get added
                new ProjectItemInstance(project, "i1", "a1", new KeyValuePair<string, string>[] { new KeyValuePair<string, string>( "m1", "m3") }, project.FullPath), // Should get added
            };
 
            // Perform the addition
            lookup.AddNewItemsOfItemType("i1", newItems, doNotAddDuplicates: true);
 
            var group = lookup.GetItems("i1");
 
            // We should have the original two duplicates plus one new addition.
            Assert.Equal(5, group.Count);
 
            // Four of the items will have the a1 include
            Assert.Equal(4, group.Where(item => item.EvaluatedInclude == "a1").Count());
 
            // One item will have the a2 include
            Assert.Single(group, item => item.EvaluatedInclude == "a2");
 
            scope.LeaveScope();
 
            group = lookup.GetItems("i1");
 
            // We should have the original two duplicates plus one new addition.
            Assert.Equal(5, group.Count);
 
            // Four of the items will have the a1 include
            Assert.Equal(4, group.Where(item => item.EvaluatedInclude == "a1").Count());
 
            // One item will have the a2 include
            Assert.Single(group, item => item.EvaluatedInclude == "a2");
        }
 
        [Fact]
        public void Removes()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            // One item in the project
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a1", project.FullPath);
            table1.Add(item1);
            Lookup lookup = LookupHelpers.CreateLookup(table1);
 
            // Start a target
            Lookup.Scope enteredScope = lookup.EnterScope("x");
 
            // Start a task (eg) and add a new item
            Lookup.Scope enteredScope2 = lookup.EnterScope("x");
            ProjectItemInstance item2 = new ProjectItemInstance(project, "i1", "a2", project.FullPath);
            lookup.AddNewItem(item2);
 
            // Remove one item
            lookup.RemoveItems("i1", [item1]);
 
            // We see one item
            Assert.Single(lookup.GetItems("i1"));
            Assert.Equal("a2", lookup.GetItems("i1").First().EvaluatedInclude);
 
            // Remove the other item
            lookup.RemoveItems("i1", [item2]);
 
            // We see no items
            Assert.Empty(lookup.GetItems("i1"));
 
            // Finish the task
            enteredScope2.LeaveScope();
 
            // We still see no items
            Assert.Empty(lookup.GetItems("i1"));
 
            // But there's still one item in the project
            Assert.Equal("a1", table1["i1"].First().EvaluatedInclude);
            Assert.Single(table1["i1"]);
 
            // Finish the target
            enteredScope.LeaveScope();
 
            // We still see no items
            Assert.Empty(lookup.GetItems("i1"));
 
            // And now there are no items in the project either
            Assert.Empty(table1["i1"]);
        }
 
        [Fact]
        public void RemoveItemPopulatedInLowerScope()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            Lookup lookup = LookupHelpers.CreateLookup(table1);
            ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath);
 
            // Start a target
            Lookup.Scope enteredScope = lookup.EnterScope("x");
 
            // There's one item in this batch
            lookup.PopulateWithItem(item1);
 
            // We see it
            Assert.Single(lookup.GetItems("i1"));
 
            // Make a clone so we can keep an eye on that item
            Lookup lookup2 = lookup.Clone();
 
            // We can see the item in the clone
            Assert.Single(lookup2.GetItems("i1"));
 
            // Start a task (eg)
            Lookup.Scope enteredScope2 = lookup.EnterScope("x");
 
            // We see the item below
            Assert.Single(lookup.GetItems("i1"));
 
            // Remove that item
            lookup.RemoveItems("i1", [item1]);
 
            // We see no items
            Assert.Empty(lookup.GetItems("i1"));
 
            // The clone is unaffected so far
            Assert.Single(lookup2.GetItems("i1"));
 
            // Finish the task
            enteredScope2.LeaveScope();
 
            // We still see no items
            Assert.Empty(lookup.GetItems("i1"));
 
            // But now the clone doesn't either
            Assert.Empty(lookup2.GetItems("i1"));
 
            // Finish the target
            enteredScope.LeaveScope();
 
            // We still see no items
            Assert.Empty(lookup.GetItems("i1"));
            Assert.Empty(lookup2.GetItems("i1"));
        }
 
        [Fact]
        public void RemoveItemAddedInLowerScope()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            Lookup lookup = LookupHelpers.CreateLookup(table1);
 
            // Start a target
            Lookup.Scope enteredScope = lookup.EnterScope("x");
 
            // Add an item
            ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath);
            lookup.AddNewItem(item1);
 
            // Start a task (eg)
            Lookup.Scope enteredScope2 = lookup.EnterScope("x");
 
            // We see the item below
            Assert.Single(lookup.GetItems("i1"));
 
            // Remove that item
            lookup.RemoveItems("i1", [item1]);
 
            // We see no items
            Assert.Empty(lookup.GetItems("i1"));
 
            // Finish the task
            enteredScope2.LeaveScope();
 
            // We still see no items
            Assert.Empty(lookup.GetItems("i1"));
 
            // Finish the target
            enteredScope.LeaveScope();
 
            // We still see no items
            Assert.Empty(lookup.GetItems("i1"));
        }
 
        /// <summary>
        /// Ensure that once keepOnlySpecified is set to true, it remains in effect.
        /// </summary>
        [Fact]
        public void KeepMetadataOnlySpecifiedPropagate1()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            Lookup lookup = LookupHelpers.CreateLookup(table1);
 
            Lookup.Scope enteredScope = lookup.EnterScope("x");
 
            // Add an item with m=m1
            ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath);
            item1.SetMetadata("m1", "m1");
            item1.SetMetadata("m2", "m2");
            lookup.AddNewItem(item1);
 
            Lookup.Scope enteredScope2 = lookup.EnterScope("x");
 
            // Get rid of all of the metadata.
            Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: true);
            ICollection<ProjectItemInstance> group = lookup.GetItems(item1.ItemType);
            lookup.ModifyItems(item1.ItemType, group, newMetadata);
 
            group = lookup.GetItems("i1");
            Assert.Single(group);
 
            // m1 and m2 are gone.
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m1"));
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m2"));
 
            enteredScope2.LeaveScope();
 
            // Add metadata m3.
            Lookup.MetadataModifications newMetadata2 = new Lookup.MetadataModifications(keepOnlySpecified: false);
            newMetadata2.Add("m3", "m3");
            group = lookup.GetItems(item1.ItemType);
            lookup.ModifyItems(item1.ItemType, group, newMetadata2);
 
            group = lookup.GetItems("i1");
            Assert.Single(group);
 
            // m1 and m2 are gone
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m1"));
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m2"));
 
            // m3 is still there.
            Assert.Equal("m3", group.First().GetMetadataValue("m3"));
 
            enteredScope.LeaveScope();
 
            group = lookup.GetItems("i1");
            Assert.Single(group);
 
            // m1 and m2 are gone
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m1"));
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m2"));
 
            // m3 is still there.
            Assert.Equal("m3", group.First().GetMetadataValue("m3"));
        }
 
        /// <summary>
        /// Ensure that if keepOnlySpecified is specified after some metadata have been set in a higher scope that it will
        /// eliminate that metadata are the current scope and beyond.
        /// </summary>
        [Fact]
        public void KeepMetadataOnlySpecifiedPropagate2()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            Lookup lookup = LookupHelpers.CreateLookup(table1);
 
            Lookup.Scope enteredScope = lookup.EnterScope("x");
 
            // Add an item with m=m1
            ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath);
            item1.SetMetadata("m1", "m1");
            item1.SetMetadata("m2", "m2");
            lookup.AddNewItem(item1);
 
            Lookup.Scope enteredScope2 = lookup.EnterScope("x");
 
            // Add m3 metadata
            Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false);
            newMetadata.Add("m3", "m3");
            ICollection<ProjectItemInstance> group = lookup.GetItems(item1.ItemType);
            lookup.ModifyItems(item1.ItemType, group, newMetadata);
 
            group = lookup.GetItems("i1");
            Assert.Single(group);
 
            // All metadata are present
            Assert.Equal("m1", group.First().GetMetadataValue("m1"));
            Assert.Equal("m2", group.First().GetMetadataValue("m2"));
            Assert.Equal("m3", group.First().GetMetadataValue("m3"));
 
            enteredScope2.LeaveScope();
 
            // Now clear metadata
            Lookup.MetadataModifications newMetadata2 = new Lookup.MetadataModifications(keepOnlySpecified: true);
            group = lookup.GetItems(item1.ItemType);
            lookup.ModifyItems(item1.ItemType, group, newMetadata2);
 
            group = lookup.GetItems("i1");
            Assert.Single(group);
 
            // All metadata are gone
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m1"));
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m2"));
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m3"));
 
            enteredScope.LeaveScope();
 
            group = lookup.GetItems("i1");
            Assert.Single(group);
 
            // All metadata are gone
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m1"));
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m2"));
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m3"));
        }
 
        /// <summary>
        /// Ensure that once keepOnlySpecified is set to true, it remains in effect, but that metadata explicitly added at subsequent levels is still retained.
        /// </summary>
        [Fact]
        public void KeepMetadataOnlySpecifiedPropagate3()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            Lookup lookup = LookupHelpers.CreateLookup(table1);
 
            Lookup.Scope enteredScope = lookup.EnterScope("x");
 
            // Add an item with m=m1
            ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath);
            item1.SetMetadata("m1", "m1");
            item1.SetMetadata("m2", "m2");
            lookup.AddNewItem(item1);
 
            Lookup.Scope enteredScope2 = lookup.EnterScope("x");
 
            // Get rid of all of the metadata, then add m3
            Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: true);
            newMetadata.Add("m3", "m3");
            ICollection<ProjectItemInstance> group = lookup.GetItems(item1.ItemType);
            lookup.ModifyItems(item1.ItemType, group, newMetadata);
 
            group = lookup.GetItems("i1");
            Assert.Single(group);
 
            // m1 and m2 are gone.
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m1"));
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m2"));
 
            // m3 is still there.
            Assert.Equal("m3", group.First().GetMetadataValue("m3"));
 
            enteredScope2.LeaveScope();
 
            // Add metadata m4.
            Lookup.MetadataModifications newMetadata2 = new Lookup.MetadataModifications(keepOnlySpecified: true);
            newMetadata2.Add("m4", "m4");
            group = lookup.GetItems(item1.ItemType);
            lookup.ModifyItems(item1.ItemType, group, newMetadata2);
 
            group = lookup.GetItems("i1");
            Assert.Single(group);
 
            // m1, m2 and m3 are gone
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m1"));
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m2"));
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m3"));
 
            // m4 is still there.
            Assert.Equal("m4", group.First().GetMetadataValue("m4"));
 
            enteredScope.LeaveScope();
 
            group = lookup.GetItems("i1");
            Assert.Single(group);
 
            // m1, m2 and m3 are gone
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m1"));
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m2"));
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m3"));
 
            // m4 is still there.
            Assert.Equal("m4", group.First().GetMetadataValue("m4"));
        }
 
 
        /// <summary>
        /// Ensure that once keepOnlySpecified is set to true, it remains in effect, and that if a metadata modification is declared as 'keep value' that
        /// the value as lower scopes is retained.
        /// </summary>
        [Fact]
        public void KeepMetadataOnlySpecifiedPropagate4()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            Lookup lookup = LookupHelpers.CreateLookup(table1);
 
            Lookup.Scope enteredScope = lookup.EnterScope("x");
 
            // Add an item with m=m1
            ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath);
            item1.SetMetadata("m1", "m1");
            item1.SetMetadata("m2", "m2");
            lookup.AddNewItem(item1);
 
            Lookup.Scope enteredScope2 = lookup.EnterScope("x");
 
            // Get rid of all of the metadata, then add m3
            Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: true);
            newMetadata.Add("m3", "m3");
            ICollection<ProjectItemInstance> group = lookup.GetItems(item1.ItemType);
            lookup.ModifyItems(item1.ItemType, group, newMetadata);
 
            group = lookup.GetItems("i1");
            Assert.Single(group);
 
            // m1 and m2 are gone.
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m1"));
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m2"));
 
            // m3 is still there.
            Assert.Equal("m3", group.First().GetMetadataValue("m3"));
 
            enteredScope2.LeaveScope();
 
            // Keep m3.
            Lookup.MetadataModifications newMetadata2 = new Lookup.MetadataModifications(keepOnlySpecified: true);
            newMetadata2["m3"] = Lookup.MetadataModification.CreateFromNoChange();
            group = lookup.GetItems(item1.ItemType);
            lookup.ModifyItems(item1.ItemType, group, newMetadata2);
 
            group = lookup.GetItems("i1");
            Assert.Single(group);
 
            // m1 and m2 are gone
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m1"));
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m2"));
 
            // m3 is still there
            Assert.Equal("m3", group.First().GetMetadataValue("m3"));
 
            enteredScope.LeaveScope();
 
            group = lookup.GetItems("i1");
            Assert.Single(group);
 
            // m1 and m2 are gone
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m1"));
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m2"));
 
            // m3 is still there.
            Assert.Equal("m3", group.First().GetMetadataValue("m3"));
        }
 
        /// <summary>
        /// Ensure that when keepOnlySpecified is true, we will clear all metadata unless it is retained using the 'NoChange' modification type.
        /// </summary>
        [Fact]
        public void KeepMetadataOnlySpecified()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            Lookup lookup = LookupHelpers.CreateLookup(table1);
 
            Lookup.Scope enteredScope = lookup.EnterScope("x");
 
            // Add an item with m=m1
            ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath);
            item1.SetMetadata("m1", "m1");
            item1.SetMetadata("m2", "m2");
            lookup.AddNewItem(item1);
 
            Lookup.Scope enteredScope2 = lookup.EnterScope("x");
 
            // Test keeping only specified metadata
            Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: true);
            newMetadata["m1"] = Lookup.MetadataModification.CreateFromNoChange();
            ICollection<ProjectItemInstance> group = lookup.GetItems(item1.ItemType);
            lookup.ModifyItems(item1.ItemType, group, newMetadata);
 
            group = lookup.GetItems("i1");
            Assert.Single(group);
 
            // m1 is still here.
            Assert.Equal("m1", group.First().GetMetadataValue("m1"));
 
            // m2 is gone
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m2"));
 
            enteredScope2.LeaveScope();
 
            group = lookup.GetItems("i1");
            Assert.Single(group);
 
            // m1 should still be here
            Assert.Equal("m1", group.First().GetMetadataValue("m1"));
 
            // m2 is gone.
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m2"));
 
            enteredScope.LeaveScope();
 
            group = lookup.GetItems("i1");
            Assert.Single(group);
 
            // m1 should still be here
            Assert.Equal("m1", group.First().GetMetadataValue("m1"));
 
            // m2 should not persist here either
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m2"));
        }
 
        [Fact]
        public void KeepMetadataOnlySpecifiedNoneSpecified()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            Lookup lookup = LookupHelpers.CreateLookup(table1);
 
            Lookup.Scope enteredScope = lookup.EnterScope("x");
 
            // Add an item with m=m1
            ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath);
            item1.SetMetadata("m1", "m1");
            item1.SetMetadata("m2", "m2");
            lookup.AddNewItem(item1);
 
            Lookup.Scope enteredScope2 = lookup.EnterScope("x");
 
            // Test keeping only specified metadata
            Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: true);
            ICollection<ProjectItemInstance> group = lookup.GetItems(item1.ItemType);
            lookup.ModifyItems(item1.ItemType, group, newMetadata);
 
            group = lookup.GetItems("i1");
            Assert.Single(group);
 
            // m1 and m2 are gone.
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m1"));
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m2"));
 
            enteredScope2.LeaveScope();
 
            group = lookup.GetItems("i1");
            Assert.Single(group);
 
            // m1 and m2 are gone.
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m1"));
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m2"));
 
            enteredScope.LeaveScope();
 
            group = lookup.GetItems("i1");
            Assert.Single(group);
 
            // m1 and m2 are gone.
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m1"));
            Assert.Equal(String.Empty, group.First().GetMetadataValue("m2"));
        }
 
        [Fact]
        public void ModifyItem()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            Lookup lookup = LookupHelpers.CreateLookup(table1);
 
            Lookup.Scope enteredScope = lookup.EnterScope("x");
 
            // Add an item with m=m1
            ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath);
            item1.SetMetadata("m", "m1");
            lookup.AddNewItem(item1);
 
            Lookup.Scope enteredScope2 = lookup.EnterScope("x");
 
            // Change the item to be m=m2
            Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false);
            newMetadata.Add("m", "m2");
            ICollection<ProjectItemInstance> group = new List<ProjectItemInstance>();
            group.Add(item1);
            lookup.ModifyItems(item1.ItemType, group, newMetadata);
 
            // Now it has m=m2
            group = lookup.GetItems("i1");
            Assert.Single(group);
            Assert.Equal("m2", group.First().GetMetadataValue("m"));
 
            // But the original item hasn't changed yet
            Assert.Equal("m1", item1.GetMetadataValue("m"));
 
            enteredScope2.LeaveScope();
 
            // It still has m=m2
            group = lookup.GetItems("i1");
            Assert.Single(group);
            Assert.Equal("m2", group.First().GetMetadataValue("m"));
 
            // The original item still hasn't changed
            // even though it was added in this scope
            Assert.Equal("m1", item1.GetMetadataValue("m"));
 
            enteredScope.LeaveScope();
 
            // It still has m=m2
            group = lookup.GetItems("i1");
            Assert.Single(group);
            Assert.Equal("m2", group.First().GetMetadataValue("m"));
 
            // But now the original item has changed
            Assert.Equal("m2", item1.GetMetadataValue("m"));
        }
 
        /// <summary>
        /// Modifications should be merged
        /// </summary>
        [Fact]
        public void ModifyItemModifiedInPreviousScope()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            Lookup lookup = LookupHelpers.CreateLookup(table1);
 
            // Add an item with m=m1 and n=n1
            ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath);
            item1.SetMetadata("m", "m1");
            table1.Add(item1);
 
            lookup.EnterScope("x");
 
            // Make a modification to the item to be m=m2
            Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false);
            newMetadata.Add("m", "m2");
            newMetadata.Add("n", "n2");
            ICollection<ProjectItemInstance> group = new List<ProjectItemInstance>();
            group.Add(item1);
            lookup.ModifyItems(item1.ItemType, group, newMetadata);
 
            lookup.EnterScope("x");
 
            // Make another modification to the item
            newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false);
            newMetadata.Add("m", "m3");
            newMetadata.Add("o", "o3");
            lookup.ModifyItems(item1.ItemType, group, newMetadata);
 
            // It's now m=m3, n=n2, o=o3
            group = lookup.GetItems("i1");
            Assert.Single(group);
            Assert.Equal("m3", group.First().GetMetadataValue("m"));
            Assert.Equal("n2", group.First().GetMetadataValue("n"));
            Assert.Equal("o3", group.First().GetMetadataValue("o"));
        }
 
        /// <summary>
        /// Modifications should be merged
        /// </summary>
        [Fact]
        public void ModifyItemTwiceInSameScope1()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            Lookup lookup = LookupHelpers.CreateLookup(table1);
 
            // Add an item with m=m1 and n=n1
            ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath);
            item1.SetMetadata("m", "m1");
            table1.Add(item1);
 
            lookup.EnterScope("x");
 
            // Make a modification to the item to be m=m2
            Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false);
            newMetadata.Add("m", "m2");
            ICollection<ProjectItemInstance> group = new List<ProjectItemInstance>();
            group.Add(item1);
            lookup.ModifyItems(item1.ItemType, group, newMetadata);
 
            // Make an unrelated modification to the item
            newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false);
            newMetadata.Add("n", "n1");
            lookup.ModifyItems(item1.ItemType, group, newMetadata);
 
            // It's now m=m2
            group = lookup.GetItems("i1");
            Assert.Single(group);
            Assert.Equal("m2", group.First().GetMetadataValue("m"));
        }
 
        /// <summary>
        /// Modifications should be merged
        /// </summary>
        [Fact]
        public void ModifyItemTwiceInSameScope2()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            Lookup lookup = LookupHelpers.CreateLookup(table1);
 
            // Add an item with m=m1 and n=n1 and o=o1
            ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath);
            item1.SetMetadata("m", "m1");
            item1.SetMetadata("n", "n1");
            item1.SetMetadata("o", "o1");
            table1.Add(item1);
 
            Lookup.Scope enteredScope = lookup.EnterScope("x");
 
            // It's still m=m1, n=n1, o=o1
            ICollection<ProjectItemInstance> group = lookup.GetItems("i1");
            Assert.Single(group);
            Assert.Equal("m1", group.First().GetMetadataValue("m"));
            Assert.Equal("n1", group.First().GetMetadataValue("n"));
            Assert.Equal("o1", group.First().GetMetadataValue("o"));
 
            // Make a modification to the item to be m=m2 and n=n2
            Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false);
            newMetadata.Add("m", "m2");
            newMetadata.Add("n", "n2");
            group = new List<ProjectItemInstance>();
            group.Add(item1);
            lookup.ModifyItems("i1", group, newMetadata);
 
            // It's now m=m2, n=n2, o=o1
            ICollection<ProjectItemInstance> foundGroup = lookup.GetItems("i1");
            Assert.Single(foundGroup);
            Assert.Equal("m2", foundGroup.First().GetMetadataValue("m"));
            Assert.Equal("n2", foundGroup.First().GetMetadataValue("n"));
            Assert.Equal("o1", foundGroup.First().GetMetadataValue("o"));
 
            // Make a modification to the item to be n=n3
            newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false);
            newMetadata.Add("n", "n3");
            lookup.ModifyItems("i1", group, newMetadata);
 
            // It's now m=m2, n=n3, o=o1
            foundGroup = lookup.GetItems("i1");
            Assert.Single(foundGroup);
            Assert.Equal("m2", foundGroup.First().GetMetadataValue("m"));
            Assert.Equal("n3", foundGroup.First().GetMetadataValue("n"));
            Assert.Equal("o1", foundGroup.First().GetMetadataValue("o"));
 
            // But the original item hasn't changed yet
            Assert.Equal("m1", item1.GetMetadataValue("m"));
            Assert.Equal("n1", item1.GetMetadataValue("n"));
            Assert.Equal("o1", item1.GetMetadataValue("o"));
 
            enteredScope.LeaveScope();
 
            // It's still m=m2, n=n3, o=o1
            foundGroup = lookup.GetItems("i1");
            Assert.Single(foundGroup);
            Assert.Equal("m2", foundGroup.First().GetMetadataValue("m"));
            Assert.Equal("n3", foundGroup.First().GetMetadataValue("n"));
            Assert.Equal("o1", foundGroup.First().GetMetadataValue("o"));
 
            // And the original item has changed
            Assert.Equal("m2", item1.GetMetadataValue("m"));
            Assert.Equal("n3", item1.GetMetadataValue("n"));
            Assert.Equal("o1", item1.GetMetadataValue("o"));
        }
 
 
        [Fact]
        public void ModifyItemThatWasAddedInSameScope()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            Lookup lookup = LookupHelpers.CreateLookup(table1);
 
            Lookup.Scope enteredScope = lookup.EnterScope("x");
 
            // Add an item with m=m1
            ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath);
            item1.SetMetadata("m", "m1");
            lookup.AddNewItem(item1);
 
            // Change the item to be m=m2
            Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false);
            newMetadata.Add("m", "m2");
            ICollection<ProjectItemInstance> group = new List<ProjectItemInstance>();
            group.Add(item1);
            lookup.ModifyItems(item1.ItemType, group, newMetadata);
 
            // Now it has m=m2
            group = lookup.GetItems("i1");
            Assert.Single(group);
            Assert.Equal("m2", group.First().GetMetadataValue("m"));
 
            // But the original item hasn't changed yet
            Assert.Equal("m1", item1.GetMetadataValue("m"));
 
            enteredScope.LeaveScope();
 
            // It still has m=m2
            group = lookup.GetItems("i1");
            Assert.Single(group);
            Assert.Equal("m2", group.First().GetMetadataValue("m"));
 
            // But now the original item has changed as well
            Assert.Equal("m2", item1.GetMetadataValue("m"));
        }
 
        /// <summary>
        /// Modifying an item in the outside scope is prohibited-
        /// purely because we don't need to do it in our code
        /// </summary>
        [Fact]
        public void ModifyItemInOutsideScope()
        {
            Assert.Throws<InternalErrorException>(() =>
            {
                ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
                Lookup lookup = LookupHelpers.CreateLookup(new ItemDictionary<ProjectItemInstance>());
                lookup.AddNewItem(new ProjectItemInstance(project, "x", "y", project.FullPath));
            });
        }
        /// <summary>
        /// After modification, should be able to GetItem and then modify it again
        /// </summary>
        [Fact]
        public void ModifyItemPreviouslyModifiedAndGottenThroughGetItem()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            Lookup lookup = LookupHelpers.CreateLookup(table1);
 
            // Add an item with m=m1 and n=n1
            ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath);
            item1.SetMetadata("m", "m1");
            table1.Add(item1);
 
            Lookup.Scope enteredScope = lookup.EnterScope("x");
 
            // Make a modification to the item to be m=m2
            Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false);
            newMetadata.Add("m", "m2");
            ICollection<ProjectItemInstance> group = new List<ProjectItemInstance>();
            group.Add(item1);
            lookup.ModifyItems(item1.ItemType, group, newMetadata);
 
            // Get the item (under the covers, it cloned it in order to apply the modification)
            ICollection<ProjectItemInstance> group2 = lookup.GetItems(item1.ItemType);
            Assert.Single(group2);
            ProjectItemInstance item1b = group2.First();
 
            // Modify to m=m3
            Lookup.MetadataModifications newMetadata2 = new Lookup.MetadataModifications(keepOnlySpecified: false);
            newMetadata2.Add("m", "m3");
            ICollection<ProjectItemInstance> group3 = new List<ProjectItemInstance>();
            group3.Add(item1b);
            lookup.ModifyItems(item1b.ItemType, group3, newMetadata2);
 
            // Modifications are visible
            ICollection<ProjectItemInstance> group4 = lookup.GetItems(item1b.ItemType);
            Assert.Single(group4);
            Assert.Equal("m3", group4.First().GetMetadataValue("m"));
 
            // Leave scope
            enteredScope.LeaveScope();
 
            // Still visible
            ICollection<ProjectItemInstance> group5 = lookup.GetItems(item1b.ItemType);
            Assert.Single(group5);
            Assert.Equal("m3", group5.First().GetMetadataValue("m"));
        }
 
 
        /// <summary>
        /// After modification, should be able to GetItem and then modify it again
        /// </summary>
        [Fact]
        public void ModifyItemInProjectPreviouslyModifiedAndGottenThroughGetItem()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            // Create some project state with an item with m=m1 and n=n1
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath);
            item1.SetMetadata("m", "m1");
            table1.Add(item1);
 
            Lookup lookup = LookupHelpers.CreateLookup(table1);
 
            Lookup.Scope enteredScope = lookup.EnterScope("x");
 
            // Make a modification to the item to be m=m2
            Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false);
            newMetadata.Add("m", "m2");
            List<ProjectItemInstance> group = new List<ProjectItemInstance>();
            group.Add(item1);
            lookup.ModifyItems(item1.ItemType, group, newMetadata);
 
            // Get the item (under the covers, it cloned it in order to apply the modification)
            ICollection<ProjectItemInstance> group2 = lookup.GetItems(item1.ItemType);
            Assert.Single(group2);
            ProjectItemInstance item1b = group2.First();
 
            // Modify to m=m3
            Lookup.MetadataModifications newMetadata2 = new Lookup.MetadataModifications(keepOnlySpecified: false);
            newMetadata2.Add("m", "m3");
            List<ProjectItemInstance> group3 = new List<ProjectItemInstance>();
            group3.Add(item1b);
            lookup.ModifyItems(item1b.ItemType, group3, newMetadata2);
 
            // Modifications are visible
            ICollection<ProjectItemInstance> group4 = lookup.GetItems(item1b.ItemType);
            Assert.Single(group4);
            Assert.Equal("m3", group4.First().GetMetadataValue("m"));
 
            // Leave scope
            enteredScope.LeaveScope();
 
            // Still visible
            ICollection<ProjectItemInstance> group5 = lookup.GetItems(item1b.ItemType);
            Assert.Single(group5);
            Assert.Equal("m3", group5.First().GetMetadataValue("m"));
 
            // And the one in the project is changed
            Assert.Equal("m3", item1.GetMetadataValue("m"));
        }
 
        /// <summary>
        /// After modification, should be able to GetItem and then remove it
        /// </summary>
        [Fact]
        public void RemoveItemPreviouslyModifiedAndGottenThroughGetItem()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            Lookup lookup = LookupHelpers.CreateLookup(table1);
 
            // Add an item with m=m1 and n=n1
            ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath);
            item1.SetMetadata("m", "m1");
            table1.Add(item1);
 
            lookup.EnterScope("x");
 
            // Make a modification to the item to be m=m2
            Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false);
            newMetadata.Add("m", "m2");
            List<ProjectItemInstance> group = new List<ProjectItemInstance>();
            group.Add(item1);
            lookup.ModifyItems(item1.ItemType, group, newMetadata);
 
            // Get the item (under the covers, it cloned it in order to apply the modification)
            ICollection<ProjectItemInstance> group2 = lookup.GetItems(item1.ItemType);
            Assert.Single(group2);
            ProjectItemInstance item1b = group2.First();
 
            // Remove the item
            lookup.RemoveItems(item1.ItemType, [item1b]);
 
            // There's now no items at all
            ICollection<ProjectItemInstance> group3 = lookup.GetItems(item1.ItemType);
            Assert.Empty(group3);
        }
 
        /// <summary>
        /// After modification, should be able to GetItem and then remove it
        /// </summary>
        [Fact]
        public void RemoveItemFromProjectPreviouslyModifiedAndGottenThroughGetItem()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            // Create some project state with an item with m=m1 and n=n1
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            ProjectItemInstance item1 = new ProjectItemInstance(project, "i1", "a2", project.FullPath);
            item1.SetMetadata("m", "m1");
            table1.Add(item1);
 
            Lookup lookup = LookupHelpers.CreateLookup(table1);
 
            Lookup.Scope enteredScope = lookup.EnterScope("x");
 
            // Make a modification to the item to be m=m2
            Lookup.MetadataModifications newMetadata = new Lookup.MetadataModifications(keepOnlySpecified: false);
            newMetadata.Add("m", "m2");
            List<ProjectItemInstance> group = new List<ProjectItemInstance>();
            group.Add(item1);
            lookup.ModifyItems(item1.ItemType, group, newMetadata);
 
            // Get the item (under the covers, it cloned it in order to apply the modification)
            ICollection<ProjectItemInstance> group2 = lookup.GetItems(item1.ItemType);
            Assert.Single(group2);
            ProjectItemInstance item1b = group2.First();
 
            // Remove the item
            lookup.RemoveItems(item1.ItemType, [item1b]);
 
            // There's now no items at all
            ICollection<ProjectItemInstance> group3 = lookup.GetItems(item1.ItemType);
            Assert.Empty(group3);
 
            // Leave scope
            enteredScope.LeaveScope();
 
            // And now none left in the project either
            Assert.Empty(table1["i1"]);
        }
 
        /// <summary>
        /// If the property isn't modified, the initial property
        /// should be returned
        /// </summary>
        [Fact]
        public void UnmodifiedProperty()
        {
            PropertyDictionary<ProjectPropertyInstance> group = new PropertyDictionary<ProjectPropertyInstance>();
            ProjectPropertyInstance property = ProjectPropertyInstance.Create("p1", "v1");
            group.Set(property);
            Lookup lookup = LookupHelpers.CreateLookup(group);
 
            Assert.Equal(property, lookup.GetProperty("p1"));
 
            lookup.EnterScope("x");
 
            Assert.Equal(property, lookup.GetProperty("p1"));
        }
 
        /// <summary>
        /// If the property isn't found, should return null
        /// </summary>
        [Fact]
        public void NonexistentProperty()
        {
            PropertyDictionary<ProjectPropertyInstance> group = new PropertyDictionary<ProjectPropertyInstance>();
            Lookup lookup = LookupHelpers.CreateLookup(group);
 
            Assert.Null(lookup.GetProperty("p1"));
 
            lookup.EnterScope("x");
 
            Assert.Null(lookup.GetProperty("p1"));
        }
 
        /// <summary>
        /// If the property is modified, the updated value should be returned,
        /// both before and after leaving scope.
        /// </summary>
        [Fact]
        public void ModifiedProperty()
        {
            PropertyDictionary<ProjectPropertyInstance> group = new PropertyDictionary<ProjectPropertyInstance>();
            group.Set(ProjectPropertyInstance.Create("p1", "v1"));
            Lookup lookup = LookupHelpers.CreateLookup(group);
            // Enter scope so that property sets are allowed on it
            Lookup.Scope enteredScope = lookup.EnterScope("x");
 
            // Change the property value
            lookup.SetProperty(ProjectPropertyInstance.Create("p1", "v2"));
 
            // Lookup is updated, but not original item group
            Assert.Equal("v2", lookup.GetProperty("p1").EvaluatedValue);
            Assert.Equal("v1", group["p1"].EvaluatedValue);
 
            Lookup.Scope enteredScope2 = lookup.EnterScope("x");
 
            // Change the value again in the new scope
            lookup.SetProperty(ProjectPropertyInstance.Create("p1", "v3"));
 
            // Lookup is updated, but not the original item group
            Assert.Equal("v3", lookup.GetProperty("p1").EvaluatedValue);
            Assert.Equal("v1", group["p1"].EvaluatedValue);
 
            Lookup.Scope enteredScope3 = lookup.EnterScope("x");
 
            // Change the value again in the new scope
            lookup.SetProperty(ProjectPropertyInstance.Create("p1", "v4"));
 
            Assert.Equal("v4", lookup.GetProperty("p1").EvaluatedValue);
 
            enteredScope3.LeaveScope();
 
            Assert.Equal("v4", lookup.GetProperty("p1").EvaluatedValue);
 
            // Leave to the outer scope
            enteredScope2.LeaveScope();
            enteredScope.LeaveScope();
 
            // Now the lookup and original group are updated
            Assert.Equal("v4", lookup.GetProperty("p1").EvaluatedValue);
            Assert.Equal("v4", group["p1"].EvaluatedValue);
        }
 
        /// <summary>
        /// Regression coverage for the perf fix in <see cref="Lookup.GetItems"/> when many
        /// items are removed across batches: the result must still contain exactly the items
        /// that were not removed, regardless of whether the implementation uses the
        /// linear-scan fast path or the HashSet path.
        /// </summary>
        [Fact]
        public void GetItemsAfterManyBatchedRemoves_ReturnsCorrectItems()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            const string itemType = "i";
            const int baseCount = 200;
            const int batchCount = 50; // 50 batches × 4 = 200 removes total → exercises HashSet path
            const int batchSize = 4;
 
            var allItems = new List<ProjectItemInstance>(baseCount);
            for (int i = 0; i < baseCount; i++)
            {
                var item = new ProjectItemInstance(project, itemType, $"item_{i}", project.FullPath);
                table1.Add(item);
                allItems.Add(item);
            }
 
            Lookup lookup = LookupHelpers.CreateLookup(table1);
            lookup.EnterScope("x");
 
            var removedSet = new HashSet<ProjectItemInstance>();
            for (int b = 0; b < batchCount; b++)
            {
                var batch = new List<ProjectItemInstance>(batchSize);
                for (int k = 0; k < batchSize; k++)
                {
                    int idx = b * batchSize + k;
                    batch.Add(allItems[idx]);
                    removedSet.Add(allItems[idx]);
                }
                lookup.RemoveItems(itemType, batch);
            }
 
            ICollection<ProjectItemInstance> remaining = lookup.GetItems(itemType);
 
            remaining.Count.ShouldBe(baseCount - (batchCount * batchSize));
            foreach (ProjectItemInstance item in remaining)
            {
                removedSet.ShouldNotContain(item);
            }
            // And no expected-remaining item is missing
            for (int i = batchCount * batchSize; i < baseCount; i++)
            {
                remaining.ShouldContain(allItems[i]);
            }
        }
 
        /// <summary>
        /// Effective output is independent of how many phantom (no-op) removes are mixed
        /// with the real ones. Whether the implementation uses a small or large remove set
        /// internally must not change which items survive.
        /// </summary>
        [Fact]
        public void GetItems_PhantomRemovesDoNotChangeResult()
        {
            const int baseCount = 50;
            const int realRemoveCount = 5;
            const string itemType = "i";
 
            // Build items once and share between both runs so the resulting collections
            // contain the same ProjectItemInstance references, allowing reference-equal
            // set comparison.
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            var allItems = new List<ProjectItemInstance>(baseCount);
            for (int i = 0; i < baseCount; i++)
            {
                allItems.Add(new ProjectItemInstance(project, itemType, $"item_{i}", project.FullPath));
            }
 
            // Phantom (no-op) remove references padding the second run.
            var phantoms = new List<ProjectItemInstance>();
            for (int i = 0; i < 20; i++)
            {
                phantoms.Add(new ProjectItemInstance(project, itemType, $"phantom_{i}", project.FullPath));
            }
 
            ICollection<ProjectItemInstance> noPhantoms = RunWith(allItems, [], realRemoveCount, itemType);
            ICollection<ProjectItemInstance> withPhantoms = RunWith(allItems, phantoms, realRemoveCount, itemType);
 
            noPhantoms.Count.ShouldBe(baseCount - realRemoveCount);
            withPhantoms.Count.ShouldBe(baseCount - realRemoveCount);
            // Reference-equal set comparison: both runs must return the exact same items.
            new HashSet<ProjectItemInstance>(withPhantoms).SetEquals(noPhantoms).ShouldBeTrue();
 
            static ICollection<ProjectItemInstance> RunWith(
                List<ProjectItemInstance> allItems,
                List<ProjectItemInstance> phantomRemoves,
                int realRemoveCount,
                string itemType)
            {
                var table = new ItemDictionary<ProjectItemInstance>();
                foreach (var item in allItems)
                {
                    table.Add(item);
                }
 
                Lookup lookup = LookupHelpers.CreateLookup(table);
                lookup.EnterScope("x");
 
                var toRemove = new List<ProjectItemInstance>(realRemoveCount + phantomRemoves.Count);
                for (int i = 0; i < realRemoveCount; i++)
                {
                    toRemove.Add(allItems[i]);
                }
                toRemove.AddRange(phantomRemoves);
 
                lookup.RemoveItems(itemType, toRemove);
                return lookup.GetItems(itemType);
            }
        }
 
        /// <summary>
        /// Verifies that GetItems uses reference equality (matching pre-#12320 behavior).
        /// Two different <see cref="ProjectItemInstance"/> instances with identical
        /// EvaluatedInclude must be treated as distinct: removing one must not remove the other.
        /// </summary>
        [Fact]
        public void GetItems_RemoveUsesReferenceEquality_NotValueEquality()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            const string itemType = "i";
            const string sharedSpec = "duplicate.cpp";
 
            // Many items so the HashSet path is exercised
            var allItems = new List<ProjectItemInstance>();
            for (int i = 0; i < 30; i++)
            {
                var item = new ProjectItemInstance(project, itemType, sharedSpec, project.FullPath);
                table1.Add(item);
                allItems.Add(item);
            }
 
            Lookup lookup = LookupHelpers.CreateLookup(table1);
            lookup.EnterScope("x");
 
            // Remove only the first 10 references, even though all share the same EvaluatedInclude
            var toRemove = allItems.Take(10).ToList();
            lookup.RemoveItems(itemType, toRemove);
 
            ICollection<ProjectItemInstance> remaining = lookup.GetItems(itemType);
 
            // Exactly the 20 untouched references should remain
            remaining.Count.ShouldBe(20);
            for (int i = 0; i < 10; i++)
            {
                remaining.ShouldNotContain(allItems[i]);
            }
            for (int i = 10; i < 30; i++)
            {
                remaining.ShouldContain(allItems[i]);
            }
        }
 
        /// <summary>
        /// Merged-subscope variant of <see cref="GetItemsAfterManyBatchedRemoves_ReturnsCorrectItems"/>:
        /// each batch lives in a child scope that is then merged into the outer scope, mirroring
        /// how batched intrinsic-task removes accumulate. Exercises the HashSet path with
        /// removes coming from multiple scope levels (multiple lists in <c>allRemoves</c>).
        /// </summary>
        [Fact]
        public void GetItemsAfterMergedSubScopeRemoves_ReturnsCorrectItems()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            const string itemType = "i";
            const int baseCount = 100;
            const int batchCount = 20;
            const int batchSize = 3;
 
            var allItems = new List<ProjectItemInstance>(baseCount);
            for (int i = 0; i < baseCount; i++)
            {
                var item = new ProjectItemInstance(project, itemType, $"item_{i}", project.FullPath);
                table1.Add(item);
                allItems.Add(item);
            }
 
            Lookup lookup = LookupHelpers.CreateLookup(table1);
            Lookup.Scope outer = lookup.EnterScope("outer");
 
            for (int b = 0; b < batchCount; b++)
            {
                Lookup.Scope inner = lookup.EnterScope("batch");
 
                var batch = new List<ProjectItemInstance>(batchSize);
                for (int k = 0; k < batchSize; k++)
                {
                    batch.Add(allItems[b * batchSize + k]);
                }
                lookup.RemoveItems(itemType, batch);
                inner.LeaveScope();
            }
 
            ICollection<ProjectItemInstance> remaining = lookup.GetItems(itemType);
 
            remaining.Count.ShouldBe(baseCount - (batchCount * batchSize));
            for (int i = 0; i < batchCount * batchSize; i++)
            {
                remaining.ShouldNotContain(allItems[i]);
            }
            for (int i = batchCount * batchSize; i < baseCount; i++)
            {
                remaining.ShouldContain(allItems[i]);
            }
 
            outer.LeaveScope();
        }
 
        /// <summary>
        /// Removes that don't match anything in the base group must be no-ops: the original
        /// items must all survive. Exercises the HashSet path with totalRemoves above the
        /// threshold while none of the removes correspond to existing items.
        /// </summary>
        [Fact]
        public void GetItems_RemovesNotInGroup_AreNoOps()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            ItemDictionary<ProjectItemInstance> table1 = new ItemDictionary<ProjectItemInstance>();
            const string itemType = "i";
 
            var originalItems = new List<ProjectItemInstance>();
            for (int i = 0; i < 30; i++)
            {
                var item = new ProjectItemInstance(project, itemType, $"original_{i}", project.FullPath);
                table1.Add(item);
                originalItems.Add(item);
            }
 
            // Build 20 distinct ProjectItemInstance references that were never added to table1.
            var phantomRemoves = new List<ProjectItemInstance>();
            for (int i = 0; i < 20; i++)
            {
                phantomRemoves.Add(new ProjectItemInstance(project, itemType, $"phantom_{i}", project.FullPath));
            }
 
            Lookup lookup = LookupHelpers.CreateLookup(table1);
            lookup.EnterScope("x");
            lookup.RemoveItems(itemType, phantomRemoves);
 
            ICollection<ProjectItemInstance> remaining = lookup.GetItems(itemType);
 
            remaining.Count.ShouldBe(30);
            foreach (var original in originalItems)
            {
                remaining.ShouldContain(original);
            }
        }
 
        /// <summary>
        /// The HashSet path filters both <c>groupFound</c> AND <c>allAdds</c>. This test
        /// covers the adds branch: add many new items, then remove a subset (above threshold)
        /// and verify exactly the unremoved adds survive in the GetItems result.
        /// </summary>
        [Fact]
        public void GetItems_RemoveSubsetOfAdds_ReturnsRemainingAdds()
        {
            ProjectInstance project = ProjectHelpers.CreateEmptyProjectInstance();
            // Empty base table: groupFound will be empty, so the HashSet path applies only to adds.
            Lookup lookup = LookupHelpers.CreateLookup(new ItemDictionary<ProjectItemInstance>());
            const string itemType = "i";
 
            lookup.EnterScope("x");
 
            var addedItems = new List<ProjectItemInstance>();
            for (int i = 0; i < 20; i++)
            {
                var item = new ProjectItemInstance(project, itemType, $"add_{i}", project.FullPath);
                addedItems.Add(item);
                lookup.AddNewItem(item);
            }
 
            // Remove 15 of the adds → totalRemoves > 8 → HashSet path exercised on the adds branch.
            var toRemove = addedItems.Take(15).ToList();
            lookup.RemoveItems(itemType, toRemove);
 
            ICollection<ProjectItemInstance> remaining = lookup.GetItems(itemType);
 
            remaining.Count.ShouldBe(5);
            for (int i = 0; i < 15; i++)
            {
                remaining.ShouldNotContain(addedItems[i]);
            }
            for (int i = 15; i < 20; i++)
            {
                remaining.ShouldContain(addedItems[i]);
            }
        }
    }
 
    internal sealed class LookupHelpers
    {
        internal static Lookup CreateEmptyLookup()
        {
            Lookup lookup = new Lookup(new ItemDictionary<ProjectItemInstance>(), new PropertyDictionary<ProjectPropertyInstance>());
            return lookup;
        }
 
        internal static Lookup CreateLookup(ItemDictionary<ProjectItemInstance> items)
        {
            Lookup lookup = new Lookup(items, new PropertyDictionary<ProjectPropertyInstance>());
            return lookup;
        }
 
        internal static Lookup CreateLookup(PropertyDictionary<ProjectPropertyInstance> properties)
        {
            Lookup lookup = new Lookup(new ItemDictionary<ProjectItemInstance>(), properties);
            return lookup;
        }
 
        internal static Lookup CreateLookup(PropertyDictionary<ProjectPropertyInstance> properties, ItemDictionary<ProjectItemInstance> items)
        {
            Lookup lookup = new Lookup(items, properties);
            return lookup;
        }
    }
}