Skip to content

Commit 02d2e89

Browse files
authored
Updated test access to build environment information. (#32)
Tests need access to the information about the environment to determine the correct versioning for a build. Normally, this is available in the Environment variables but test runs DO NOT get an inherited set of environment variables from the command that launched them. * Added forcing CI info to blank in the task's props file * Added setting of the environment variables with IDisposable RAII pattern to clean up afterwards when validating the as-built task assembly. * Added [Start|End]-FakeReleaseBuild.ps1 scripts to aid in replicating a release build environment locally. * Added BuildKind to the generatedVersion.props file to allow "communication" of the environment variables that existed at time of the task assembly build. Co-authored-by: smaillet <25911635+smaillet@users.noreply.github.com>
1 parent 09ce948 commit 02d2e89

File tree

6 files changed

+172
-84
lines changed

6 files changed

+172
-84
lines changed

End-FakeReleaseBuild.ps1

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
$env:CLI=$null
2+
$env:GITHUB_ACTIONS=$null
3+
$env:GITHUB_REF=$null

New-GeneratedVersionProps.ps1

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,13 @@ try
358358
$informationalVersionElement.InnerText = $csemVer.ToString($true, $false) # long form of version
359359
$propGroupElement.AppendChild($informationalVersionElement) | Out-Null
360360

361+
# inform unit testing of the environment as the env vars are NOT accessible to the tests
362+
# Sadly, the `dotnet test` command does not spawn the tests with an inherited environment.
363+
# So they cannot know what the scenario is.
364+
$buildKindElement = $xmlDoc.CreateElement('BuildKind')
365+
$buildKindElement.InnerText = $buildKind
366+
$propGroupElement.AppendChild($buildKindElement) | Out-Null
367+
361368
# Unit tests need to see the CI build info as it isn't something they can determine on their own.
362369
# The Build index is based on a timestamp and the build name depends on the runtime environment
363370
# to set some env vars etc...
@@ -375,7 +382,6 @@ try
375382
$ciBuildNameElement.InnerText = $verInfo['CiBuildName']
376383
$propGroupElement.AppendChild($ciBuildNameElement) | Out-Null
377384
}
378-
379385
$buildGeneratedPropsPath = Join-Path $buildInfo['RepoRootPath'] 'GeneratedVersion.props'
380386
$xmlDoc.Save($buildGeneratedPropsPath)
381387
}

Start-FakeReleaseBuild.ps1

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Fake Release build
2+
$env:CLI="true"
3+
$env:GITHUB_ACTIONS="true"
4+
$env:GITHUB_REF="refs/tags/v5.0.0.rc"

src/Ubiquity.NET.Versioning.Build.Tasks/build/Ubiquity.NET.Versioning.Build.Tasks.props

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@
2020
<CiBuildName Condition="'$(CiBuildName)'=='' AND $(IsPullRequestBuild) AND !$(IsReleaseBuild)">PRQ</CiBuildName>
2121
<CiBuildName Condition="'$(CiBuildName)'=='' AND $(IsAutomatedBuild) AND !$(IsReleaseBuild)">BLD</CiBuildName>
2222
<CiBuildName Condition="'$(CiBuildName)'=='' AND !$(IsReleaseBuild)">ZZZ</CiBuildName>
23+
</PropertyGroup>
2324

25+
<!-- Force empty values for CI Build info in a release build -->
26+
<PropertyGroup Condition="$(IsReleaseBuild)">
27+
<CiBuildName/>
28+
<CiBuildIndex />
29+
<BuildTime />
2430
</PropertyGroup>
2531
</Project>

src/Ubiquity.Versioning.Build.Tasks.UT/AssemblyValidationTests.cs

Lines changed: 92 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using System.Runtime.Loader;
1313

1414
using Microsoft.Build.Evaluation;
15+
using Microsoft.Build.Utilities.ProjectCreation;
1516
using Microsoft.VisualStudio.TestTools.UnitTesting;
1617

