Skip to content

Commit 7a486ad

Browse files
feat: Add CI Detection Tests (#182)
1 parent 732193a commit 7a486ad

4 files changed

Lines changed: 126 additions & 0 deletions

File tree

Directory.Packages.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
55
</PropertyGroup>
66
<ItemGroup>
7+
<PackageVersion Include="Microsoft.Build" Version="18.3.3" />
8+
<PackageVersion Include="Microsoft.Build.Locator" Version="1.11.2" />
79
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.103" />
810
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
911
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.103" />
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
using System.IO;
2+
using System.Text.RegularExpressions;
3+
using System.Xml.Linq;
4+
using System.Xml;
5+
using Microsoft.Build.Construction;
6+
using Microsoft.Build.Evaluation;
7+
using Xunit;
8+
9+
namespace IntelliTect.Multitool.Tests;
10+
11+
[Collection(MSBuildCollection.CollectionName)]
12+
public class CIDetectionTests
13+
{
14+
private static readonly string TargetsPath = Path.Combine(
15+
RepositoryPaths.GetDefaultRepoRoot(),
16+
"IntelliTect.Multitool", "Build", "IntelliTect.Multitool.targets");
17+
18+
// Derived from the targets file itself — automatically stays in sync when new CI variables are added.
19+
private static readonly Lazy<IReadOnlyCollection<string>> _ciVarNames = new(() =>
20+
{
21+
var doc = XDocument.Load(TargetsPath);
22+
var conditions = doc.Descendants()
23+
.Where(e => e.Name.LocalName == "CI" && e.Attribute("Condition") != null)
24+
.Select(e => e.Attribute("Condition")!.Value);
25+
var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
26+
foreach (string condition in conditions)
27+
foreach (Match m in Regex.Matches(condition, @"\$\((\w+)\)"))
28+
names.Add(m.Groups[1].Value);
29+
return names;
30+
});
31+
32+
[Theory]
33+
[InlineData("GITHUB_ACTIONS", "true")]
34+
[InlineData("GITLAB_CI", "true")]
35+
[InlineData("CIRCLECI", "true")]
36+
[InlineData("CONTINUOUS_INTEGRATION", "true")]
37+
[InlineData("TF_BUILD", "true")]
38+
[InlineData("TEAMCITY_VERSION", "1.0")]
39+
[InlineData("APPVEYOR", "True")]
40+
[InlineData("BuildRunner", "MyGet")]
41+
[InlineData("JENKINS_URL", "http://jenkins")]
42+
[InlineData("TRAVIS", "true")]
43+
[InlineData("BUDDY", "true")]
44+
[InlineData("CODEBUILD_CI", "true")]
45+
public void CiEnvVar_SetsCIPropertyToTrue(string envVar, string value)
46+
{
47+
string ci = EvaluateCIProperty(new Dictionary<string, string> { [envVar] = value });
48+
Assert.Equal("true", ci);
49+
}
50+
51+
[Fact]
52+
public void NoCiEnvVars_SetsCIPropertyToFalse()
53+
{
54+
string ci = EvaluateCIProperty([]);
55+
Assert.Equal("false", ci);
56+
}
57+
58+
[Fact]
59+
public void CiAlreadyTrue_IsNotOverridden()
60+
{
61+
string ci = EvaluateCIProperty(new Dictionary<string, string> { ["CI"] = "true" });
62+
Assert.Equal("true", ci);
63+
}
64+
65+
private static string EvaluateCIProperty(Dictionary<string, string> overrides)
66+
{
67+
// Clear all CI-related variables parsed from the targets file so that any env vars
68+
// set on the host runner (e.g. GITHUB_ACTIONS=true on GitHub Actions) don't leak in.
69+
// Global properties override process env vars in MSBuild evaluation.
70+
var globalProperties = _ciVarNames.Value
71+
.ToDictionary(v => v, _ => "", StringComparer.OrdinalIgnoreCase);
72+
foreach (var (key, value) in overrides)
73+
globalProperties[key] = value;
74+
75+
// CI itself is not in _ciVarNames (it's the output property, not an input condition
76+
// variable), but GitHub Actions sets CI=true in the OS environment. If it leaks in,
77+
// the outer <PropertyGroup Condition="'$(CI)' == ''"> guard short-circuits and the
78+
// entire detection block is skipped. Temporarily remove it from the process environment
79+
// so MSBuild doesn't see it, unless the caller is explicitly testing the CI=true case.
80+
string? savedCI = Environment.GetEnvironmentVariable("CI");
81+
if (!overrides.ContainsKey("CI"))
82+
Environment.SetEnvironmentVariable("CI", null);
83+
try
84+
{
85+
using var collection = new ProjectCollection(globalProperties);
86+
string xml = $"""
87+
<Project>
88+
<Import Project="{TargetsPath.Replace(@"\", "/")}" />
89+
</Project>
90+
""";
91+
using var reader = XmlReader.Create(new StringReader(xml));
92+
ProjectRootElement rootElement = ProjectRootElement.Create(reader, collection);
93+
var project = new Project(rootElement, null, null, collection);
94+
return project.GetPropertyValue("CI");
95+
}
96+
finally
97+
{
98+
Environment.SetEnvironmentVariable("CI", savedCI);
99+
}
100+
}
101+
}

IntelliTect.Multitool.Tests/IntelliTect.Multitool.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
</PropertyGroup>
1414

1515
<ItemGroup>
16+
<PackageReference Include="Microsoft.Build" ExcludeAssets="runtime" />
17+
<PackageReference Include="Microsoft.Build.Locator" />
1618
<PackageReference Include="Moq" />
1719
<PackageReference Include="Microsoft.NET.Test.Sdk" />
1820
<PackageReference Include="xunit" />
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Microsoft.Build.Locator;
2+
using Xunit;
3+
4+
namespace IntelliTect.Multitool.Tests;
5+
6+
[CollectionDefinition(CollectionName)]
7+
public class MSBuildCollection : ICollectionFixture<MSBuildFixture>
8+
{
9+
public const string CollectionName = "MSBuild";
10+
}
11+
12+
public class MSBuildFixture : IDisposable
13+
{
14+
public MSBuildFixture()
15+
{
16+
if (!MSBuildLocator.IsRegistered)
17+
MSBuildLocator.RegisterDefaults();
18+
}
19+
20+
public void Dispose() { }
21+
}

0 commit comments

Comments
 (0)