Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to ignore targets when determining if a build request matches a cache entry #100

Merged
merged 1 commit into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,7 @@ These settings are common across all plugins, although different implementations
| `$(MSBuildCacheAllowProcessCloseAfterProjectFinishProcessPatterns)` | `Glob[]` | `\**\mspdbsrv.exe` | Processes to allow to exit after the project which launched it completes, ie detached processes. |
| `$(MSBuildCacheGlobalPropertiesToIgnore)` | `string[]` | `CurrentSolutionConfigurationContents; ShouldUnsetParentConfigurationAndPlatform; BuildingInsideVisualStudio; BuildingSolutionFile; SolutionDir; SolutionExt; SolutionFileName; SolutionName; SolutionPath; _MSDeployUserAgent`, as well as all proeprties related to plugin settings | The list of global properties to exclude from consideration by the cache |
| `$(MSBuildCacheGetResultsForUnqueriedDependencies)` | `bool` | false | Whether to try and query the cache for dependencies if they have not previously been requested. This option can help in cases where the build isn't done in graph order, or if some projects are skipped. |


| `$(MSBuildCacheTargetsToIgnore)` | `string[]` | `GetTargetFrameworks;GetNativeManifest;GetCopyToOutputDirectoryItems;GetTargetFrameworksWithPlatformForSingleTargetFramework` | The list of targets to ignore when determining if a build request matches a cache entry. This is intended for "information gathering" targets which do not have side-effect. eg. a build with `/t:Build` and `/t:Build;GetTargetFrameworks` should be considered to have equivalent results. Note: This only works "one-way" in that the build request is allowed to have missing targets, while the cache entry is not. This is to avoid a situation where a build request recieves a cache hit with missing target results, where a cache hit with extra target results is acceptable. |

When configuring settings which are list types, you should always append to the existing value to avoid overriding the defaults:

Expand Down
350 changes: 204 additions & 146 deletions src/Common.Tests/PluginSettingsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,169 @@ public void LocalCacheSizeInMegabytesSetting()
pluginSettings => pluginSettings.LocalCacheSizeInMegabytes,
new[] { 123u, 456u, 789u });

private static IEnumerable<object[]> GlobTestData
[TestMethod]
[DynamicData(nameof(GlobTestCases), DynamicDataDisplayName = nameof(GetTestCaseDisplayName))]
public void IgnoredInputPatternsSetting(GlobTestCase testCase)
=> TestGlobListSetting(
nameof(PluginSettings.IgnoredInputPatterns),
testCase,
pluginSettings => pluginSettings.IgnoredInputPatterns);

[TestMethod]
[DynamicData(nameof(GlobTestCases), DynamicDataDisplayName = nameof(GetTestCaseDisplayName))]
public void IgnoredOutputPatternsSetting(GlobTestCase testCase)
=> TestGlobListSetting(
nameof(PluginSettings.IgnoredOutputPatterns),
testCase,
pluginSettings => pluginSettings.IgnoredOutputPatterns);

[TestMethod]
[DynamicData(nameof(GlobTestCases), DynamicDataDisplayName = nameof(GetTestCaseDisplayName))]
public void IdenticalDuplicateOutputPatternsSetting(GlobTestCase testCase)
=> TestGlobListSetting(
nameof(PluginSettings.IdenticalDuplicateOutputPatterns),
testCase,
pluginSettings => pluginSettings.IdenticalDuplicateOutputPatterns);

[TestMethod]
public void RemoteCacheIsReadOnlySetting()
=> TestBoolSetting(nameof(PluginSettings.RemoteCacheIsReadOnly), pluginSettings => pluginSettings.RemoteCacheIsReadOnly);

[TestMethod]
public void AsyncCachePublishingSetting()
=> TestBoolSetting(nameof(PluginSettings.AsyncCachePublishing), pluginSettings => pluginSettings.AsyncCachePublishing);

[TestMethod]
public void AsyncCacheMaterializationSetting()
=> TestBoolSetting(nameof(PluginSettings.AsyncCacheMaterialization), pluginSettings => pluginSettings.AsyncCacheMaterialization);

[TestMethod]
[DynamicData(nameof(GlobTestCases), DynamicDataDisplayName = nameof(GetTestCaseDisplayName))]
public void AllowFileAccessAfterProjectFinishProcessPatternsSetting(GlobTestCase testCase)
=> TestGlobListSetting(
nameof(PluginSettings.AllowFileAccessAfterProjectFinishProcessPatterns),
testCase,
pluginSettings => pluginSettings.AllowFileAccessAfterProjectFinishProcessPatterns);

