|
// 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;
}
}
}
|