Skip to content

Support C# format strings in config #4605

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
24 changes: 24 additions & 0 deletions src/GitVersion.Core.Tests/Extensions/ShouldlyExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace GitVersion.Core.Tests.Extensions;

public static class ShouldlyExtensions
{
/// <summary>
/// Asserts that the action throws an exception of type TException
/// with the expected message.
/// </summary>
public static void ShouldThrowWithMessage<TException>(this Action action, string expectedMessage) where TException : Exception
{
var ex = Should.Throw<TException>(action);
ex.Message.ShouldBe(expectedMessage);
}

/// <summary>
/// Asserts that the action throws an exception of type TException,
/// and allows further assertion on the exception instance.
/// </summary>
public static void ShouldThrow<TException>(this Action action, Action<TException> additionalAssertions) where TException : Exception
{
var ex = Should.Throw<TException>(action);
additionalAssertions(ex);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -244,4 +244,29 @@ public void FormatProperty_NullObject_WithFallback_QuotedAndEmpty()
var actual = target.FormatWith(propertyObject, this.environment);
Assert.That(actual, Is.EqualTo(""));
}

[Test]
public void FormatAssemblyInformationalVersionWithSemanticVersionCustomFormattedCommitsSinceVersionSource()
{
var semanticVersion = new SemanticVersion
{
Major = 1,
Minor = 2,
Patch = 3,
PreReleaseTag = new SemanticVersionPreReleaseTag(string.Empty, 9, true),
BuildMetaData = new SemanticVersionBuildMetaData("Branch.main")
{
Branch = "main",
VersionSourceSha = "versionSourceSha",
Sha = "commitSha",
ShortSha = "commitShortSha",
CommitsSinceVersionSource = 42,
CommitDate = DateTimeOffset.Parse("2014-03-06 23:59:59Z")
}
};
const string target = "{Major}.{Minor}.{Patch}-{CommitsSinceVersionSource:0000}";
const string expected = "1.2.3-0042";
var actual = target.FormatWith(semanticVersion, this.environment);
Assert.That(actual, Is.EqualTo(expected));
}
}
39 changes: 39 additions & 0 deletions src/GitVersion.Core.Tests/Formatting/DateFormatterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using GitVersion.Formatting;

namespace GitVersion.Core.Tests.Formatting;

[TestFixture]
public class DateFormatterTests
{
[Test]
public void Priority_ShouldBe2() => new DateFormatter().Priority.ShouldBe(2);

[Test]
public void TryFormat_NullValue_ReturnsFalse()
{
var sut = new DateFormatter();
var result = sut.TryFormat(null, "yyyy-MM-dd", out var formatted);
result.ShouldBeFalse();
formatted.ShouldBeEmpty();
}

[TestCase("2021-01-01", "date:yyyy-MM-dd", "2021-01-01")]
[TestCase("2021-01-01T12:00:00Z", "date:yyyy-MM-ddTHH:mm:ssZ", "2021-01-01T12:00:00Z")]
public void TryFormat_ValidDateFormats_ReturnsExpectedResult(string input, string format, string expected)
{
var date = DateTime.Parse(input);
var sut = new DateFormatter();
var result = sut.TryFormat(date, format, out var formatted);
result.ShouldBeTrue();
formatted.ShouldBe(expected);
}

[Test]
public void TryFormat_UnsupportedFormat_ReturnsFalse()
{
var sut = new DateFormatter();
var result = sut.TryFormat(DateTime.Now, "unsupported", out var formatted);
result.ShouldBeFalse();
formatted.ShouldBeEmpty();
}
}
41 changes: 41 additions & 0 deletions src/GitVersion.Core.Tests/Formatting/FormattableFormatterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using GitVersion.Formatting;

namespace GitVersion.Core.Tests.Formatting;

[TestFixture]
public class FormattableFormatterTests
{
[Test]
public void Priority_ShouldBe2() => new FormattableFormatter().Priority.ShouldBe(2);

[Test]
public void TryFormat_NullValue_ReturnsFalse()
{
var sut = new FormattableFormatter();
var result = sut.TryFormat(null, "G", out var formatted);
result.ShouldBeFalse();
formatted.ShouldBeEmpty();
}

[TestCase(123.456, "F2", "123.46")]
[TestCase(1234.456, "F2", "1234.46")]
public void TryFormat_ValidFormats_ReturnsExpectedResult(object input, string format, string expected)
{
var sut = new FormattableFormatter();
var result = sut.TryFormat(input, format, out var formatted);
result.ShouldBeTrue();
formatted.ShouldBe(expected);
}

[TestCase(123.456, "C", "Format 'C' is not supported in FormattableFormatter")]
[TestCase(123.456, "P", "Format 'P' is not supported in FormattableFormatter")]
[TestCase(1234567890, "N0", "Format 'N0' is not supported in FormattableFormatter")]
[TestCase(1234567890, "Z", "Format 'Z' is not supported in FormattableFormatter")]
public void TryFormat_UnsupportedFormat_ReturnsFalse(object input, string format, string expected)
{
var sut = new FormattableFormatter();
var result = sut.TryFormat(input, format, out var formatted);
result.ShouldBeFalse();
formatted.ShouldBe(expected);
}
}
38 changes: 38 additions & 0 deletions src/GitVersion.Core.Tests/Formatting/NumericFormatterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using GitVersion.Formatting;

