From ac9485d87bc1c8d0f22a5b8bb2c40bd7e8e7baf3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Nov 2025 21:06:22 +0000 Subject: [PATCH 1/2] Fix legacy composite format syntax handling (Issue #4654) This commit fixes the bug where legacy .NET composite format syntax like {CommitsSinceVersionSource:0000;;''} was not working correctly. The issue was that the formatter would output literal text instead of properly formatted values. For example, it would output: "6.13.54-gv6-CommitsSinceVersionSource-0000-----" instead of: "6.13.54-gv60002" Changes: 1. Updated RegexPatterns.cs to allow semicolons and quotes in format strings by changing the format pattern from [A-Za-z0-9\.\-,]+ to [A-Za-z0-9\.\-,;'"]+ 2. Added LegacyCompositeFormatter.cs to properly handle the three-section composite format syntax (positive;negative;zero) with support for: - Zero-suppression using empty string ('') - Proper handling of negative values (no double negative) - Numeric formatting in all sections 3. Updated ValueFormatter.cs to register LegacyCompositeFormatter with priority 1 and allow null values to be processed by formatters 4. Updated StringFormatWithExtension.cs to not return early for null values when legacy syntax is detected 5. Added Issue4654Tests.cs to verify the fix works correctly The fix ensures backward compatibility with legacy .NET composite format syntax while maintaining support for modern format strings. --- .../Issues/Issue4654Tests.cs | 101 +++++++++++ src/GitVersion.Core/Core/RegexPatterns.cs | 2 +- .../Formatting/LegacyCompositeFormatter.cs | 164 ++++++++++++++++++ .../Formatting/StringFormatWithExtension.cs | 8 +- .../Formatting/ValueFormatter.cs | 12 +- 5 files changed, 280 insertions(+), 7 deletions(-) create mode 100644 src/GitVersion.Core.Tests/Issues/Issue4654Tests.cs create mode 100644 src/GitVersion.Core/Formatting/LegacyCompositeFormatter.cs diff --git a/src/GitVersion.Core.Tests/Issues/Issue4654Tests.cs b/src/GitVersion.Core.Tests/Issues/Issue4654Tests.cs new file mode 100644 index 0000000000..e1e363971c --- /dev/null +++ b/src/GitVersion.Core.Tests/Issues/Issue4654Tests.cs @@ -0,0 +1,101 @@ +using System.Globalization; +using GitVersion.Core.Tests.Helpers; +using GitVersion.Formatting; + +namespace GitVersion.Core.Tests.Issues; + +[TestFixture] +public class Issue4654Tests +{ + private const string TestVersion = "6.13.54"; + private const string TestVersionWithPreRelease = "6.13.54-gv60002"; + private const string TestPreReleaseLabel = "gv6"; + private const string TestPreReleaseLabelWithDash = "-gv6"; + + [Test] + [Category("Issue4654")] + public void Issue4654_ExactReproduction_ShouldFormatCorrectly() + { + var semanticVersion = new SemanticVersion + { + Major = 6, + Minor = 13, + Patch = 54, + PreReleaseTag = new SemanticVersionPreReleaseTag(TestPreReleaseLabel, 1, true), + BuildMetaData = new SemanticVersionBuildMetaData() + { + Branch = "feature/gv6", + VersionSourceSha = "21d7e26e6ff58374abd3daf2177be4b7a9c49040", + Sha = "489a0c0ab425214def918e36399f3cc3c9a9c42d", + ShortSha = "489a0c0", + CommitsSinceVersionSource = 2, + CommitDate = DateTimeOffset.Parse("2025-08-12", CultureInfo.InvariantCulture), + UncommittedChanges = 0 + } + }; + + var extendedVersion = new + { + semanticVersion.Major, + semanticVersion.Minor, + semanticVersion.Patch, + semanticVersion.BuildMetaData.CommitsSinceVersionSource, + MajorMinorPatch = $"{semanticVersion.Major}.{semanticVersion.Minor}.{semanticVersion.Patch}", + PreReleaseLabel = semanticVersion.PreReleaseTag.Name, + PreReleaseLabelWithDash = string.IsNullOrEmpty(semanticVersion.PreReleaseTag.Name) + ? "" + : $"-{semanticVersion.PreReleaseTag.Name}", + AssemblySemFileVer = TestVersion + ".0", + AssemblySemVer = TestVersion + ".0", + BranchName = "feature/gv6", + EscapedBranchName = "feature-gv6", + FullSemVer = "6.13.54-gv6.1+2", + SemVer = "6.13.54-gv6.1" + }; + + const string template = "{MajorMinorPatch}{PreReleaseLabelWithDash}{CommitsSinceVersionSource:0000;;''}"; + const string expected = TestVersionWithPreRelease; + + var actual = template.FormatWith(extendedVersion, new TestEnvironment()); + + actual.ShouldBe(expected, "The legacy ;;'' syntax should format CommitsSinceVersionSource as 0002, not as literal text"); + } + + [Test] + [Category("Issue4654")] + public void Issue4654_WithoutLegacySyntax_ShouldStillWork() + { + var testData = new + { + MajorMinorPatch = TestVersion, + PreReleaseLabelWithDash = TestPreReleaseLabelWithDash, + CommitsSinceVersionSource = 2 + }; + + const string template = "{MajorMinorPatch}{PreReleaseLabelWithDash}{CommitsSinceVersionSource:0000}"; + const string expected = TestVersionWithPreRelease; + + var actual = template.FormatWith(testData, new TestEnvironment()); + + actual.ShouldBe(expected, "New format syntax should work correctly"); + } + + [Test] + [Category("Issue4654")] + public void Issue4654_ZeroValueWithLegacySyntax_ShouldUseEmptyFallback() + { + var mainBranchData = new + { + MajorMinorPatch = TestVersion, + PreReleaseLabelWithDash = "", + CommitsSinceVersionSource = 0 + }; + + const string template = "{MajorMinorPatch}{PreReleaseLabelWithDash}{CommitsSinceVersionSource:0000;;''}"; + const string expected = TestVersion; + + var actual = template.FormatWith(mainBranchData, new TestEnvironment()); + + actual.ShouldBe(expected, "Zero values should use the third section (empty string) in legacy ;;'' syntax"); + } +} diff --git a/src/GitVersion.Core/Core/RegexPatterns.cs b/src/GitVersion.Core/Core/RegexPatterns.cs index a84161731d..00940c9872 100644 --- a/src/GitVersion.Core/Core/RegexPatterns.cs +++ b/src/GitVersion.Core/Core/RegexPatterns.cs @@ -93,7 +93,7 @@ internal static partial class Common | # OR (?[A-Za-z_][A-Za-z0-9_]*) # member/property name (?: # Optional format specifier - :(?[A-Za-z0-9\.\-,]+) # Colon followed by format string (no spaces, ?, or }), format cannot contain colon + :(?[A-Za-z0-9\.\-,;'"]+) # Colon followed by format string (including semicolons and quotes for legacy composite format) )? # Format is optional ) # End group for env or member (?: # Optional fallback group diff --git a/src/GitVersion.Core/Formatting/LegacyCompositeFormatter.cs b/src/GitVersion.Core/Formatting/LegacyCompositeFormatter.cs new file mode 100644 index 0000000000..26f1bbfc23 --- /dev/null +++ b/src/GitVersion.Core/Formatting/LegacyCompositeFormatter.cs @@ -0,0 +1,164 @@ +using System.Globalization; + +namespace GitVersion.Formatting; + +internal class LegacyCompositeFormatter : IValueFormatter +{ + public int Priority => 1; + + public bool TryFormat(object? value, string format, out string result) => + TryFormat(value, format, CultureInfo.InvariantCulture, out result); + + public bool TryFormat(object? value, string format, CultureInfo cultureInfo, out string result) + { + result = string.Empty; + + if (!HasLegacySyntax(format)) + return false; + + var sections = ParseSections(format); + var index = GetSectionIndex(value, sections.Length); + + if (index >= sections.Length) + return true; + + var section = sections[index]; + + // Use absolute value for negative numbers to prevent double negatives + var valueToFormat = (index == 1 && value != null && IsNumeric(value) && Convert.ToDouble(value) < 0) + ? Math.Abs(Convert.ToDouble(value)) + : value; + + result = IsQuotedLiteral(section) + ? UnquoteString(section) + : FormatWithSection(valueToFormat, section, cultureInfo, sections, index); + + return true; + } + + private static bool HasLegacySyntax(string format) => + !string.IsNullOrEmpty(format) && format.Contains(';') && !format.Contains("??"); + + private static string[] ParseSections(string format) + { + var sections = new List(); + var current = new StringBuilder(); + var inQuotes = false; + var quoteChar = '\0'; + + foreach (var c in format) + { + if (!inQuotes && (c == '\'' || c == '"')) + { + inQuotes = true; + quoteChar = c; + } + else if (inQuotes && c == quoteChar) + { + inQuotes = false; + } + else if (!inQuotes && c == ';') + { + sections.Add(current.ToString()); + current.Clear(); + continue; + } + + current.Append(c); + } + + sections.Add(current.ToString()); + return [.. sections]; + } + + private static int GetSectionIndex(object? value, int sectionCount) + { + if (sectionCount == 1) return 0; + if (value == null) return sectionCount >= 3 ? 2 : 0; + + if (!IsNumeric(value)) return 0; + + var num = Convert.ToDouble(value); + return num switch + { + > 0 => 0, + < 0 when sectionCount >= 2 => 1, + 0 when sectionCount >= 3 => 2, + _ => 0 + }; + } + + private static bool IsNumeric(object value) => + value is byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal; + + private static bool IsQuotedLiteral(string section) + { + if (string.IsNullOrEmpty(section)) return true; + var trimmed = section.Trim(); + return (trimmed.StartsWith('\'') && trimmed.EndsWith('\'')) || + (trimmed.StartsWith('"') && trimmed.EndsWith('"')); + } + + private static string UnquoteString(string section) + { + if (string.IsNullOrEmpty(section)) return string.Empty; + var trimmed = section.Trim(); + + // Handle empty quoted strings like '' and "" + if (trimmed == "''" || trimmed == "\"\"") + return string.Empty; + + return IsQuoted(trimmed) && trimmed.Length > 2 + ? trimmed[1..^1] + : trimmed; + + static bool IsQuoted(string s) => + (s.StartsWith('\'') && s.EndsWith('\'')) || (s.StartsWith('"') && s.EndsWith('"')); + } + + private static string FormatWithSection(object? value, string section, IFormatProvider formatProvider, string[]? sections = null, int index = 0) + { + if (string.IsNullOrEmpty(section)) return string.Empty; + if (IsQuotedLiteral(section)) return UnquoteString(section); + + try + { + return value switch + { + IFormattable formattable => formattable.ToString(section, formatProvider), + not null when IsValidFormatString(section) => + string.Format(formatProvider, "{0:" + section + "}", value), + not null when index > 0 && sections != null && sections.Length > 0 && IsValidFormatString(sections[0]) => + // For invalid formats in non-first sections, use first section format + string.Format(formatProvider, "{0:" + sections[0] + "}", value), + not null => value.ToString() ?? string.Empty, + _ => section // Only for null values without valid format + }; + } + catch (FormatException) + { + // On format exception, try first section format or return value string + if (index > 0 && sections != null && sections.Length > 0 && IsValidFormatString(sections[0])) + { + try + { + return string.Format(formatProvider, "{0:" + sections[0] + "}", value); + } + catch + { + return value?.ToString() ?? section; + } + } + return value?.ToString() ?? section; + } + } + + private static bool IsValidFormatString(string format) + { + if (string.IsNullOrEmpty(format)) return false; + + var firstChar = char.ToUpperInvariant(format[0]); + return "CDEFGNPXR".Contains(firstChar) || + format.All(c => "0123456789.,#".Contains(c)); + } +} diff --git a/src/GitVersion.Core/Formatting/StringFormatWithExtension.cs b/src/GitVersion.Core/Formatting/StringFormatWithExtension.cs index 0f061c4f0b..4f770ff656 100644 --- a/src/GitVersion.Core/Formatting/StringFormatWithExtension.cs +++ b/src/GitVersion.Core/Formatting/StringFormatWithExtension.cs @@ -84,7 +84,8 @@ private static string EvaluateMember(T source, string member, string? format, var getter = ExpressionCompiler.CompileGetter(source.GetType(), memberPath); var value = getter(source); - if (value is null) + // Only return early for null if format doesn't use legacy syntax + if (value is null && !HasLegacySyntax(format)) return fallback ?? string.Empty; if (format is not null && ValueFormatter.Default.TryFormat( @@ -95,6 +96,9 @@ private static string EvaluateMember(T source, string member, string? format, return formatted; } - return value.ToString() ?? fallback ?? string.Empty; + return value?.ToString() ?? fallback ?? string.Empty; } + + private static bool HasLegacySyntax(string? format) => + !string.IsNullOrEmpty(format) && format.Contains(';') && !format.Contains("??"); } diff --git a/src/GitVersion.Core/Formatting/ValueFormatter.cs b/src/GitVersion.Core/Formatting/ValueFormatter.cs index 0e0be49645..58989c80b6 100644 --- a/src/GitVersion.Core/Formatting/ValueFormatter.cs +++ b/src/GitVersion.Core/Formatting/ValueFormatter.cs @@ -13,6 +13,7 @@ internal class ValueFormatter : InvariantFormatter, IValueFormatterCombiner internal ValueFormatter() => formatters = [ + new LegacyCompositeFormatter(), new StringFormatter(), new FormattableFormatter(), new NumericFormatter(), @@ -22,17 +23,20 @@ internal ValueFormatter() public override bool TryFormat(object? value, string format, CultureInfo cultureInfo, out string result) { result = string.Empty; - if (value is null) - { - return false; - } + // Allow formatters to handle null values (e.g., legacy composite formatter for zero sections) foreach (var formatter in formatters.OrderBy(f => f.Priority)) { if (formatter.TryFormat(value, format, out result)) return true; } + // Only return false if no formatter could handle it + if (value is null) + { + return false; + } + return false; } From b638fe2889abc5de38004d707e8e0f1317a59883 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Nov 2025 00:09:35 +0000 Subject: [PATCH 2/2] Add comprehensive test coverage for legacy composite formatter Adds all test files from the FormatNoFormat branch to ensure complete coverage of the LegacyCompositeFormatter functionality: - BackwardCompatibilityTests.cs (2 tests): Tests regex pattern parsing for legacy semicolon syntax and mixed new/old syntax - LegacyFormatterProblemTests.cs (9 tests): Comprehensive edge case tests covering: * Null value handling * Missing property error handling * Double negative prevention * Positive/negative/zero section selection * Numeric formatting in all sections * Invalid format handling * Issue #4654 verification tests - LegacyFormattingSyntaxTests.cs (7 tests): Tests for legacy format syntax parsing and behavior Also fixes namespace from GitVersion.Tests.Formatting to GitVersion.Core.Tests.Formatting in: - ValueFormatterTests.cs - StringFormatterTests.cs - DateFormatterTests.cs Total: 21 tests covering legacy composite format functionality --- .../Formatting/BackwardCompatibilityTests.cs | 35 ++++ .../Formatting/DateFormatterTests.cs | 2 +- .../Formatting/LegacyFormatterProblemTests.cs | 163 ++++++++++++++++++ .../Formatting/LegacyFormattingSyntaxTests.cs | 138 +++++++++++++++ .../Formatting/StringFormatterTests.cs | 2 +- .../Formatting/ValueFormatterTests.cs | 2 +- 6 files changed, 339 insertions(+), 3 deletions(-) create mode 100644 src/GitVersion.Core.Tests/Formatting/BackwardCompatibilityTests.cs create mode 100644 src/GitVersion.Core.Tests/Formatting/LegacyFormatterProblemTests.cs create mode 100644 src/GitVersion.Core.Tests/Formatting/LegacyFormattingSyntaxTests.cs diff --git a/src/GitVersion.Core.Tests/Formatting/BackwardCompatibilityTests.cs b/src/GitVersion.Core.Tests/Formatting/BackwardCompatibilityTests.cs new file mode 100644 index 0000000000..390d86427c --- /dev/null +++ b/src/GitVersion.Core.Tests/Formatting/BackwardCompatibilityTests.cs @@ -0,0 +1,35 @@ +namespace GitVersion.Core.Tests.Formatting; + +[TestFixture] +public class LegacyRegexPatternTests +{ + [Test] + public void ExpandTokensRegex_ShouldParseLegacySemicolonSyntax() + { + const string input = "{CommitsSinceVersionSource:0000;;''}"; + + var matches = RegexPatterns.Common.ExpandTokensRegex().Matches(input); + + matches.Count.ShouldBe(1); + var match = matches[0]; + match.Groups["member"].Value.ShouldBe("CommitsSinceVersionSource"); + match.Groups["format"].Success.ShouldBeTrue(); + } + + [Test] + public void ExpandTokensRegex_ShouldHandleMixedSyntax() + { + const string input = "{NewStyle:0000 ?? 'fallback'} {OldStyle:pos;neg;zero}"; + + var matches = RegexPatterns.Common.ExpandTokensRegex().Matches(input); + + matches.Count.ShouldBe(2); + + var newMatch = matches[0]; + newMatch.Groups["member"].Value.ShouldBe("NewStyle"); + newMatch.Groups["fallback"].Value.ShouldBe("fallback"); + + var oldMatch = matches[1]; + oldMatch.Groups["member"].Value.ShouldBe("OldStyle"); + } +} diff --git a/src/GitVersion.Core.Tests/Formatting/DateFormatterTests.cs b/src/GitVersion.Core.Tests/Formatting/DateFormatterTests.cs index 417c61d6b9..dd89018c81 100644 --- a/src/GitVersion.Core.Tests/Formatting/DateFormatterTests.cs +++ b/src/GitVersion.Core.Tests/Formatting/DateFormatterTests.cs @@ -1,7 +1,7 @@ using System.Globalization; using GitVersion.Formatting; -namespace GitVersion.Tests.Formatting; +namespace GitVersion.Core.Tests.Formatting; [TestFixture] public class DateFormatterTests diff --git a/src/GitVersion.Core.Tests/Formatting/LegacyFormatterProblemTests.cs b/src/GitVersion.Core.Tests/Formatting/LegacyFormatterProblemTests.cs new file mode 100644 index 0000000000..e501e10d0e --- /dev/null +++ b/src/GitVersion.Core.Tests/Formatting/LegacyFormatterProblemTests.cs @@ -0,0 +1,163 @@ +using GitVersion.Core.Tests.Helpers; +using GitVersion.Formatting; + +namespace GitVersion.Core.Tests.Formatting; + +[TestFixture] +public class LegacyFormatterProblemTests +{ + private TestEnvironment environment; + + [SetUp] + public void Setup() => environment = new TestEnvironment(); + + // ========================================== + // PROBLEM 1: Non-existent properties + // ========================================== + + [Test] + [Category("Problem2")] + public void Problem2_NullValue_ShouldUseZeroSection() + { + var testObject = new { Value = (int?)null }; + const string template = "{Value:positive;negative;zero}"; + const string expected = "zero"; + + var actual = template.FormatWith(testObject, environment); + actual.ShouldBe(expected, "Null values should use zero section without transformation"); + } + + [Test] + [Category("Problem1")] + public void Problem1_MissingProperty_ShouldFailGracefully() + { + // Test tries to use {MajorMinorPatch} on SemanticVersion but that property doesn't exist + var semanticVersion = new SemanticVersion + { + Major = 1, + Minor = 2, + Patch = 3 + }; + + const string template = "{MajorMinorPatch}"; // This property doesn't exist on SemanticVersion + + // Currently this will throw or behave unexpectedly + // Should either throw meaningful error or handle gracefully + Assert.Throws(() => template.FormatWith(semanticVersion, environment)); + } + + // ========================================== + // PROBLEM 2: Double negative handling + // ========================================== + + [Test] + [Category("Problem2")] + public void Problem2_NegativeValue_ShouldNotDoubleNegative() + { + var testObject = new { Value = -5 }; + const string template = "{Value:positive;negative;zero}"; + + // EXPECTED: "negative" (just the literal text from section 2) + // ACTUAL: "-negative" (the negative sign from -5 plus the literal "negative") + const string expected = "negative"; + + var actual = template.FormatWith(testObject, environment); + + // This will currently fail - we get "-negative" instead of "negative" + actual.ShouldBe(expected, "Negative values should use section text without the negative sign"); + } + + [Test] + [Category("Problem2")] + public void Problem2_PositiveValue_ShouldFormatCorrectly() + { + var testObject = new { Value = 5 }; + const string template = "{Value:positive;negative;zero}"; + const string expected = "positive"; + + var actual = template.FormatWith(testObject, environment); + actual.ShouldBe(expected); + } + + [Test] + [Category("Problem2")] + public void Problem2_ZeroValue_ShouldUseZeroSection() + { + var testObject = new { Value = 0 }; + const string template = "{Value:positive;negative;zero}"; + const string expected = "zero"; + + var actual = template.FormatWith(testObject, environment); + actual.ShouldBe(expected); + } + + // ========================================== + // PROBLEM 3: Insufficient formatting logic + // ========================================== + + [Test] + [Category("Problem3")] + public void Problem3_NumericFormatting_AllSectionsShouldFormat() + { + // Test that numeric formatting works in ALL sections, not just first + var testObject = new { Value = -42 }; + const string template = "{Value:0000;0000;0000}"; // All sections should pad with zeros + + // EXPECTED: "0042" (absolute value 42, formatted with 0000 in negative section) + // ACTUAL: "0000" (literal text instead of formatted value) + const string expected = "0042"; + + var actual = template.FormatWith(testObject, environment); + actual.ShouldBe(expected, "Negative section should format the absolute value, not return literal"); + } + + [Test] + [Category("Problem3")] + public void Problem3_FirstSectionWorks_OthersDont() + { + // Demonstrate that first section works but others don't + var positiveObject = new { Value = 42 }; + var negativeObject = new { Value = -42 }; + + const string template = "{Value:0000;WRONG;WRONG}"; + + // First section (positive) should work correctly + var positiveResult = template.FormatWith(positiveObject, environment); + positiveResult.ShouldBe("0042", "First section should format correctly"); + + // Second section (negative) should return literal when invalid format provided + var negativeResult = template.FormatWith(negativeObject, environment); + // Invalid format "WRONG" should return literal to give user feedback about their error + negativeResult.ShouldBe("WRONG", "Invalid format should return literal to indicate user error"); + } + + // ========================================== + // VERIFY #4654 FIX STILL WORKS + // ========================================== + + [Test] + [Category("Issue4654")] + public void Issue4654_LegacySyntax_ShouldStillWork() + { + // Verify the original #4654 fix still works + var testObject = new { CommitsSinceVersionSource = 2 }; + const string template = "{CommitsSinceVersionSource:0000;;''}"; + const string expected = "0002"; + + var actual = template.FormatWith(testObject, environment); + actual.ShouldBe(expected, "Issue #4654 fix must be preserved"); + } + + [Test] + [Category("Issue4654")] + public void Issue4654_ZeroValue_ShouldUseEmptyString() + { + // Zero values should use the third section (empty string) + var testObject = new { CommitsSinceVersionSource = 0 }; + const string template = "{CommitsSinceVersionSource:0000;;''}"; + const string expected = ""; + + var actual = template.FormatWith(testObject, environment); + actual.ShouldBe(expected, "Zero values should use third section (empty)"); + } +} diff --git a/src/GitVersion.Core.Tests/Formatting/LegacyFormattingSyntaxTests.cs b/src/GitVersion.Core.Tests/Formatting/LegacyFormattingSyntaxTests.cs new file mode 100644 index 0000000000..a63ddbe2ef --- /dev/null +++ b/src/GitVersion.Core.Tests/Formatting/LegacyFormattingSyntaxTests.cs @@ -0,0 +1,138 @@ +using System.Globalization; +using GitVersion.Core.Tests.Helpers; +using GitVersion.Formatting; + +namespace GitVersion.Core.Tests.Formatting; + +[TestFixture] +public class LegacyFormattingSyntaxTests +{ + [Test] + public void FormatWith_LegacyZeroFallbackSyntax_ShouldWork() + { + var semanticVersion = new SemanticVersion + { + Major = 6, + Minor = 13, + Patch = 54, + PreReleaseTag = new SemanticVersionPreReleaseTag("gv6", 1, true), + BuildMetaData = new SemanticVersionBuildMetaData() + { + Branch = "feature/gv6", + VersionSourceSha = "versionSourceSha", + Sha = "489a0c0ab425214def918e36399f3cc3c9a9c42d", + ShortSha = "489a0c0", + CommitsSinceVersionSource = 2, + CommitDate = DateTimeOffset.Parse("2025-08-12", CultureInfo.InvariantCulture) + } + }; + + const string template = "{MajorMinorPatch}{PreReleaseLabelWithDash}{CommitsSinceVersionSource:0000;;''}"; + const string expected = "6.13.54-gv60002"; + + var actual = template.FormatWith(semanticVersion, new TestEnvironment()); + + actual.ShouldBe(expected); + } + + [Test] + public void FormatWith_LegacyThreeSectionSyntax_ShouldWork() + { + var testObject = new { Value = -5 }; + const string template = "{Value:positive;negative;zero}"; + const string expected = "negative"; + + var actual = template.FormatWith(testObject, new TestEnvironment()); + + actual.ShouldBe(expected); + } + + [Test] + public void FormatWith_LegacyTwoSectionSyntax_ShouldWork() + { + var testObject = new { Value = -10 }; + const string template = "{Value:positive;negative}"; + const string expected = "negative"; + + var actual = template.FormatWith(testObject, new TestEnvironment()); + + actual.ShouldBe(expected); + } + + [Test] + public void FormatWith_LegacyZeroValue_ShouldUseThirdSection() + { + var testObject = new { Value = 0 }; + const string template = "{Value:pos;neg;ZERO}"; + const string expected = "ZERO"; + + var actual = template.FormatWith(testObject, new TestEnvironment()); + + actual.ShouldBe(expected); + } + + [Test] + public void FormatWith_MixedLegacyAndNewSyntax_ShouldWork() + { + var testObject = new + { + OldStyle = 0, + NewStyle = 42, + RegularProp = "test" + }; + const string template = "{OldStyle:pos;neg;''}{NewStyle:0000 ?? 'fallback'}{RegularProp}"; + const string expected = "0042test"; + + var actual = template.FormatWith(testObject, new TestEnvironment()); + + actual.ShouldBe(expected); + } + + [Test] + public void FormatWith_LegacyWithStandardFormatSpecifiers_ShouldWork() + { + var testObject = new { Amount = 1234.56 }; + const string template = "{Amount:C2;(C2);'No Amount'}"; + const string expected = "¤1,234.56"; + + var actual = template.FormatWith(testObject, new TestEnvironment()); + + actual.ShouldBe(expected); + } + + [Test] + public void FormatWith_Issue4654ExactCase_ShouldWork() + { + var semanticVersion = new SemanticVersion + { + Major = 6, + Minor = 13, + Patch = 54, + PreReleaseTag = new SemanticVersionPreReleaseTag("gv6", 1, true), + BuildMetaData = new SemanticVersionBuildMetaData("Branch.feature-gv6") + { + CommitsSinceVersionSource = 2 + } + }; + + var mainBranchVersion = new SemanticVersion + { + Major = 6, + Minor = 13, + Patch = 54, + PreReleaseTag = new SemanticVersionPreReleaseTag(string.Empty, 0, true), + BuildMetaData = new SemanticVersionBuildMetaData() + { + CommitsSinceVersionSource = 0 + } + }; + + const string template = "{MajorMinorPatch}{PreReleaseLabelWithDash}{CommitsSinceVersionSource:0000;;''}"; + + var featureResult = template.FormatWith(semanticVersion, new TestEnvironment()); + featureResult.ShouldBe("6.13.54-gv60002"); + + var mainResult = template.FormatWith(mainBranchVersion, new TestEnvironment()); + mainResult.ShouldBe("6.13.54"); + } +} diff --git a/src/GitVersion.Core.Tests/Formatting/StringFormatterTests.cs b/src/GitVersion.Core.Tests/Formatting/StringFormatterTests.cs index b9f3c0a27d..7c9024fe79 100644 --- a/src/GitVersion.Core.Tests/Formatting/StringFormatterTests.cs +++ b/src/GitVersion.Core.Tests/Formatting/StringFormatterTests.cs @@ -1,6 +1,6 @@ using GitVersion.Formatting; -namespace GitVersion.Tests.Formatting; +namespace GitVersion.Core.Tests.Formatting; [TestFixture] public class StringFormatterTests diff --git a/src/GitVersion.Core.Tests/Formatting/ValueFormatterTests.cs b/src/GitVersion.Core.Tests/Formatting/ValueFormatterTests.cs index 9950cac172..675377215e 100644 --- a/src/GitVersion.Core.Tests/Formatting/ValueFormatterTests.cs +++ b/src/GitVersion.Core.Tests/Formatting/ValueFormatterTests.cs @@ -1,7 +1,7 @@ using System.Globalization; using GitVersion.Formatting; -namespace GitVersion.Tests.Formatting; +namespace GitVersion.Core.Tests.Formatting; [TestFixture] public class ValueFormatterTests