Skip to content

Commit 7a2203e

Browse files
authored
Fixes #72 (#74)
* Formatting of pre-release numbers for CI builds should ALWAYS include the pre-release number and fix for a pre-release build. * Moved non-test functionality into Support namespace to help clarify role * Clarified docs on behavior of CI version - Clearly that's a complex and subtle point!
1 parent 6296a2e commit 7a2203e

23 files changed

+214
-66
lines changed

docfx/build-tasks/UnderstandingCIBuilds.md

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ Hopefully examples will make things more clear:
3232
* As with the previous example this is ordered AFTER the release it is based on
3333
and BEFORE the Patch+1 version (`v5.0.4-beta.0.1`).
3434

35+
>[!NOTE]
36+
> As [[BUG] - CI version string formatting does not ALWAYS include pre-release information for a pre-release](https://github.com/UbiquityDotNET/CSemVer.GitBuild/issues/72)
37+
> points out the formatting of pre-release version numbers is different in CSemVer-CI. In a CI
38+
> version the `Number` and `Fix` values are ALWAYS included, even if `0`. In a CSemVer
39+
> things are more complex. In such a case, the `Number` and `Fix` are NOT shown if 0.
40+
> ***Unless*** the `Number` is `0` AND `Fix > 0` in that case the `Number` is shown
41+
> as a zero and the `Fix` is shown as it's non-zero value.
42+
3543
## lifetime scope of a CI Build
3644
The lifetime of a CI build is generally very short and once a version is released
3745
all CIs that led up to that release are essentially moot.
@@ -99,18 +107,18 @@ on the ordered integral form of the version and increments that, until it reache
99107
maximum)
100108

101109
### Ordered Version
102-
Ordered versions are a concept unique to Constrained Semantic versions. The constraints
103-
applied to a SemVer allow creation of an integral value for all versions, except CI
104-
builds. Ignoring CI builds for the moment, the ordered number is computed from the
105-
values of the various parts of a version as they are constrained by the CSemVer spec.
106-
The math involved is not important for this discussion. Just that each Constrained
107-
Version is representable as a distinct integral value (63 bits actually). A CSemVer-CI
108-
build has two elements the base build and the additional 'BuildIndex' and 'BuildName'
109-
components. This means the string, File version and ordered version numbers are
110-
confusingly different for a CI build. The ordered version number does NOT account for
111-
CI in any way. It is ONLY able to convert to/from a CSemVer. Thus, a CSemVer-CI has
112-
an ambiguous conversion. Should it convert the Patch+1 form in a string or the
113-
base build number?.
110+
Ordered versions are a concept unique to Constrained Semantic versions. The
111+
constraints applied to a SemVer allow creation of an integral value for all versions,
112+
except CI builds. Ignoring CI builds for the moment, the ordered number is computed
113+
from the values of the various parts of a version as they are constrained by the
114+
CSemVer spec. The math involved is not important for this discussion. Just that each
115+
Constrained Version is representable as a distinct integral value (63 bits actually).
116+
A CSemVer-CI build has two elements the base build and the additional 'BuildIndex' and
117+
'BuildName' components. This means the string, File version and ordered version
118+
numbers are confusingly different for a CI build. The ordered version number does NOT
119+
account for CI in any way. It is ONLY able to convert to/from a CSemVer. Thus, a
120+
CSemVer-CI has an ambiguous conversion. Should it convert the Patch+1 form in a string
121+
or the base build number?
114122

115123
### File Version Quad and UINT64
116124
A file Version quad is a data structure that is blittable as an unsigned 64 bit value.
@@ -153,10 +161,11 @@ Bits 1-63 are the same as the ordered version of the base build for a CI build a
153161
the same as the ordered version of a release build.
154162

155163
------
156-
<sup><a id="footnote_1">1</a></sup> Endianess of the platform does not matter as the bits are numbered as MSB->LSB
157-
and the actual byte layout is dependent on the target platform even though the bits
158-
are not. It is NOT safe to transfer a FileVersion (or Ordered version) as in integral
159-
value without considering the endianess of the source, target and transport mechanism,
160-
all of which are out of scope for this library and the CSemVer spec in general.
164+
<sup><a id="footnote_1">1</a></sup> Endianess of the platform does not matter for the
165+
purposes of discussion or this library, as the bits are numbered as MSB->LSB and the
166+
actual byte layout is dependent on the target platform even though the bits are not.
167+
It is NOT safe to transfer a FileVersion (or Ordered version) as in integral value
168+
without considering the endianess of the source, target and transport mechanism, all
169+
of which are out of scope for this library and the CSemVer spec in general.
161170

