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 1 commit 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/Helpers/DateFormatterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using GitVersion.Helpers;

namespace GitVersion.Tests.Helpers;

[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();
}
}
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}'");
}
}
}
41 changes: 41 additions & 0 deletions src/GitVersion.Core.Tests/Helpers/FormattableFormatterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using GitVersion.Helpers;

namespace GitVersion.Tests.Helpers;

[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);
}
}
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);
}
}
}
38 changes: 38 additions & 0 deletions src/GitVersion.Core.Tests/Helpers/NumericFormatterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using GitVersion.Helpers;

namespace GitVersion.Tests.Helpers;

[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();
}
}
67 changes: 67 additions & 0 deletions src/GitVersion.Core.Tests/Helpers/SanitizeEnvVarNameTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using GitVersion.Core.Tests.Extensions;
using GitVersion.Helpers;

namespace GitVersion.Tests.Helpers;

public partial class InputSanitizerTests
{
[TestFixture]
public class SanitizeEnvVarNameTests : InputSanitizerTests
{
[Test]
public void SanitizeEnvVarName_WithValidName_ReturnsInput()
{
var sut = new InputSanitizer();
const string validName = "VALID_ENV_VAR";
sut.SanitizeEnvVarName(validName).ShouldBe(validName);
}

[TestCase("")]
[TestCase(" ")]
[TestCase("\t")]
public void SanitizeEnvVarName_WithEmptyOrWhitespace_ThrowsArgumentException(string invalidName)
{
var sut = new InputSanitizer();
Action act = () => sut.SanitizeEnvVarName(invalidName);
act.ShouldThrowWithMessage<ArgumentException>("Environment variable name cannot be null or empty.");
}

[Test]
public void SanitizeEnvVarName_WithTooLongName_ThrowsArgumentException()
{
var sut = new InputSanitizer();
var longName = new string('A', 201);
Action act = () => sut.SanitizeEnvVarName(longName);
act.ShouldThrowWithMessage<ArgumentException>("Environment variable name too long: 'AAAAAAAAAAAAAAAAAAAA...'");
}

[Test]
public void SanitizeEnvVarName_WithMaxValidLength_ReturnsInput()
{
var sut = new InputSanitizer();
var maxLengthName = new string('A', 200);
sut.SanitizeEnvVarName(maxLengthName).ShouldBe(maxLengthName);
}

[Test]
public void SanitizeEnvVarName_WithInvalidCharacters_ThrowsArgumentException()
{
var sut = new InputSanitizer();
const string invalidName = "INVALID@NAME";
Action act = () => sut.SanitizeEnvVarName(invalidName);
act.ShouldThrowWithMessage<ArgumentException>("Environment variable name contains disallowed characters: 'INVALID@NAME'");
}

[TestCase("PATH")]
[TestCase("HOME")]
[TestCase("USER_NAME")]
[TestCase("MY_VAR_123")]
[TestCase("_PRIVATE_VAR")]
[TestCase("VAR123")]
public void SanitizeEnvVarName_WithValidNames_ReturnsInput(string validName)
{
var sut = new InputSanitizer();
sut.SanitizeEnvVarName(validName).ShouldBe(validName);
}
}
}
Loading