diff --git a/Directory.Packages.props b/Directory.Packages.props index ded4255..3d5dabf 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,6 +4,8 @@ false + + diff --git a/IntelliTect.Multitool.Tests/CIDetectionTests.cs b/IntelliTect.Multitool.Tests/CIDetectionTests.cs new file mode 100644 index 0000000..42b1e09 --- /dev/null +++ b/IntelliTect.Multitool.Tests/CIDetectionTests.cs @@ -0,0 +1,101 @@ +using System.IO; +using System.Text.RegularExpressions; +using System.Xml.Linq; +using System.Xml; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Xunit; + +namespace IntelliTect.Multitool.Tests; + +[Collection(MSBuildCollection.CollectionName)] +public class CIDetectionTests +{ + private static readonly string TargetsPath = Path.Combine( + RepositoryPaths.GetDefaultRepoRoot(), + "IntelliTect.Multitool", "Build", "IntelliTect.Multitool.targets"); + + // Derived from the targets file itself — automatically stays in sync when new CI variables are added. + private static readonly Lazy> _ciVarNames = new(() => + { + var doc = XDocument.Load(TargetsPath); + var conditions = doc.Descendants() + .Where(e => e.Name.LocalName == "CI" && e.Attribute("Condition") != null) + .Select(e => e.Attribute("Condition")!.Value); + var names = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (string condition in conditions) + foreach (Match m in Regex.Matches(condition, @"\$\((\w+)\)")) + names.Add(m.Groups[1].Value); + return names; + }); + + [Theory] + [InlineData("GITHUB_ACTIONS", "true")] + [InlineData("GITLAB_CI", "true")] + [InlineData("CIRCLECI", "true")] + [InlineData("CONTINUOUS_INTEGRATION", "true")] + [InlineData("TF_BUILD", "true")] + [InlineData("TEAMCITY_VERSION", "1.0")] + [InlineData("APPVEYOR", "True")] + [InlineData("BuildRunner", "MyGet")] + [InlineData("JENKINS_URL", "http://jenkins")] + [InlineData("TRAVIS", "true")] + [InlineData("BUDDY", "true")] + [InlineData("CODEBUILD_CI", "true")] + public void CiEnvVar_SetsCIPropertyToTrue(string envVar, string value) + { + string ci = EvaluateCIProperty(new Dictionary { [envVar] = value }); + Assert.Equal("true", ci); + } + + [Fact] + public void NoCiEnvVars_SetsCIPropertyToFalse() + { + string ci = EvaluateCIProperty([]); + Assert.Equal("false", ci); + } + + [Fact] + public void CiAlreadyTrue_IsNotOverridden() + { + string ci = EvaluateCIProperty(new Dictionary { ["CI"] = "true" }); + Assert.Equal("true", ci); + } + + private static string EvaluateCIProperty(Dictionary overrides) + { + // Clear all CI-related variables parsed from the targets file so that any env vars + // set on the host runner (e.g. GITHUB_ACTIONS=true on GitHub Actions) don't leak in. + // Global properties override process env vars in MSBuild evaluation. + var globalProperties = _ciVarNames.Value + .ToDictionary(v => v, _ => "", StringComparer.OrdinalIgnoreCase); + foreach (var (key, value) in overrides) + globalProperties[key] = value; + + // CI itself is not in _ciVarNames (it's the output property, not an input condition + // variable), but GitHub Actions sets CI=true in the OS environment. If it leaks in, + // the outer guard short-circuits and the + // entire detection block is skipped. Temporarily remove it from the process environment + // so MSBuild doesn't see it, unless the caller is explicitly testing the CI=true case. + string? savedCI = Environment.GetEnvironmentVariable("CI"); + if (!overrides.ContainsKey("CI")) + Environment.SetEnvironmentVariable("CI", null); + try + { + using var collection = new ProjectCollection(globalProperties); + string xml = $""" + + + + """; + using var reader = XmlReader.Create(new StringReader(xml)); + ProjectRootElement rootElement = ProjectRootElement.Create(reader, collection); + var project = new Project(rootElement, null, null, collection); + return project.GetPropertyValue("CI"); + } + finally + { + Environment.SetEnvironmentVariable("CI", savedCI); + } + } +} diff --git a/IntelliTect.Multitool.Tests/IntelliTect.Multitool.Tests.csproj b/IntelliTect.Multitool.Tests/IntelliTect.Multitool.Tests.csproj index a734b6f..6474654 100644 --- a/IntelliTect.Multitool.Tests/IntelliTect.Multitool.Tests.csproj +++ b/IntelliTect.Multitool.Tests/IntelliTect.Multitool.Tests.csproj @@ -13,6 +13,8 @@ + + diff --git a/IntelliTect.Multitool.Tests/MSBuildFixture.cs b/IntelliTect.Multitool.Tests/MSBuildFixture.cs new file mode 100644 index 0000000..08140a6 --- /dev/null +++ b/IntelliTect.Multitool.Tests/MSBuildFixture.cs @@ -0,0 +1,21 @@ +using Microsoft.Build.Locator; +using Xunit; + +namespace IntelliTect.Multitool.Tests; + +[CollectionDefinition(CollectionName)] +public class MSBuildCollection : ICollectionFixture +{ + public const string CollectionName = "MSBuild"; +} + +public class MSBuildFixture : IDisposable +{ + public MSBuildFixture() + { + if (!MSBuildLocator.IsRegistered) + MSBuildLocator.RegisterDefaults(); + } + + public void Dispose() { } +}