Skip to content
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
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.Build" Version="18.3.3" />
<PackageVersion Include="Microsoft.Build.Locator" Version="1.11.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.103" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.103" />
Expand Down
101 changes: 101 additions & 0 deletions IntelliTect.Multitool.Tests/CIDetectionTests.cs
Original file line number Diff line number Diff line change
@@ -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<IReadOnlyCollection<string>> _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<string>(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<string, string> { [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<string, string> { ["CI"] = "true" });
Assert.Equal("true", ci);
}

private static string EvaluateCIProperty(Dictionary<string, string> 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 <PropertyGroup Condition="'$(CI)' == ''"> 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 = $"""
<Project>
<Import Project="{TargetsPath.Replace(@"\", "/")}" />
</Project>
""";
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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Build" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.Build.Locator" />
<PackageReference Include="Moq" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
Expand Down
21 changes: 21 additions & 0 deletions IntelliTect.Multitool.Tests/MSBuildFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Microsoft.Build.Locator;
using Xunit;

namespace IntelliTect.Multitool.Tests;

[CollectionDefinition(CollectionName)]
public class MSBuildCollection : ICollectionFixture<MSBuildFixture>
{
public const string CollectionName = "MSBuild";
}

public class MSBuildFixture : IDisposable
{
public MSBuildFixture()
{
if (!MSBuildLocator.IsRegistered)
MSBuildLocator.RegisterDefaults();
}

public void Dispose() { }
}
Loading