namespace GitVersion.Core.Tests.Formatting;

[TestFixture]
public class NumericFormatterTests
{
[Test]
public void Priority_ShouldBe1() => new NumericFormatter().Priority.ShouldBe(1);
[Test]
public void TryFormat_NullValue_ReturnsFalse()
{
var sut = new NumericFormatter();
var result = sut.TryFormat(null, "n", out var formatted);
result.ShouldBeFalse();
formatted.ShouldBeEmpty();
}

[TestCase("1234.5678", "n", "1,234.57")]
[TestCase("1234.5678", "f2", "1234.57")]
[TestCase("1234.5678", "f0", "1235")]
[TestCase("1234.5678", "g", "1234.5678")]
public void TryFormat_ValidFormats_ReturnsExpectedResult(string input, string format, string expected)
{
var sut = new NumericFormatter();
var result = sut.TryFormat(input, format, out var formatted);
result.ShouldBeTrue();
formatted.ShouldBe(expected);
}
[Test]
public void TryFormat_UnsupportedFormat_ReturnsFalse()
{
var sut = new NumericFormatter();
var result = sut.TryFormat(1234.5678, "z", out var formatted);
result.ShouldBeFalse();
formatted.ShouldBeEmpty();
}
}
76 changes: 76 additions & 0 deletions src/GitVersion.Core.Tests/Formatting/StringFormatterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using GitVersion.Formatting;

namespace GitVersion.Core.Tests.Formatting;

[TestFixture]
public class StringFormatterTests
{
[Test]
public void Priority_ShouldBe2() => new StringFormatter().Priority.ShouldBe(2);

[TestCase("u")]
[TestCase("")]
[TestCase(" ")]
[TestCase("invalid")]
public void TryFormat_NullValue_ReturnsFalse(string format)
{
var sut = new StringFormatter();
var result = sut.TryFormat(null, format, out var formatted);
result.ShouldBeFalse();
formatted.ShouldBeEmpty();
}

[TestCase("hello", "u", "HELLO")]
[TestCase("HELLO", "l", "hello")]
[TestCase("hello world", "t", "Hello World")]
[TestCase("hELLO", "s", "Hello")]
[TestCase("hello world", "c", "HelloWorld")]
public void TryFormat_ValidFormats_ReturnsExpectedResult(string input, string format, string expected)
{
var sut = new StringFormatter();
var result = sut.TryFormat(input, format, out var formatted);
result.ShouldBeTrue();
formatted.ShouldBe(expected);
}

[TestCase("", "s")]
[TestCase("", "u")]
[TestCase("", "l")]
[TestCase("", "t")]
[TestCase("", "c")]
public void TryFormat_EmptyStringWithValidFormat_ReturnsEmpty(string input, string format)
{
var sut = new StringFormatter();
var result = sut.TryFormat(input, format, out var formatted);
result.ShouldBeTrue();
formatted.ShouldBeEmpty();
}

[TestCase("test", "")]
[TestCase("test", " ")]
[TestCase("test", "invalid")]
[TestCase("invalid", "")]
[TestCase("invalid", " ")]
[TestCase("invalid", "invalid")]
public void TryFormat_ValidStringWithInvalidFormat_ReturnsFalse(string input, string format)
{
var sut = new StringFormatter();
var result = sut.TryFormat(input, format, out var formatted);
result.ShouldBeFalse();
formatted.ShouldBeEmpty();
}

[TestCase("", "")]
[TestCase("", " ")]
[TestCase("", "invalid")]
[TestCase(" ", "")]
[TestCase(" ", " ")]
[TestCase(" ", "invalid")]
public void TryFormat_EmptyOrWhitespaceStringWithInvalidFormat_ReturnsTrue(string input, string format)
{
var sut = new StringFormatter();
var result = sut.TryFormat(input, format, out var formatted);
result.ShouldBeTrue();
formatted.ShouldBeEmpty();
}
}
60 changes: 60 additions & 0 deletions src/GitVersion.Core.Tests/Helpers/EdgeCaseTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using GitVersion.Core.Tests.Extensions;
using GitVersion.Helpers;

namespace GitVersion.Tests.Helpers;