[TestMethod]
[DynamicData(nameof(GlobTestCases), DynamicDataDisplayName = nameof(GetTestCaseDisplayName))]
public void AllowFileAccessAfterProjectFinishFilePatternsSetting(GlobTestCase testCase)
=> TestGlobListSetting(
nameof(PluginSettings.AllowFileAccessAfterProjectFinishFilePatterns),
testCase,
pluginSettings => pluginSettings.AllowFileAccessAfterProjectFinishFilePatterns);

[TestMethod]
[DynamicData(nameof(GlobTestCases), DynamicDataDisplayName = nameof(GetTestCaseDisplayName))]
public void AllowProcessCloseAfterProjectFinishProcessPatternsSetting(GlobTestCase testCase)
=> TestGlobListSetting(
nameof(PluginSettings.AllowProcessCloseAfterProjectFinishProcessPatterns),
testCase,
pluginSettings => pluginSettings.AllowProcessCloseAfterProjectFinishProcessPatterns);

[TestMethod]
[DynamicData(nameof(StringListTestCases), DynamicDataDisplayName = nameof(GetTestCaseDisplayName))]
public void GlobalPropertiesToIgnoreSetting(StringListTestCase testCase)
=> TestStringListSetting(nameof(PluginSettings.GlobalPropertiesToIgnore), testCase, pluginSettings => pluginSettings.GlobalPropertiesToIgnore);

[TestMethod]
public void GetResultsForUnqueriedDependenciesSetting()
=> TestBoolSetting(nameof(PluginSettings.GetResultsForUnqueriedDependencies), pluginSettings => pluginSettings.GetResultsForUnqueriedDependencies);

[TestMethod]
[DynamicData(nameof(StringListTestCases), DynamicDataDisplayName = nameof(GetTestCaseDisplayName))]
public void TargetsToIgnoreSetting(StringListTestCase testCase)
=> TestStringListSetting(nameof(PluginSettings.TargetsToIgnore), testCase, pluginSettings => pluginSettings.TargetsToIgnore);

private static void TestBoolSetting(string settingName, Func<PluginSettings, bool> valueAccessor)
=> TestBasicSetting(
settingName,
valueAccessor,
testValues: [false, true]);

private static void TestBasicSetting<T>(
string settingName,
Func<PluginSettings, T> valueAccessor,
ReadOnlySpan<T> testValues)
{
T defaultValue = valueAccessor(DefaultPluginSettings);

TestBasicSettingValue(null, defaultValue);
TestBasicSettingValue(string.Empty, defaultValue);
TestBasicSettingValue(defaultValue?.ToString(), defaultValue);

foreach (T testValue in testValues)
{
TestBasicSettingValue(testValue?.ToString(), testValue);
}

void TestBasicSettingValue(string? settingValue, T expectedValue)
{
Dictionary<string, string> settings = new(StringComparer.OrdinalIgnoreCase);
if (settingValue != null)
{
settings.Add(settingName, settingValue);
}

PluginSettings pluginSettings = PluginSettings.Create<PluginSettings>(settings, NullPluginLogger.Instance, RepoRoot);

Assert.AreEqual(expectedValue, valueAccessor(pluginSettings));
}
}

private static void TestGlobListSetting(
string settingName,
GlobTestCase testCase,
Func<PluginSettings, IReadOnlyCollection<Glob>> valueAccessor)
{
Dictionary<string, string> settings = new(StringComparer.OrdinalIgnoreCase)
{
{ settingName, testCase.Glob },
};

PluginSettings pluginSettings = PluginSettings.Create<PluginSettings>(settings, NullPluginLogger.Instance, RepoRoot);

foreach (string path in testCase.ExpectedMatching)
{
Assert.IsTrue(MatchesGlobs(path), $"Path did not match any patterns: {path}");
}

foreach (string path in testCase.ExpectedNotMatching)
{
Assert.IsFalse(MatchesGlobs(path), $"Path matched pattern unexpectedly: {path}");
}

bool MatchesGlobs(string path)
{
foreach (Glob glob in valueAccessor(pluginSettings))
{
if (glob.IsMatch(path))
{
return true;
}
}

return false;
}
}