1718
using Ubiquity.NET.Versioning;
@@ -51,100 +52,110 @@ public void ValidateRepoAssemblyVersion( string targetFramework)
5152
};
5253

5354
// For a CI build load the ciBuildIndex and ciBuildName from the generatedversion.props file
54-
// so the test knows what to expect. This does NOT verify the behavior of the tasks exactly unfortunately.
55-
// There is a non-determinism in computing the index based on a time stamp in particular a single date/time
56-
// string is converted based on seconds since midnight today (in UTC) so if two different implementations
57-
// compute a value at a different time that varies by as much as 2 seconds, then they will produce different
58-
// results even if behaving correctly. The use of seconds since midnight today makes it non-deterministic...
59-
// Unfortunately that's the algorithm chosen, though since this is a major release (and a full rename that
60-
// is something to re-visit) Until, that is deterministic, use the generated CI info all up. Other tests will
61-
// need to validate the behavior of the task.
62-
var (ciBuildIndex, ciBuildName, buildTime) = TestUtils.GetGeneratedCiBuildInfo();
63-
64-
// Build name depends on context of the build (Local, PR, CI, Release)
65-
// and therefore is NOT hard-coded in the tests.
66-
if(!string.IsNullOrWhiteSpace(ciBuildName))
55+
// so the test knows what to expect.
56+
var (ciBuildIndex, ciBuildName, buildTime, envControl) = TestUtils.GetGeneratedBuildInfo();
57+
using(envControl)
6758
{
68-
globalProperties["CiBuildName"] = ciBuildName;
69-
}
70-
71-
if(!string.IsNullOrWhiteSpace(buildTime))
72-
{
73-
// NOT using exact parsing as that's 'flaky' at best and doesn't actually handle all ISO-8601 formats
74-
// Also, NOT using assumption of UTC as commit dates from repo are local time based. ToBuildIndex() will
75-
// convert to UTC so that the resulting index is still consistent.
76-
var parsedBuildTime = DateTime.Parse(buildTime, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);
77-
string indexFromLib = parsedBuildTime.ToBuildIndex();
78-
Assert.AreEqual(indexFromLib, ciBuildIndex, "Index computed with versioning library should match the index computed by scripts");
79-
80-
globalProperties["BuildTime"] = buildTime;
81-
}
82-
83-
using var collection = new ProjectCollection(globalProperties);
84-
85-
var (buildResults, props) = Context.CreateTestProjectAndInvokeTestedPackage(targetFramework, collection);
59+
if(!string.IsNullOrWhiteSpace(ciBuildIndex))
60+
{
61+
globalProperties["CiBuildIndex"] = ciBuildIndex;
62+
}
8663

87-
string? taskAssembly = buildResults.Creator.ProjectInstance.GetOptionalProperty("_Ubiquity_NET_Versioning_Build_Tasks");
88-
Assert.IsNotNull( taskAssembly, "Task assembly property should contain full path to the task DLL (Not NULL)" );
89-
Context.WriteLine( $"Task Assembly: '{taskAssembly}'" );
64+
// Build name depends on context of the build (Local, PR, CI, Release)
65+
// and therefore is NOT hard-coded in the tests.
66+
if(!string.IsNullOrWhiteSpace(ciBuildName))
67+
{
68+
globalProperties["CiBuildName"] = ciBuildName;
69+
}
9070