public partial class InputSanitizerTests
{
[TestFixture]
public class EdgeCaseTests : InputSanitizerTests
{
[TestCase(49)]
[TestCase(50)]
public void SanitizeFormat_WithBoundaryLengths_ReturnsInput(int length)
{
var input = new string('x', length);
new InputSanitizer().SanitizeFormat(input).ShouldBe(input);
}

[TestCase(199)]
[TestCase(200)]
public void SanitizeEnvVarName_WithBoundaryLengths_ReturnsInput(int length)
{
var input = new string('A', length);
new InputSanitizer().SanitizeEnvVarName(input).ShouldBe(input);
}

[TestCase(99)]
[TestCase(100)]
public void SanitizeMemberName_WithBoundaryLengths_ReturnsInput(int length)
{
var input = new string('A', length);
new InputSanitizer().SanitizeMemberName(input).ShouldBe(input);
}

[Test]
public void SanitizeFormat_WithUnicode_ReturnsInput()
{
const string unicodeFormat = "测试format";
new InputSanitizer().SanitizeFormat(unicodeFormat).ShouldBe(unicodeFormat);
}

[Test]
public void SanitizeEnvVarName_WithUnicode_ThrowsArgumentException()
{
const string unicodeEnvVar = "测试_VAR";
Action act = () => new InputSanitizer().SanitizeEnvVarName(unicodeEnvVar);
act.ShouldThrowWithMessage<ArgumentException>(
$"Environment variable name contains disallowed characters: '{unicodeEnvVar}'");
}

[Test]
public void SanitizeMemberName_WithUnicode_ThrowsArgumentException()
{
const string unicodeMember = "测试Member";
Action act = () => new InputSanitizer().SanitizeMemberName(unicodeMember);
act.ShouldThrowWithMessage<ArgumentException>(
$"Member name contains disallowed characters: '{unicodeMember}'");
}
}
}
82 changes: 82 additions & 0 deletions src/GitVersion.Core.Tests/Helpers/InputSanitizerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using GitVersion.Core.Tests.Extensions;
using GitVersion.Helpers;

namespace GitVersion.Tests.Helpers;

[TestFixture]
public partial class InputSanitizerTests
{
[TestFixture]
public class SanitizeFormatTests : InputSanitizerTests
{
[Test]
public void SanitizeFormat_WithValidFormat_ReturnsInput()
{
var sut = new InputSanitizer();
const string validFormat = "yyyy-MM-dd";
sut.SanitizeFormat(validFormat).ShouldBe(validFormat);
}

[TestCase("")]
[TestCase(" ")]
[TestCase("\t")]
public void SanitizeFormat_WithEmptyOrWhitespace_ThrowsFormatException(string invalidFormat)
{
var sut = new InputSanitizer();
Action act = () => sut.SanitizeFormat(invalidFormat);
act.ShouldThrowWithMessage<FormatException>("Format string cannot be empty.");
}

[Test]
public void SanitizeFormat_WithTooLongFormat_ThrowsFormatException()
{
var sut = new InputSanitizer();
var longFormat = new string('x', 51);
Action act = () => sut.SanitizeFormat(longFormat);
act.ShouldThrowWithMessage<FormatException>("Format string too long: 'xxxxxxxxxxxxxxxxxxxx...'");
}

[Test]
public void SanitizeFormat_WithMaxValidLength_ReturnsInput()
{
var sut = new InputSanitizer();
var maxLengthFormat = new string('x', 50);
sut.SanitizeFormat(maxLengthFormat).ShouldBe(maxLengthFormat);
}

[TestCase("\r", TestName = "SanitizeFormat_ControlChar_CR")]
[TestCase("\n", TestName = "SanitizeFormat_ControlChar_LF")]
[TestCase("\0", TestName = "SanitizeFormat_ControlChar_Null")]
[TestCase("\x01", TestName = "SanitizeFormat_ControlChar_0x01")]
[TestCase("\x1F", TestName = "SanitizeFormat_ControlChar_0x1F")]
public void SanitizeFormat_WithControlCharacters_ThrowsFormatException(string controlChar)
{
var sut = new InputSanitizer();
var formatWithControl = $"valid{controlChar}format";
Action act = () => sut.SanitizeFormat(formatWithControl);
act.ShouldThrowWithMessage<FormatException>("Format string contains invalid control characters");
}

[Test]
public void SanitizeFormat_WithTabCharacter_ReturnsInput()
{
var sut = new InputSanitizer();
const string formatWithTab = "format\twith\ttab";
sut.SanitizeFormat(formatWithTab).ShouldBe(formatWithTab);
}

[TestCase("yyyy-MM-dd")]
[TestCase("HH:mm:ss")]
[TestCase("0.00")]
[TestCase("C2")]
[TestCase("X8")]
[TestCase("format with spaces")]
[TestCase("format-with-dashes")]
[TestCase("format_with_underscores")]
public void SanitizeFormat_WithValidFormats_ReturnsInput(string validFormat)
{
var sut = new InputSanitizer();
sut.SanitizeFormat(validFormat).ShouldBe(validFormat);
}
}
}
Loading
Loading