private static void TestStringListSetting(
string settingName,
StringListTestCase testCase,
Func<PluginSettings, IReadOnlyCollection<string>> valueAccessor)
{
Dictionary<string, string> settings = new(StringComparer.OrdinalIgnoreCase);
if (testCase.SettingValue != null)
{
settings.Add(settingName, testCase.SettingValue);
}

PluginSettings pluginSettings = PluginSettings.Create<PluginSettings>(settings, NullPluginLogger.Instance, RepoRoot);

CollectionAssert.AreEqual(testCase.ExpectedValues.ToList(), valueAccessor(pluginSettings).ToList());
}

public static IEnumerable<object[]> GlobTestCases
{
get
{
Expand Down Expand Up @@ -273,157 +435,46 @@ private static IEnumerable<object[]> GlobTestData
}
}

[TestMethod]
[DynamicData(nameof(GlobTestData), DynamicDataDisplayName = nameof(GetTestCaseDisplayName))]
public void IgnoredInputPatternsSetting(GlobTestCase testCase)
=> TestGlobListSetting(
nameof(PluginSettings.IgnoredInputPatterns),
testCase,
pluginSettings => pluginSettings.IgnoredInputPatterns);

[TestMethod]
[DynamicData(nameof(GlobTestData), DynamicDataDisplayName = nameof(GetTestCaseDisplayName))]
public void IgnoredOutputPatternsSetting(GlobTestCase testCase)
=> TestGlobListSetting(
nameof(PluginSettings.IgnoredOutputPatterns),
testCase,
pluginSettings => pluginSettings.IgnoredOutputPatterns);

[TestMethod]
[DynamicData(nameof(GlobTestData), DynamicDataDisplayName = nameof(GetTestCaseDisplayName))]
public void IdenticalDuplicateOutputPatternsSetting(GlobTestCase testCase)
=> TestGlobListSetting(
nameof(PluginSettings.IdenticalDuplicateOutputPatterns),
testCase,
pluginSettings => pluginSettings.IdenticalDuplicateOutputPatterns);

[TestMethod]
public void RemoteCacheIsReadOnlySetting()
=> TestBoolSetting(nameof(PluginSettings.RemoteCacheIsReadOnly), pluginSettings => pluginSettings.RemoteCacheIsReadOnly);

[TestMethod]
public void AsyncCachePublishingSetting()
=> TestBoolSetting(nameof(PluginSettings.AsyncCachePublishing), pluginSettings => pluginSettings.AsyncCachePublishing);

[TestMethod]
public void AsyncCacheMaterializationSetting()
=> TestBoolSetting(nameof(PluginSettings.AsyncCacheMaterialization), pluginSettings => pluginSettings.AsyncCacheMaterialization);

[TestMethod]
[DynamicData(nameof(GlobTestData), DynamicDataDisplayName = nameof(GetTestCaseDisplayName))]
public void AllowFileAccessAfterProjectFinishProcessPatternsSetting(GlobTestCase testCase)
=> TestGlobListSetting(
nameof(PluginSettings.AllowFileAccessAfterProjectFinishProcessPatterns),
testCase,
pluginSettings => pluginSettings.AllowFileAccessAfterProjectFinishProcessPatterns);

[TestMethod]
[DynamicData(nameof(GlobTestData), DynamicDataDisplayName = nameof(GetTestCaseDisplayName))]
public void AllowFileAccessAfterProjectFinishFilePatternsSetting(GlobTestCase testCase)
=> TestGlobListSetting(
nameof(PluginSettings.AllowFileAccessAfterProjectFinishFilePatterns),
testCase,
pluginSettings => pluginSettings.AllowFileAccessAfterProjectFinishFilePatterns);

[TestMethod]
[DynamicData(nameof(GlobTestData), DynamicDataDisplayName = nameof(GetTestCaseDisplayName))]
public void AllowProcessCloseAfterProjectFinishProcessPatternsSetting(GlobTestCase testCase)
=> TestGlobListSetting(
nameof(PluginSettings.AllowProcessCloseAfterProjectFinishProcessPatterns),
testCase,
pluginSettings => pluginSettings.AllowProcessCloseAfterProjectFinishProcessPatterns);