91-
Assert.IsFalse( string.IsNullOrWhiteSpace( taskAssembly ), "Task assembly property should contain full path to the task DLL (Not Whitespace)" );
92-
Assert.IsNotNull( props.FileVersion, "Generated properties should have a 'FileVersion'" );
93-
Context.WriteLine( $"Generated FileVersion: {props.FileVersion}" );
71+
if(!string.IsNullOrWhiteSpace(buildTime))
72+
{
73+
// NOT using exact parsing as that's 'flaky' at best and doesn't actually handle all ISO-8601 formats
74+
// Also, NOT using assumption of UTC as commit dates from repo are local time based. ToBuildIndex() will
75+
// convert to UTC so that the resulting index is still consistent.
76+
var parsedBuildTime = DateTime.Parse(buildTime, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);
77+
string indexFromLib = parsedBuildTime.ToBuildIndex();
78+
Assert.AreEqual(indexFromLib, ciBuildIndex, "Index computed with versioning library should match the index computed by scripts");
79+
80+
globalProperties["BuildTime"] = buildTime;
81+
}
9482

95-
var alc = new AssemblyLoadContext("TestALC", isCollectible: true);
96-
try
97-
{
98-
var asm = alc.LoadFromAssemblyPath(taskAssembly);
99-
Assert.IsNotNull( asm, "should be able to load task assembly" );
100-
var asmName = asm.GetName();
101-
Version? asmVer = asmName.Version;
102-
Assert.IsNotNull( asmVer, "Task assembly should have a version" );
103-
Context.WriteLine( $"TaskAssemblyVersion: {asmVer}" );
104-
Context.WriteLine( $"AssemblyName: {asmName}" );
83+
using var collection = new ProjectCollection(globalProperties);
10584

106-
Assert.IsNotNull( props.FileVersionMajor, "Property value for Major should exist" );
107-
Assert.AreEqual( (int)props.FileVersionMajor, asmVer.Major, "Major value of assembly version should match" );
85+
var (buildResults, props) = Context.CreateTestProjectAndInvokeTestedPackage(targetFramework, collection);
10886

109-
Assert.IsNotNull( props.FileVersionMinor, "Property value for Minor should exist" );
110-
Assert.AreEqual( (int)props.FileVersionMinor, asmVer.Minor, "Minor value of assembly version should match" );
87+
LogBuildMessages(buildResults.Output);
11188

112-
Assert.IsNotNull( props.FileVersionBuild, "Property value for Build should exist" );
113-
Assert.AreEqual( (int)props.FileVersionBuild, asmVer.Build, "Build value of assembly version should match" );
89+
string? taskAssembly = buildResults.Creator.ProjectInstance.GetOptionalProperty("_Ubiquity_NET_Versioning_Build_Tasks");
90+
Assert.IsNotNull( taskAssembly, "Task assembly property should contain full path to the task DLL (Not NULL)" );
91+
Context.WriteLine( $"Task Assembly: '{taskAssembly}'" );
11492

115-
Assert.IsNotNull( props.FileVersionRevision, "Property value for Revision should exist" );
116-
Assert.AreEqual( (int)props.FileVersionRevision, asmVer.Revision, "Revision value of assembly version should match" );
93+
Assert.IsFalse( string.IsNullOrWhiteSpace( taskAssembly ), "Task assembly property should contain full path to the task DLL (Not Whitespace)" );
94+
Assert.IsNotNull( props.FileVersion, "Generated properties should have a 'FileVersion'" );
95+
Context.WriteLine( $"Generated FileVersion: {props.FileVersion}" );
11796

118-
// Release builds won't have a CI component by definition so nothing to validate for those
119-
// Should get local, PR and CI builds before that to hit this case though.
120-
if(!string.IsNullOrWhiteSpace(ciBuildIndex))
97+
var alc = new AssemblyLoadContext("TestALC", isCollectible: true);
98+
try
12199
{
122-
Assert.AreEqual( ciBuildIndex, props.CiBuildIndex, "BuildIndex computed in scripts should match computed value from task");
100+
var asm = alc.LoadFromAssemblyPath(taskAssembly);
101+
Assert.IsNotNull( asm, "should be able to load task assembly" );
102+
var asmName = asm.GetName();
103+
Version? asmVer = asmName.Version;
104+
Assert.IsNotNull( asmVer, "Task assembly should have a version" );
105+
Context.WriteLine( $"TaskAssemblyVersion: {asmVer}" );
106+
Context.WriteLine( $"AssemblyName: {asmName}" );
107+
108+
Assert.IsNotNull( props.FileVersionMajor, "Property value for Major should exist" );
109+
Assert.AreEqual( (int)props.FileVersionMajor, asmVer.Major, "Major value of assembly version should match" );
110+
111+
Assert.IsNotNull( props.FileVersionMinor, "Property value for Minor should exist" );
112+
Assert.AreEqual( (int)props.FileVersionMinor, asmVer.Minor, "Minor value of assembly version should match" );
113+
114+
Assert.IsNotNull( props.FileVersionBuild, "Property value for Build should exist" );
115+
Assert.AreEqual( (int)props.FileVersionBuild, asmVer.Build, "Build value of assembly version should match" );
116+
117+
Assert.IsNotNull( props.FileVersionRevision, "Property value for Revision should exist" );
118+
Assert.AreEqual( (int)props.FileVersionRevision, asmVer.Revision, "Revision value of assembly version should match" );
119+
120+
// Release builds won't have a CI component by definition so nothing to validate for those
121+
// Should get local, PR and CI builds before that to hit this case though.
122+
if(!string.IsNullOrWhiteSpace(ciBuildIndex))
123+
{
124+
Assert.AreEqual( ciBuildIndex, props.CiBuildIndex, "BuildIndex computed in scripts should match computed value from task");
125+
}
126+
127+
// Test that AssemblyFileVersion on the task assembly matches expected value
128+
string fileVersion = ( from attr in asm.CustomAttributes
129+
where attr.AttributeType.FullName == "System.Reflection.AssemblyFileVersionAttribute"
130+
let val = attr.ConstructorArguments.Single().Value as string
131+
where val is not null
132+
select val
133+
).Single();
134+
135+
Assert.AreEqual(props.FileVersion, fileVersion);
136+
137+
// Test that AssemblyInformationalVersion on the task assembly matches expected value
138+
string informationalVersion = ( from attr in asm.CustomAttributes
139+
where attr.AttributeType.FullName == "System.Reflection.AssemblyInformationalVersionAttribute"
140+
let val = attr.ConstructorArguments.Single().Value as string
141+
where val is not null
142+
select val
143+
).Single();
144+
145+
Assert.AreEqual(props.InformationalVersion, informationalVersion);
146+
}
147+
finally
148+
{
149+
alc.Unload();
123150
}
124-
125-
// Test that AssemblyFileVersion on the task assembly matches expected value
126-
string fileVersion = ( from attr in asm.CustomAttributes
127-
where attr.AttributeType.FullName == "System.Reflection.AssemblyFileVersionAttribute"
128-
let val = attr.ConstructorArguments.Single().Value as string
129-
where val is not null
130-
select val
131-
).Single();
132-
133-
Assert.AreEqual(props.FileVersion, fileVersion);
134-
135-
// Test that AssemblyInformationalVersion on the task assembly matches expected value
136-
string informationalVersion = ( from attr in asm.CustomAttributes
137-
where attr.AttributeType.FullName == "System.Reflection.AssemblyInformationalVersionAttribute"
138-
let val = attr.ConstructorArguments.Single().Value as string
139-
where val is not null
140-
select val
141-
).Single();
142-
143-
Assert.AreEqual(props.InformationalVersion, informationalVersion);
144151
}
145-
finally
152+
}
153+
154+
private void LogBuildMessages( BuildOutput output )
155+
{
156+
foreach(string msg in output.Messages.Low)
146157
{
147-
alc.Unload();
158+
Context.WriteLine("MSBUILD: {0}", msg);
148159
}
149160
}
150161
}