162171

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
using Microsoft.Build.Utilities.ProjectCreation;
1616
using Microsoft.VisualStudio.TestTools.UnitTesting;
1717

18+
using Ubiquity.NET.Versioning.Build.Tasks.UT.Support;
19+
1820
namespace Ubiquity.NET.Versioning.Build.Tasks.UT
1921
{
2022
[TestClass]

src/Ubiquity.NET.Versioning.Build.Tasks.UT/BuildTaskErrorTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
using Microsoft.Build.Evaluation;
1212
using Microsoft.VisualStudio.TestTools.UnitTesting;
1313

14+
using Ubiquity.NET.Versioning.Build.Tasks.UT.Support;
15+
1416
namespace Ubiquity.NET.Versioning.Build.Tasks.UT
1517
{
1618
[TestClass]

src/Ubiquity.NET.Versioning.Build.Tasks.UT/BuildTaskTests.cs

Lines changed: 161 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@
77
using System;
88
using System.Collections.Generic;
99
using System.Globalization;
10+
using System.Linq;
11+
using System.Text;
1012

1113
using Microsoft.Build.Evaluation;
1214
using Microsoft.VisualStudio.TestTools.UnitTesting;
1315

16+
using Ubiquity.NET.Versioning.Build.Tasks.UT.Support;
17+
1418
namespace Ubiquity.NET.Versioning.Build.Tasks.UT
1519
{
1620
[TestClass]
@@ -52,8 +56,7 @@ public void GoldenPathTest( string targetFramework )
5256
// is not possible to know 'a priori' what the value will be..., additionally, the
5357
// CiBuildName is dependent on the environment. Other, tests validate the behavior of
5458
// those with an explicit setting...
55-
string expectedFullBuildNumber = $"20.1.5-alpha.ci.{props.CiBuildIndex}.{props.CiBuildName}";
56-
string expectedShortNumber = $"20.1.5-a.ci.{props.CiBuildIndex}.{props.CiBuildName}";
59+
string expectedFullBuildNumber = $"20.1.5-alpha.0.0.ci.{props.CiBuildIndex}.{props.CiBuildName}";
5760
string expectedFileVersion = "5.44854.3875.59947"; // CI build
5861

5962
Assert.IsNotNull(props.BuildMajor, "should have a value set for 'BuildMajor'");
@@ -123,6 +126,7 @@ public void BuildVersionXmlIsUsed( string targetFramework )
123126
// v20.1.5 => 5.44854.3880.52268 [see: https://csemver.org/playground/site/#/]
124127
// NOTE: CI build is Patch+1 for the string form
125128
// and for a FileVersion, it is baseBuild + CI bit.
129+
// Non-prerelease double dash.
126130
string expectedFullBuildNumber = $"20.1.6--ci.ABCDEF12.ZZZ";
127131
string expectedFileVersion = "5.44854.3880.52269";
128132

@@ -253,32 +257,54 @@ public void CiBuildInfoIsProcessedCorrectly( string targetFramework )
253257
}
254258

255259
[TestMethod]
256-
[DataRow("netstandard2.0")]
257-
[DataRow("net48")]
258-
[DataRow("net8.0")]
259-
public void PreReleaseFixOfZeroNotShownIfNumber( string targetFramework )
260+
[DynamicData(nameof(GetPrereleaseTestData))]
261+
public void PreReleaseFixShownCorrectly( PrereleaseTestData data )
260262
{
263+
// This test validates how pre-release number and fix are shown.
264+
// For a CSemVer-CI these are always shown, even if 0. For a
265+
// CSemVer, however, they are not shown if zero except the Number which
266+
// is shown as 0 IFF Fix > 0.
267+
261268
// NOT using BuildVersion.xml, all values set as globals to test handling of that
262269
var globalProperties = new Dictionary<string, string>
263270
{
264271
[PropertyNames.BuildMajor] = "20",
265272
[PropertyNames.BuildMinor] = "1",
266273
[PropertyNames.BuildPatch] = "5",
267274
[PropertyNames.PreReleaseName] = "delta",
268-
[PropertyNames.PreReleaseNumber] = "1",
269-
[PropertyNames.PreReleaseFix] = "0",
270-
[PropertyNames.BuildTime] = "2025-06-02T10:15:48-07:00", // Format typical of commit date time stamp
271-
[PropertyNames.CiBuildName] = "QRP", // Intentionally, not a standard value
275+
[PropertyNames.PreReleaseNumber] = data.Number.ToString(CultureInfo.InvariantCulture),
276+
[PropertyNames.PreReleaseFix] = data.Fix.ToString(CultureInfo.InvariantCulture),
277+
[EnvVarNames.IsAutomatedBuild] = "true", // Not a local build (only relevant for a CI build)
272278
};
273279

274-
// compute build index from the time stamp to get the expected value of the index
275-
// Technically, this is const as the time stamp itself is a const, but this saves on
276-
// "magic numbers" and allows easier updates to validate a different time stamp.
277-
var parsedBuildTime = DateTime.Parse(globalProperties[PropertyNames.BuildTime], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);
278-
string expectedIndex = parsedBuildTime.ToBuildIndex();
280+
string? expectedCiIndex = string.Empty;
281+
string? expectedCiName = string.Empty;
282+
string versionBase = "20.1.5-delta";
283+
284+
if(data.IsCI)
285+
{
286+
globalProperties[PropertyNames.BuildTime] = "2025-06-02T10:15:48-07:00"; // Format typical of commit date time stamp
287+
globalProperties[PropertyNames.CiBuildName] = "QRP"; // Intentionally, not a standard value
288+
289+
// compute build index from the time stamp to get the expected value of the index
290+
// Technically, this is const as the time stamp itself is a const, but this saves on
291+
// "magic numbers" and allows easier updates to validate a different time stamp.
292+
var parsedBuildTime = DateTime.Parse(globalProperties[PropertyNames.BuildTime], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);
293+
expectedCiIndex = parsedBuildTime.ToBuildIndex();
294+
expectedCiName = globalProperties[PropertyNames.CiBuildName];
295+
296+
// CI version strings are Patch+1!
297+
versionBase = "20.1.6-delta";
298+
}
299+
else
300+
{
301+
// ensure generation is based on the test input and does NOT create
302+
// a CI build version if it isn't supposed to.
303+
globalProperties[EnvVarNames.IsReleaseBuild] = "true";
304+
}
279305

280306
using var collection = new ProjectCollection(globalProperties);
281-
using var fullResults = Context.CreateTestProjectAndInvokeTestedPackage(targetFramework, collection);
307+
using var fullResults = Context.CreateTestProjectAndInvokeTestedPackage(data.Tfm, collection);
282308
var (buildResults, props) = fullResults;
283309
Assert.IsTrue(buildResults.Success);
284310

@@ -289,8 +315,25 @@ public void PreReleaseFixOfZeroNotShownIfNumber( string targetFramework )
289315
// is not possible to know 'a priori' what the value will be..., additionally, the
290316
// CiBuildName is dependent on the environment. Other, tests validate the behavior of
291317
// those with an explicit setting...
292-
string expectedFullBuildNumber = $"20.1.6-delta.1.ci.{expectedIndex}.QRP";
293-
string expectedFileVersion = "5.44854.3878.63541"; // CI Build (+1)
318+
var expectedVersionbldr = new StringBuilder(versionBase);
319+
string expectedPrerelString = data.Expected;
320+
if(!string.IsNullOrWhiteSpace(expectedPrerelString))
321+
{
322+
expectedVersionbldr.Append('.')
323+
.Append(expectedPrerelString);
324+
}
325+
326+
if( data.IsCI )
327+
{
328+
expectedVersionbldr.Append(".ci")
329+
.Append('.')
330+
.Append(expectedCiIndex)
331+
.Append('.')
332+
.Append(expectedCiName);
333+
}
334+
335+
string expectedFullBuildNumber = expectedVersionbldr.ToString();
336+
FileVersionQuad expectedFileVersion = ExpectedFileVersion(data);
294337

295338
Assert.IsNotNull(props.BuildMajor, "should have a value set for 'BuildMajor'");
296339
Assert.AreEqual(20u, props.BuildMajor.Value);
@@ -305,34 +348,63 @@ public void PreReleaseFixOfZeroNotShownIfNumber( string targetFramework )
305348
Assert.AreEqual("delta", props.PreReleaseName);
306349

307350
Assert.IsNotNull(props.PreReleaseNumber, "Should have a value set for 'PreReleaseNumber'");
308-
Assert.AreEqual((ushort)1u, props.PreReleaseNumber);
351+
Assert.AreEqual(data.Number, props.PreReleaseNumber);
309352

310353
Assert.IsNotNull(props.PreReleaseFix, "Should have a value set for 'PreReleaseFix'");
311-
Assert.AreEqual((ushort)0u, props.PreReleaseFix);
354+
Assert.AreEqual(data.Fix, props.PreReleaseFix);
312355

313356
Assert.AreEqual(expectedFullBuildNumber, props.FullBuildNumber);
314357
Assert.AreEqual(expectedFullBuildNumber, props.PackageVersion);
315358

316-
Assert.AreEqual(globalProperties[PropertyNames.BuildTime], props.BuildTime);
317-
Assert.AreEqual(expectedIndex, props.CiBuildIndex);
359+
if( data.IsCI )
360+
{
361+
Assert.AreEqual(globalProperties[PropertyNames.BuildTime], props.BuildTime);
362+
}
318363

319-
Assert.AreEqual("QRP", props.CiBuildName);
364+
// Default for these is NULL (not specified) so these should match independent of CI
365+
Assert.AreEqual(expectedCiIndex, props.CiBuildIndex);
366+
Assert.AreEqual(expectedCiName, props.CiBuildName);
320367

321368
Assert.IsNotNull(props.FileVersionMajor);
322-
Assert.AreEqual(5, props.FileVersionMajor.Value);
369+
Assert.AreEqual(expectedFileVersion.Major, props.FileVersionMajor.Value);
323370

324371
Assert.IsNotNull(props.FileVersionMinor);
325-
Assert.AreEqual(44854, props.FileVersionMinor.Value);
372+
Assert.AreEqual(expectedFileVersion.Minor, props.FileVersionMinor.Value);
326373

327374
Assert.IsNotNull(props.FileVersionBuild);
328-
Assert.AreEqual(3878, props.FileVersionBuild.Value);
375+
Assert.AreEqual(expectedFileVersion.Build, props.FileVersionBuild.Value);
329376

330377
Assert.IsNotNull(props.FileVersionRevision);
331-
Assert.AreEqual(63541, props.FileVersionRevision.Value);
378+
Assert.AreEqual(expectedFileVersion.Revision, props.FileVersionRevision.Value);
332379

333-
Assert.AreEqual(expectedFileVersion, props.FileVersion);
334-
Assert.AreEqual(expectedFileVersion, props.AssemblyVersion);
380+
string expectedFileVersionString = expectedFileVersion.ToString();
381+
Assert.AreEqual(expectedFileVersionString, props.FileVersion);
382+
Assert.AreEqual(expectedFileVersionString, props.AssemblyVersion);
335383
Assert.AreEqual(expectedFullBuildNumber, props.InformationalVersion);
384+
385+
// Inline function to support simpler conversion of parameters to
386+
// expected FileVersionQuad
387+
//
388+
// v20.1.5-delta => 5.44854.3878.63340 [see: https://csemver.org/playground/site/#/]
389+
// NOTE: CI build is Patch+1
390+
static FileVersionQuad ExpectedFileVersion( PrereleaseTestData d )
391+
{
392+
FileVersionQuad retVal = d.PrereleaseIndex switch
393+
{
394+
0 => new(5, 44854, 3878, 63340), // v20.1.5-delta[0.0]
395+
1 => new(5, 44854, 3878, 63342), // v20.1.5-delta.0.1
396+
2 => new(5, 44854, 3878, 63540), // v20.1.5-delta.1[.0]
397+
3 => new(5, 44854, 3878, 63542), // v20.1.5-delta.1.1
398+
_ => throw new InvalidOperationException("Unknown pre-release index")
399+
};
400+
401+
if(d.IsCI)
402+
{
403+
retVal = retVal with { Revision = (ushort)(retVal.Revision + 1u) };
404+
}
405+
406+
return retVal;
407+
}
336408
}
337409

338410
[TestMethod]
@@ -371,6 +443,13 @@ public void ValidateVersionFormatting( bool isPreRelease, bool isCiBuild, bool i
371443
globalProperties[PropertyNames.CiBuildIndex] = "MyIndex"; // Intentionally not a standard value
372444
globalProperties[PropertyNames.CiBuildName] = "QRP"; // Intentionally, not a standard value
373445
string ciSuffix = isPreRelease ? ".ci.MyIndex.QRP" : "--ci.MyIndex.QRP";
446+
447+
if (isPreRelease)
448+
{
449+
// CI Builds Always include both parts
450+
expectedFullBuildNumber += $".{globalProperties[PropertyNames.PreReleaseFix]}";
451+
}
452+
374453
expectedFullBuildNumber += ciSuffix;
375454
}
376455
else
@@ -454,11 +533,62 @@ public void ValidateVersionFormatting( bool isPreRelease, bool isCiBuild, bool i
454533
// NOTE: CI build is +1 (FileVersionRevision)!
455534
static FileVersionQuad ExpectedFileVersion( bool isPreRelease, bool isCiBuild )
456535
{
457-
FileVersionQuad retVal = isPreRelease ? new(5, 44854, 3876, 34610) : new(5, 44854, 3878, 23338);
536+
FileVersionQuad retVal = isPreRelease
537+
? new(5, 44854, 3876, 34610)
538+
: new(5, 44854, 3878, 23338);
458539

459540
// NOTE: ODD numbered revisions are for CI builds.
460-
return isCiBuild ? retVal with { Revision = (ushort)(retVal.Revision + 1u) } : retVal;
541+
return isCiBuild
542+
? retVal with { Revision = (ushort)(retVal.Revision + 1u) }
543+
: retVal;
461544
}
462545
}
546+
547+
private static IEnumerable<PrereleaseTestData> GetPrereleaseTestData()
548+
{
549+
return from tfm in TargetFrameworks
550+
from num in NumberOrFixArray
551+
from fix in NumberOrFixArray
552+
from isCI in BooleanValue
553+
select new PrereleaseTestData(tfm, num, fix, isCI);
554+
}
555+
556+
private static IEnumerable<string> TargetFrameworks => ["netstandard2.0", "net48", "net8.0"];
557+
558+
private static readonly bool[] BooleanValue = [true, false];
559+
560+
private static readonly byte[] NumberOrFixArray = [0, 1];
561+
}
562+
563+
public readonly record struct PrereleaseTestData(string Tfm, byte Number, byte Fix, bool IsCI)
564+
{
565+
internal string Expected
566+
{
567+
get
568+
{
569+
// CSemVer-CI ***ALWAYS*** includes the build numbers for pre-release versions
570+
// this ensures correct sort ordering of CI builds (which are POST-RELEASE)
571+
if(IsCI)
572+
{
573+
return $"{Number}.{Fix}";
574+
}
575+
576+
// Non CI Might include the release number of zero (IFF Fix > 0)
577+
var bldr = new StringBuilder();
578+
if(Number > 0 || Fix > 0)
579+
{
580+
bldr.Append(Number);
581+
if(Fix > 0)
582+
{
583+
bldr.Append('.')
584+
.Append(Fix);
585+
}
586+
}
587+
588+
return bldr.ToString();
589+
}
590+
}
591+
592+
internal int PrereleaseIndex => (Number << 1) + Fix;
463593
}
464594
}

src/Ubiquity.NET.Versioning.Build.Tasks.UT/CreateVersionInfoTaskErrorTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
using Microsoft.Build.Evaluation;
1313
using Microsoft.VisualStudio.TestTools.UnitTesting;
1414

15+
using Ubiquity.NET.Versioning.Build.Tasks.UT.Support;
16+
1517
namespace Ubiquity.NET.Versioning.Build.Tasks.UT
1618
{
1719
[TestClass]

src/Ubiquity.NET.Versioning.Build.Tasks.UT/ParseBuildVersionXmlTaskErrorTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
using Microsoft.Build.Evaluation;
1414
using Microsoft.VisualStudio.TestTools.UnitTesting;
1515

16+
using Ubiquity.NET.Versioning.Build.Tasks.UT.Support;
17+
1618
namespace Ubiquity.NET.Versioning.Build.Tasks.UT
1719
{
1820
[TestClass]

src/Ubiquity.NET.Versioning.Build.Tasks.UT/AssemblyInfo.cs renamed to src/Ubiquity.NET.Versioning.Build.Tasks.UT/Support/AssemblyInfo.cs

File renamed without changes.

0 commit comments

Comments
 (0)