[TestMethod]
[DataRow(null, new string[] { }, null, DisplayName = "Null")]
[DataRow("", new string[] { }, null, DisplayName = "Empty string")]
[DataRow("A;B;C", new string[] { "A", "B", "C" }, null, DisplayName = "Basic values")]
[DataRow(" ; A ;; ;;; B ;\r\n\r\n;\r\nC;;; ", new string[] { "A", "B", "C" }, null, DisplayName = "Whitespace and empty values")]
public void GlobalPropertiesToIgnoreSetting(string? settingValue, string[] expectedValue, object _)
public static IEnumerable<object[]> StringListTestCases
{
Dictionary<string, string> settings = new(StringComparer.OrdinalIgnoreCase);
if (settingValue != null)
{
settings.Add(nameof(PluginSettings.GlobalPropertiesToIgnore), settingValue);
}

PluginSettings pluginSettings = PluginSettings.Create<PluginSettings>(settings, NullPluginLogger.Instance, RepoRoot);

CollectionAssert.AreEqual(expectedValue, pluginSettings.GlobalPropertiesToIgnore.ToList());
}

[TestMethod]
public void GetResultsForUnqueriedDependenciesSetting()
=> TestBoolSetting(nameof(PluginSettings.GetResultsForUnqueriedDependencies), pluginSettings => pluginSettings.GetResultsForUnqueriedDependencies);

private static void TestBoolSetting(string settingName, Func<PluginSettings, bool> valueAccessor)
=> TestBasicSetting(
settingName,
valueAccessor,
testValues: [false, true]);

private static void TestBasicSetting<T>(
string settingName,
Func<PluginSettings, T> valueAccessor,
ReadOnlySpan<T> testValues)
{
T defaultValue = valueAccessor(DefaultPluginSettings);

TestBasicSettingValue(null, defaultValue);
TestBasicSettingValue(string.Empty, defaultValue);
TestBasicSettingValue(defaultValue?.ToString(), defaultValue);

foreach (T testValue in testValues)
{
TestBasicSettingValue(testValue?.ToString(), testValue);
}

void TestBasicSettingValue(string? settingValue, T expectedValue)
get
{
Dictionary<string, string> settings = new(StringComparer.OrdinalIgnoreCase);
if (settingValue != null)
yield return new object[]
{
settings.Add(settingName, settingValue);
}

PluginSettings pluginSettings = PluginSettings.Create<PluginSettings>(settings, NullPluginLogger.Instance, RepoRoot);

Assert.AreEqual(expectedValue, valueAccessor(pluginSettings));
}
}

private static void TestGlobListSetting(
string settingName,
GlobTestCase testCase,
Func<PluginSettings, IReadOnlyCollection<Glob>> valueAccessor)
{
Dictionary<string, string> settings = new(StringComparer.OrdinalIgnoreCase)
{
{ settingName, testCase.Glob },
};

PluginSettings pluginSettings = PluginSettings.Create<PluginSettings>(settings, NullPluginLogger.Instance, RepoRoot);

foreach (string path in testCase.ExpectedMatching)
{
Assert.IsTrue(MatchesGlobs(path), $"Path did not match any patterns: {path}");
}

foreach (string path in testCase.ExpectedNotMatching)
{
Assert.IsFalse(MatchesGlobs(path), $"Path matched pattern unexpectedly: {path}");
}

bool MatchesGlobs(string path)
{
foreach (Glob glob in valueAccessor(pluginSettings))
new StringListTestCase
{
DisplayName = "Null",
SettingValue = null,
ExpectedValues = [],
}
};
yield return new object[]
{
if (glob.IsMatch(path))
new StringListTestCase
{
return true;
DisplayName = "Empty string",
SettingValue = string.Empty,
ExpectedValues = [],
}
}

return false;
};
yield return new object[]
{
new StringListTestCase
{
DisplayName = "Basic values",
SettingValue = "A;B;C",
ExpectedValues = [ "A", "B", "C" ],
}
};
yield return new object[]
{
new StringListTestCase
{
DisplayName = "Whitespace and empty values",
SettingValue = " ; A ;; ;;; B ;\r\n\r\n;\r\nC;;; ",
ExpectedValues = [ "A", "B", "C" ],
}
};
}
}

Expand All @@ -444,4 +495,11 @@ public sealed class GlobTestCase : TestCaseBase

public required IReadOnlyList<string> ExpectedNotMatching { get; init; }
}

public sealed class StringListTestCase : TestCaseBase
{
public required string? SettingValue { get; init; }

public required IReadOnlyList<string> ExpectedValues { get; init; }
}
}
Loading
Loading