src/Ubiquity.Versioning.Build.Tasks.UT/TestUtils.cs

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
// </copyright>
55
// -----------------------------------------------------------------------
66

7+
using System;
8+
using System.Diagnostics.CodeAnalysis;
79
using System.IO;
810

911
using Microsoft.Build.Definition;
@@ -13,7 +15,7 @@ namespace Ubiquity.Versioning.Build.Tasks.UT
1315
{
1416
internal static class TestUtils
1517
{
16-
internal static (string CiBuildIndex, string CiBuildName, string BuildTime) GetGeneratedCiBuildInfo( )
18+
internal static (string CiBuildIndex, string CiBuildName, string BuildTime, IDisposable EnvControl) GetGeneratedBuildInfo( )
1719
{
1820
using var dummyCollection = new ProjectCollection();
1921
var options = new ProjectOptions()
@@ -22,7 +24,63 @@ internal static (string CiBuildIndex, string CiBuildName, string BuildTime) GetG
2224
};
2325

2426
var project = Project.FromFile(Path.Combine(TestModuleFixtures.RepoRoot, "GeneratedVersion.props"), options);
25-
return (project.GetPropertyValue("CiBuildIndex"), project.GetPropertyValue("CiBuildName"), project.GetPropertyValue("BuildTime"));
27+
28+
return (
29+
project.GetPropertyValue( "CiBuildIndex" ),
30+
project.GetPropertyValue( "CiBuildName" ),
31+
project.GetPropertyValue( "BuildTime" ),
32+
project.SetEnvFromGeneratedVersionInfo()
33+
);
34+
}
35+
36+
[SuppressMessage( "Performance", "CA1859:Use concrete types when possible for improved performance", Justification = "Not possible, file scoped type" )]
37+
private static IDisposable SetEnvFromGeneratedVersionInfo( this Project project )
38+
{
39+
// Reset-environment variables for this test process based on the build-kind set by build scripts
40+
// This is needed as the tests don't inherit the environment of the command that runs them.
41+
switch(project.GetPropertyValue( "BuildKind" ))
42+
{
43+
case "LocalBuild":
44+
Environment.SetEnvironmentVariable( "IsAutomatedBuild", "false" );
45+
Environment.SetEnvironmentVariable( "IsPullRequestBuild", "false" );
46+
Environment.SetEnvironmentVariable( "IsReleaseBuild", "false" );
47+
break;
48+
49+
case "PullRequestBuild":
50+
Environment.SetEnvironmentVariable( "IsAutomatedBuild", "true" );
51+
Environment.SetEnvironmentVariable( "IsPullRequestBuild", "true" );
52+
Environment.SetEnvironmentVariable( "IsReleaseBuild", "false" );
53+
break;
54+
55+
case "CiBuild":
56+
Environment.SetEnvironmentVariable( "IsAutomatedBuild", "true" );
57+
Environment.SetEnvironmentVariable( "IsPullRequestBuild", "false" );
58+
Environment.SetEnvironmentVariable( "IsReleaseBuild", "false" );
59+
break;
60+
61+
case "ReleaseBuild":
62+
Environment.SetEnvironmentVariable( "IsAutomatedBuild", "true" );
63+
Environment.SetEnvironmentVariable( "IsPullRequestBuild", "false" );
64+
Environment.SetEnvironmentVariable( "IsReleaseBuild", "true" );
65+
break;
66+
67+
default:
68+
throw new InvalidOperationException( "Unknown build kind in GeneratedVersion.props" );
69+
}
70+
71+
return new ResetEnv();
72+
}
73+
}
74+
75+
[SuppressMessage( "StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "DUH! It's file scoped!" )]
76+
file sealed class ResetEnv
77+
: IDisposable
78+
{
79+
public void Dispose( )
80+
{
81+
Environment.SetEnvironmentVariable( "IsAutomatedBuild", null );
82+
Environment.SetEnvironmentVariable( "IsPullRequestBuild", null );
83+
Environment.SetEnvironmentVariable( "IsReleaseBuild", null );
2684
}
2785
}
2886
}

0 commit comments

Comments
 (0)