8 Commits
v1.3.0 ... main

Author SHA1 Message Date
d11c74e5d6 setup multiple dotnet versions
All checks were successful
.NET Test / .NET tests (push) Successful in 2m44s
.NET Publish / publish (push) Successful in 51s
2025-11-11 21:53:10 +04:00
85721b9769 added net10.0 support
Some checks failed
.NET Test / .NET tests (push) Failing after 1m0s
2025-11-11 21:48:35 +04:00
f7484b35e2 test pipeline tweaks
Some checks failed
.NET Test / .NET 8.0 (push) Failing after 11m49s
.NET Test / .NET 9.0 (push) Successful in 12m3s
2025-11-11 19:11:12 +04:00
a490a9b328 fix dotnet build command
Some checks failed
.NET Test / .NET 8.0 (push) Failing after 1m13s
.NET Test / .NET 9.0 (push) Failing after 1m20s
2025-11-11 19:04:29 +04:00
034a88ba8f pipeline fix
Some checks failed
.NET Test / .NET 9.0 (push) Failing after 42s
.NET Test / .NET 8.0 (push) Has been cancelled
2025-11-11 19:01:52 +04:00
e28fc62b31 switch from FluentAssertions to Shouldly
Some checks failed
.NET Test / test (8.x) (push) Failing after 1m21s
.NET Test / test (9.x) (push) Failing after 1m25s
2025-11-11 17:57:10 +04:00
312219d42f minor refactoring and tests
All checks were successful
.NET Test / test (push) Successful in 45s
.NET Publish / publish (push) Successful in 42s
2025-08-06 22:02:38 +04:00
7eb3008738 guid generation fix
All checks were successful
.NET Test / test (push) Successful in 1m2s
2025-08-06 21:59:44 +04:00
19 changed files with 375 additions and 197 deletions

View File

@@ -10,13 +10,16 @@ jobs:
publish:
runs-on: ubuntu-latest
env:
DOTNET_CLI_TELEMETRY_OPTOUT: true
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- name: Setup .NET
uses: https://github.com/actions/setup-dotnet@v3
uses: https://github.com/actions/setup-dotnet@v4
with:
dotnet-version: 9.x
dotnet-version: 10.x
- name: Restore dependencies
run: dotnet restore Core/Core.csproj

View File

@@ -14,28 +14,47 @@ on:
jobs:
test:
runs-on: ubuntu-latest
name: .NET tests
env:
DOTNET_CLI_TELEMETRY_OPTOUT: true
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- name: Setup .NET
uses: https://github.com/actions/setup-dotnet@v3
uses: https://github.com/actions/setup-dotnet@v4
with:
dotnet-version: 9.x
dotnet-version: |
8.0.x
9.0.x
10.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Build .NET 10.0
run: dotnet build --no-restore --framework net10.0 --configuration Release ./Core.Tests/Core.Tests.csproj
- name: Test
run: dotnet test --no-build --verbosity normal --logger trx --results-directory "TestResults-9.x"
- name: Build .NET 9.0
run: dotnet build --no-restore --framework net9.0 --configuration Release ./Core.Tests/Core.Tests.csproj
- name: Build .NET 8.0
run: dotnet build --no-restore --framework net8.0 --configuration Release ./Core.Tests/Core.Tests.csproj
- name: Test .NET 10.0
run: dotnet run --no-build --framework net10.0 --configuration Release --project ./Core.Tests/Core.Tests.csproj -- -trx TestResults/results-net10.trx
- name: Test .NET 9.0
run: dotnet run --no-build --framework net9.0 --configuration Release --project ./Core.Tests/Core.Tests.csproj -- -trx TestResults/results-net9.trx
- name: Test .NET 8.0
run: dotnet run --no-build --framework net8.0 --configuration Release --project ./Core.Tests/Core.Tests.csproj -- -trx TestResults/results-net8.trx
- name: Upload dotnet test results
uses: actions/upload-artifact@v3
with:
name: dotnet-results-9.x
path: TestResults-9.x
name: test-results
path: TestResults
if: ${{ always() }}
retention-days: 30

View File

@@ -1,4 +1,5 @@
{
"dotnet.defaultSolution": "JustDotNet.Core.sln",
"dotnetAcquisitionExtension.enableTelemetry": false
"dotnetAcquisitionExtension.enableTelemetry": false,
"dotnet.testWindow.useTestingPlatformProtocol": true
}

View File

@@ -19,7 +19,7 @@ public class Decode
var resultString = Base32.Encode(testBytes);
var resultBytes = Base32.Decode(resultString);
resultBytes.Should().BeEquivalentTo(testBytes);
resultBytes.ShouldBeEquivalentTo(testBytes);
}
}
@@ -34,7 +34,7 @@ public class Decode
public void WhenCalledWithValidString_ShouldReturnValidByteArray(string str, byte[] expected)
{
var actualBytesArray = Base32.Decode(str);
actualBytesArray.Should().Equal(expected);
actualBytesArray.ShouldBe(expected);
}
[Theory]
@@ -44,7 +44,7 @@ public class Decode
public void WhenCalledWithValidStringThatEndsWithPaddingSign_ShouldReturnValidByteArray(string testString, byte[] expected)
{
var actualBytesArray = Base32.Decode(testString);
actualBytesArray.Should().Equal(expected);
actualBytesArray.ShouldBe(expected);
}
[Theory]
@@ -54,7 +54,7 @@ public class Decode
public void WhenCalledWithValidStringWithoutPaddingSign_ShouldReturnValidByteArray(string testString, byte[] expected)
{
var actualBytesArray = Base32.Decode(testString);
actualBytesArray.Should().Equal(expected);
actualBytesArray.ShouldBe(expected);
}
[Theory]
@@ -66,7 +66,7 @@ public class Decode
public void WhenCalledWithNotValidString_ShouldThrowFormatException(string testString)
{
Action action = () => Base32.Decode(testString);
action.Should().Throw<FormatException>();
action.ShouldThrow<FormatException>();
}
[Theory]
@@ -74,6 +74,6 @@ public class Decode
[InlineData("")]
public void WhenCalledWithNullString_ShouldReturnEmptyArray(string? testString)
{
Base32.Decode(testString).Should().BeEmpty();
Base32.Decode(testString).ShouldBeEmpty();
}
}

View File

@@ -31,7 +31,7 @@ public class Encode
var resultBytes = Base32.Decode(testString);
var resultString = Base32.Encode(resultBytes);
resultString.Should().Be(testString);
resultString.ShouldBe(testString);
}
[Theory]
@@ -47,7 +47,7 @@ public class Encode
public void WhenCalledWithNotEmptyByteArray_ShouldReturnValidString(string expected, byte[] testArray)
{
var str = Base32.Encode(testArray);
str.Should().Be(expected);
str.ShouldBe(expected);
}
[Theory]
@@ -56,7 +56,7 @@ public class Encode
public void WhenCalledWithEmptyByteArray_ShouldReturnEmptyString(byte[]? testArray)
{
var actualBase32 = Base32.Encode(testArray);
actualBase32.Should().Be(string.Empty);
actualBase32.ShouldBe(string.Empty);
}
[Theory]
@@ -68,7 +68,7 @@ public class Encode
var charsWritten = Base32.Encode(testArray, output);
charsWritten.Should().Be(0);
output.Should().Equal(['1', '2', '3', '4']);
charsWritten.ShouldBe(0);
output.ShouldBe(['1', '2', '3', '4']);
}
}

View File

@@ -7,7 +7,7 @@ public class Decode
[InlineData(554121)]
[InlineData(100454567)]
[InlineData(3210589)]
public void WhenEncodedToString_ShouldBeDecodedToTheSameByteArray(int seed)
public void WhenBytesEncodedToString_ShouldBeDecodedToTheSameByteArray(int seed)
{
var rng = new Random(seed);
@@ -19,10 +19,47 @@ public class Decode
var resultString = Base64Url.Encode(testBytes);
var resultBytes = Base64Url.Decode(resultString);
resultBytes.Should().BeEquivalentTo(testBytes);
resultBytes.ShouldBeEquivalentTo(testBytes);
}
}
[Theory]
[InlineData(72121)]
[InlineData(554121)]
[InlineData(100454567)]
[InlineData(3210589)]
public void WhenLongEncodedToString_ShouldBeDecodedToTheSameLongArray(int seed)
{
var rng = new Random(seed);
for (int i = 1; i <= 512; i++)
{
var testLong = rng.NextInt64();
var resultString = Base64Url.Encode(testLong);
var resultLong = Base64Url.DecodeLong(resultString);
resultLong.ShouldBe(testLong);
}
}
[Theory]
[InlineData("RgGxr0_n1ZI", -7866126844696657594L)]
[InlineData("sxAPfpKB5kY", 5108913293478531251L)]
[InlineData("lO4_uitvLCg", 2894810894091415188L)]
[InlineData("awxjIqZWz10", 6759716837247880299L)]
[InlineData("VjNe72vug4U", -8825948697371200682L)]
[InlineData("AAAAAAAAAAA", 0L)]
[InlineData("__________8", -1L)]
[InlineData("AQAAAAAAAAA", 1L)]
[InlineData("CgAAAAAAAAA", 10L)]
[InlineData("6AMAAAAAAAA", 1000L)]
public void WhenCalled_ShouldReturnValidLong(string testString, long expected)
{
var result = Base64Url.DecodeLong(testString);
result.ShouldBe(expected);
}
[Theory]
[InlineData("5QrdUxDUVkCAEGw8pvLsEw", "53dd0ae5-d410-4056-8010-6c3ca6f2ec13")]
[InlineData("6nE2uKQ4_0ar9kpmybgkdw", "b83671ea-38a4-46ff-abf6-4a66c9b82477")]
@@ -33,7 +70,7 @@ public class Decode
{
var result = Base64Url.DecodeGuid(testString);
var expected = Guid.Parse(expectedStr);
result.Should().Be(expected);
result.ShouldBe(expected);
}
[Theory]
@@ -48,7 +85,7 @@ public class Decode
public void WhenCalled_ShouldReturnValidBytes(string testString, byte[] expected)
{
var result = Base64Url.Decode(testString);
result.Should().BeEquivalentTo(expected);
result.ShouldBeEquivalentTo(expected);
}
[Theory]
@@ -60,7 +97,7 @@ public class Decode
public void WhenCalledWithInvalidString_ShouldThrowFormatException(string testString)
{
Action action = () => Base64Url.Decode(testString);
action.Should().Throw<FormatException>();
action.ShouldThrow<FormatException>();
}
[Theory]
@@ -72,14 +109,27 @@ public class Decode
public void WhenCalledWithInvalidGuidString_ShouldThrowFormatException(string testString)
{
Action action = () => Base64Url.DecodeGuid(testString);
action.Should().Throw<FormatException>();
action.ShouldThrow<FormatException>();
}
[Theory]
[InlineData("RgG&r0_n1ZI")]
[InlineData("sxA fpKB5kY")]
[InlineData("lO4_uitvL)g")]
[InlineData("awxjIqZ^z10")]
[InlineData("VjNe7!vug4U")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1806:Do not ignore method results", Justification = "Test case")]
public void WhenCalledWithInvalidLongString_ShouldThrowFormatException(string testString)
{
Action action = () => Base64Url.DecodeLong(testString);
action.ShouldThrow<FormatException>();
}
[Theory]
[InlineData(null)]
[InlineData("")]
public void WhenCalledWithNullString_ShouldReturnEmptyArray(string testString)
public void WhenCalledWithNullString_ShouldReturnEmptyArray(string? testString)
{
Base64Url.Decode(testString).Should().BeEmpty();
Base64Url.Decode(testString).ShouldBeEmpty();
}
}

View File

@@ -12,7 +12,24 @@ public class Encode
{
var testGuid = Guid.Parse(testGuidString);
var result = Base64Url.Encode(testGuid);
result.Should().Be(expected);
result.ShouldBe(expected);
}
[Theory]
[InlineData("RgGxr0_n1ZI", -7866126844696657594L)]
[InlineData("sxAPfpKB5kY", 5108913293478531251L)]
[InlineData("lO4_uitvLCg", 2894810894091415188L)]
[InlineData("awxjIqZWz10", 6759716837247880299L)]
[InlineData("VjNe72vug4U", -8825948697371200682L)]
[InlineData("AAAAAAAAAAA", 0L)]
[InlineData("__________8", -1L)]
[InlineData("AQAAAAAAAAA", 1L)]
[InlineData("CgAAAAAAAAA", 10L)]
[InlineData("6AMAAAAAAAA", 1000L)]
public void WhenCalledWithLong_ShouldReturnValidString(string expected, long testLong)
{
var result = Base64Url.Encode(testLong);
result.ShouldBe(expected);
}
[Theory]
@@ -27,28 +44,28 @@ public class Encode
public void WhenCalled_ShouldReturnValidString(string expected, byte[] testBytes)
{
var result = Base64Url.Encode(testBytes);
result.Should().Be(expected);
result.ShouldBe(expected);
}
[Theory]
[InlineData(new byte[] { })]
[InlineData(null)]
public void WhenCalledWithEmptyByteArray_ShouldReturnEmptyString(byte[] testArray)
public void WhenCalledWithEmptyByteArray_ShouldReturnEmptyString(byte[]? testArray)
{
var actualBase32 = Base64Url.Encode(testArray);
actualBase32.Should().Be(string.Empty);
actualBase32.ShouldBe(string.Empty);
}
[Theory]
[InlineData(new byte[] { })]
[InlineData(null)]
public void WhenCalledWithEmptyByteArray_ShouldReturnZeroAndNotChangeOutput(byte[] testArray)
public void WhenCalledWithEmptyByteArray_ShouldReturnZeroAndNotChangeOutput(byte[]? testArray)
{
char[] output = ['1', '2', '3', '4'];
var charsWritten = Base64Url.Encode(testArray, output);
charsWritten.Should().Be(0);
output.Should().Equal(['1', '2', '3', '4']);
charsWritten.ShouldBe(0);
output.ShouldBe((char[])['1', '2', '3', '4']);
}
}

View File

@@ -1,10 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
<AssemblyName>Just.Core.Tests</AssemblyName>
<RootNamespace>Just.Core.Tests</RootNamespace>
@@ -14,13 +15,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="xunit.v3" Version="3.2.0" />
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>

View File

@@ -1,2 +1,2 @@
global using Xunit;
global using FluentAssertions;
global using Shouldly;

View File

@@ -2,15 +2,43 @@ namespace Just.Core.Tests.GuidV8Tests;
public class NewGuid
{
[Theory]
[InlineData(RngEntropy.Weak)]
[InlineData(RngEntropy.Strong)]
public void Version_And_Variant_Should_Be_Correct(RngEntropy entropy)
{
var rng = new Random(25);
var referenceTime = new DateTime(2020, 05, 17, 15, 36, 13, 771, DateTimeKind.Utc);
for (int i = 0; i < 2000; i++)
{
var timestamp = referenceTime.AddSeconds(rng.Next());
var result = GuidV8.NewGuid(timestamp, entropy);
#if NET9_0_OR_GREATER
result.Version.ShouldBe(8);
(result.Variant & 0b1100).ShouldBe(0b1000);
#else
var bytes = result.ToByteArray();
// Check version (bits 4-7 of the 7th byte)
var version = (bytes[7] >> 4) & 0x0F;
version.ShouldBe(8); // UUID version 8
// Check variant (bits 6-7 of the 8th byte)
var variant = bytes[8] >> 6;
variant.ShouldBe(0b10); // Standard UUID variant
#endif
}
}
[Theory]
[InlineData(RngEntropy.Weak,
-25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000,
-25000000, -20000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000,
-2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000,
2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 250000000)]
2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 20000000, 50000000, 100000000)]
[InlineData(RngEntropy.Strong,
-25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000,
-25000000, -20000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000,
-2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000,
2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 250000000)]
2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 20000000, 50000000, 100000000)]
[InlineData(RngEntropy.Weak,
-9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449,
-8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855,
@@ -34,27 +62,27 @@ public class NewGuid
6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099,
8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)]
[InlineData(RngEntropy.Weak,
5635912, 6673780, 17277183, 17512959, 19098799, 21672621, 30581958, 30824885, 31874213, 35192781,
36337094, 37752116, 38387215, 39154682, 40525427, 52288093, 55218356, 59065156, 65231785, 75430932,
76289058, 79078058, 85770685, 85925884, 94726743, 94864163, 95781967, 96150006, 96482085, 102570414,
107768232, 110571078, 110680108, 117974892, 119800380, 126381415, 135895862, 140034471, 149039187, 150906974,
156853001, 160514433, 166446323, 170148965, 171759448, 176494242, 184537553, 188558155, 197194403, 197615804,
201195323, 202294490, 203040975, 203331457, 205016944, 213460258, 217072025, 217185345, 231344025, 232390198,
235053215, 240175073, 245030721, 252275255, 252310334, 277070940, 277359970, 280624756, 288601124, 292427106,
292563035, 299285016, 303834917, 310357836, 315078337, 316367236, 318311758, 318873972, 319675272, 321784171,
324204294, 327667283, 330287252, 338438172, 349863360, 360777768, 366398711, 368637150, 368776734, 371900343,
379094084, 379818879, 381448333, 381814627, 382393101, 382483709, 385600870, 389455134, 396115960, 399364095)]
10024, 28660, 289641, 356015, 443164, 478759, 599586, 705860, 791271, 876512,
884503, 894899, 898584, 980136, 1007927, 1680328, 1690193, 1804615, 1847117, 2005534,
2106684, 2111936, 2252935, 2271396, 2298685, 2385409, 2414094, 2451706, 2549138, 2605538,
2864629, 2923476, 3004288, 3182875, 3266693, 3379019, 3542110, 3851467, 3871420, 4035463,
4316004, 4726381, 4814068, 4902666, 4979292, 4993443, 5117765, 5240585, 5249671, 5319528,
5387595, 5434544, 5504506, 5531264, 5546173, 5780381, 5889341, 6066328, 6167883, 6185073,
6299021, 6412136, 6621498, 6666562, 6741169, 6870279, 6949855, 6965427, 7153467, 7184150,
7300456, 7311381, 7413697, 7505235, 7802811, 7979134, 8053665, 8177676, 8260284, 8260773,
8269944, 8406526, 8442932, 8475162, 8555250, 8853347, 8861733, 8892200, 9069869, 9117839,
9225445, 9245837, 9378644, 9497874, 9553625, 9650968, 9704053, 9713592, 9715054, 9735988)]
[InlineData(RngEntropy.Strong,
5635912, 6673780, 17277183, 17512959, 19098799, 21672621, 30581958, 30824885, 31874213, 35192781,
36337094, 37752116, 38387215, 39154682, 40525427, 52288093, 55218356, 59065156, 65231785, 75430932,
76289058, 79078058, 85770685, 85925884, 94726743, 94864163, 95781967, 96150006, 96482085, 102570414,
107768232, 110571078, 110680108, 117974892, 119800380, 126381415, 135895862, 140034471, 149039187, 150906974,
156853001, 160514433, 166446323, 170148965, 171759448, 176494242, 184537553, 188558155, 197194403, 197615804,
201195323, 202294490, 203040975, 203331457, 205016944, 213460258, 217072025, 217185345, 231344025, 232390198,
235053215, 240175073, 245030721, 252275255, 252310334, 277070940, 277359970, 280624756, 288601124, 292427106,
292563035, 299285016, 303834917, 310357836, 315078337, 316367236, 318311758, 318873972, 319675272, 321784171,
324204294, 327667283, 330287252, 338438172, 349863360, 360777768, 366398711, 368637150, 368776734, 371900343,
379094084, 379818879, 381448333, 381814627, 382393101, 382483709, 385600870, 389455134, 396115960, 399364095)]
10024, 28660, 289641, 356015, 443164, 478759, 599586, 705860, 791271, 876512,
884503, 894899, 898584, 980136, 1007927, 1680328, 1690193, 1804615, 1847117, 2005534,
2106684, 2111936, 2252935, 2271396, 2298685, 2385409, 2414094, 2451706, 2549138, 2605538,
2864629, 2923476, 3004288, 3182875, 3266693, 3379019, 3542110, 3851467, 3871420, 4035463,
4316004, 4726381, 4814068, 4902666, 4979292, 4993443, 5117765, 5240585, 5249671, 5319528,
5387595, 5434544, 5504506, 5531264, 5546173, 5780381, 5889341, 6066328, 6167883, 6185073,
6299021, 6412136, 6621498, 6666562, 6741169, 6870279, 6949855, 6965427, 7153467, 7184150,
7300456, 7311381, 7413697, 7505235, 7802811, 7979134, 8053665, 8177676, 8260284, 8260773,
8269944, 8406526, 8442932, 8475162, 8555250, 8853347, 8861733, 8892200, 9069869, 9117839,
9225445, 9245837, 9378644, 9497874, 9553625, 9650968, 9704053, 9713592, 9715054, 9735988)]
public void Guids_Differing_By_Minutes_Should_Be_Sortable(RngEntropy entropy, params int[] seconds)
{
var rng = new Random(25);
@@ -69,22 +97,22 @@ public class NewGuid
var sut = expected.Values.ToArray();
rng.Shuffle(sut);
sut.Order().Should().Equal(expected.Select(x => x.Value));
sut.OrderBy(x => x.ToString()).Should().Equal(expected.Select(x => x.Value));
sut.Order().ShouldBe(expected.Select(x => x.Value));
sut.OrderBy(x => x.ToString()).ShouldBe(expected.Select(x => x.Value));
sut.OrderDescending().Should().Equal(expected.Reverse().Select(x => x.Value));
sut.OrderByDescending(x => x.ToString()).Should().Equal(expected.Reverse().Select(x => x.Value));
sut.OrderDescending().ShouldBe(expected.Reverse().Select(x => x.Value));
sut.OrderByDescending(x => x.ToString()).ShouldBe(expected.Reverse().Select(x => x.Value));
}
[Theory]
[InlineData(RngEntropy.Weak,
-250000000, -25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000,
-100000000, -25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000,
-2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000,
2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 2100000000)]
2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 200000000)]
[InlineData(RngEntropy.Strong,
-250000000, -25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000,
-100000000, -25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000,
-2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000,
2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 2100000000)]
2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 200000000)]
[InlineData(RngEntropy.Weak,
-9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449,
-8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855,
@@ -143,11 +171,11 @@ public class NewGuid
var sut = expected.Values.ToArray();
rng.Shuffle(sut);
sut.Order().Should().Equal(expected.Select(x => x.Value));
sut.OrderBy(x => x.ToString()).Should().Equal(expected.Select(x => x.Value));
sut.Order().ShouldBe(expected.Select(x => x.Value));
sut.OrderBy(x => x.ToString()).ShouldBe(expected.Select(x => x.Value));
sut.OrderDescending().Should().Equal(expected.Reverse().Select(x => x.Value));
sut.OrderByDescending(x => x.ToString()).Should().Equal(expected.Reverse().Select(x => x.Value));
sut.OrderDescending().ShouldBe(expected.Reverse().Select(x => x.Value));
sut.OrderByDescending(x => x.ToString()).ShouldBe(expected.Reverse().Select(x => x.Value));
}
[Theory]
@@ -217,10 +245,10 @@ public class NewGuid
var sut = expected.Values.ToArray();
rng.Shuffle(sut);
sut.Order().Should().Equal(expected.Select(x => x.Value));
sut.OrderBy(x => x.ToString()).Should().Equal(expected.Select(x => x.Value));
sut.Order().ShouldBe(expected.Select(x => x.Value));
sut.OrderBy(x => x.ToString()).ShouldBe(expected.Select(x => x.Value));
sut.OrderDescending().Should().Equal(expected.Reverse().Select(x => x.Value));
sut.OrderByDescending(x => x.ToString()).Should().Equal(expected.Reverse().Select(x => x.Value));
sut.OrderDescending().ShouldBe(expected.Reverse().Select(x => x.Value));
sut.OrderByDescending(x => x.ToString()).ShouldBe(expected.Reverse().Select(x => x.Value));
}
}

View File

@@ -1,3 +1,4 @@
namespace Just.Core.Tests.SeqIdTests;
public class NextId
@@ -25,9 +26,9 @@ public class NextId
long sequencePart = (id >> SeqShift) & SeqMask;
long randomPart = id & RandMask;
timestampPart.Should().Be(500);
sequencePart.Should().Be(0);
randomPart.Should().BeInRange(0, RandMask);
timestampPart.ShouldBe(500);
sequencePart.ShouldBe(0);
randomPart.ShouldBeInRange(0, RandMask);
}
[Fact]
@@ -44,7 +45,7 @@ public class NextId
long sequence1 = (id1 >> SeqShift) & SeqMask;
long sequence2 = (id2 >> SeqShift) & SeqMask;
sequence2.Should().Be(sequence1 + 1);
sequence2.ShouldBe(sequence1 + 1);
}
[Fact]
@@ -61,7 +62,7 @@ public class NextId
// Assert
long sequence = (id >> SeqShift) & SeqMask;
sequence.Should().Be(0);
sequence.ShouldBe(0);
}
[Fact]
@@ -74,7 +75,7 @@ public class NextId
// Act & Assert
_seqId.Next(time1); // First call sets last timestamp
Action act = () => _seqId.Next(time2);
act.Should().Throw<InvalidOperationException>()
act.ShouldThrow<InvalidOperationException>()
.WithMessage("Refused to create new SeqId. Last timestamp is in the future.");
}
@@ -90,7 +91,7 @@ public class NextId
_seqId.Next(time); // Exhauste sequence
}
Action act = () => _seqId.Next(time);
act.Should().Throw<IndexOutOfRangeException>()
act.ShouldThrow<IndexOutOfRangeException>()
.WithMessage("Refused to create new SeqId. Sequence exhausted.");
}
@@ -105,7 +106,7 @@ public class NextId
// Assert
long randomPart = id & RandMask;
randomPart.Should().BeInRange(0, RandMask);
randomPart.ShouldBeInRange(0, RandMask);
}
[Fact]
@@ -119,7 +120,7 @@ public class NextId
// Assert
long randomPart = id & RandMask;
randomPart.Should().BeInRange(0, RandMask);
randomPart.ShouldBeInRange(0, RandMask);
}
[Fact]
@@ -127,14 +128,14 @@ public class NextId
{
// Arrange
var now = DateTime.UtcNow;
var defaultEpoch = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc);
long expectedTimestamp = (long)(now - defaultEpoch).TotalMilliseconds;
var defaultEpoch = SeqId.DefaultEpoch;
long expectedTimestamp = ((long)(now - defaultEpoch).TotalMilliseconds) & TimestampMask; // Mask handles overflow
// Act
long id = SeqId.NextId();
// Assert
long timestampPart = (id >> TimestampShift) & TimestampMask;
timestampPart.Should().BeCloseTo(expectedTimestamp & TimestampMask, 1); // Mask handles overflow
timestampPart.ShouldBeInRange(expectedTimestamp, expectedTimestamp + 1);
}
}

View File

@@ -0,0 +1,11 @@
namespace Just.Core.Tests;
public static class ShouldlyExtensions
{
public static TException WithMessage<TException>(this TException exception, string expectedMessage, string? customMessage = null)
where TException : Exception
{
exception.Message.ShouldBe(expectedMessage, customMessage: customMessage);
return exception;
}
}

View File

@@ -23,7 +23,7 @@ public class Populate
stream.Populate(buffer, offset, length);
buffer.Skip(offset).Take(length).Should().Equal(streamContent.Take(length));
buffer.Skip(offset).Take(length).ShouldBe(streamContent.Take(length));
}
[Theory]
@@ -38,7 +38,7 @@ public class Populate
stream.Populate(buffer);
buffer.Should().Equal(streamContent.Take(bufferSize));
buffer.ShouldBe(streamContent.Take(bufferSize));
}
[Theory]
@@ -53,6 +53,6 @@ public class Populate
Action action = () => stream.Populate(buffer);
action.Should().Throw<EndOfStreamException>();
action.ShouldThrow<EndOfStreamException>();
}
}

View File

@@ -15,7 +15,7 @@ public class PopulateAsync
Func<Task> action = async () => await stream.PopulateAsync(buffer, cts.Token);
cts.Cancel();
await action.Should().ThrowAsync<OperationCanceledException>();
await action.ShouldThrowAsync<OperationCanceledException>();
}
[Theory]
@@ -31,13 +31,14 @@ public class PopulateAsync
[InlineData(5, 5)]
public async Task WhenCalled_ShouldPopulateSpecifiedRange(int offset, int length)
{
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
byte[] streamContent = [0x01, 0x02, 0x03, 0x04, 0x05,];
using var stream = new MemoryStream(streamContent);
var buffer = new byte[10];
await stream.PopulateAsync(buffer, offset, length);
await stream.PopulateAsync(buffer, offset, length, cts.Token);
buffer.Skip(offset).Take(length).Should().Equal(streamContent.Take(length));
buffer.Skip(offset).Take(length).ShouldBe(streamContent.Take(length));
}
[Theory]
@@ -47,12 +48,13 @@ public class PopulateAsync
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, }, 5)]
public async Task WhenStreamContainsSameOrGreaterAmmountOfItems_ShouldPopulateBuffer(byte[] streamContent, int bufferSize)
{
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
using var stream = new MemoryStream(streamContent);
var buffer = new byte[bufferSize];
await stream.PopulateAsync(buffer);
await stream.PopulateAsync(buffer, cts.Token);
buffer.Should().Equal(streamContent.Take(bufferSize));
buffer.ShouldBe(streamContent.Take(bufferSize));
}
[Theory]
@@ -67,6 +69,6 @@ public class PopulateAsync
Func<Task> action = async () => await stream.PopulateAsync(buffer);
await action.Should().ThrowAsync<EndOfStreamException>();
await action.ShouldThrowAsync<EndOfStreamException>();
}
}

View File

@@ -16,9 +16,9 @@ public static class Base32
? stackalloc char[outLength]
: new char[outLength];
_ = Encode(input, output);
var size = Encode(input, output);
return new string(output);
return new string(output[..size]);
}
[Pure]
@@ -26,20 +26,31 @@ public static class Base32
{
if (input.IsEmpty) return 0;
int outputLength = 8 * ((input.Length + 4) / 5);
if (output.Length < outputLength)
{
throw new ArgumentException("Encoded input can not fit in output span.", nameof(output));
}
output = output[..outputLength];
int i = 0;
ReadOnlySpan<char> alphabet = Alphabet;
Span<byte> alphabetKeys = stackalloc byte[8];
for (int offset = 0; offset < input.Length;)
{
int numCharsToOutput = GetNextGroup(input, ref offset, out byte a, out byte b, out byte c, out byte d, out byte e, out byte f, out byte g, out byte h);
alphabetKeys.Clear();
int numCharsToOutput = GetNextGroup(input, ref offset, alphabetKeys);
output[i++] = (numCharsToOutput > 0) ? alphabet[a] : Padding;
output[i++] = (numCharsToOutput > 1) ? alphabet[b] : Padding;
output[i++] = (numCharsToOutput > 2) ? alphabet[c] : Padding;
output[i++] = (numCharsToOutput > 3) ? alphabet[d] : Padding;
output[i++] = (numCharsToOutput > 4) ? alphabet[e] : Padding;
output[i++] = (numCharsToOutput > 5) ? alphabet[f] : Padding;
output[i++] = (numCharsToOutput > 6) ? alphabet[g] : Padding;
output[i++] = (numCharsToOutput > 7) ? alphabet[h] : Padding;
output[i++] = (numCharsToOutput > 0) ? alphabet[alphabetKeys[0]] : Padding;
output[i++] = (numCharsToOutput > 1) ? alphabet[alphabetKeys[1]] : Padding;
output[i++] = (numCharsToOutput > 2) ? alphabet[alphabetKeys[2]] : Padding;
output[i++] = (numCharsToOutput > 3) ? alphabet[alphabetKeys[3]] : Padding;
output[i++] = (numCharsToOutput > 4) ? alphabet[alphabetKeys[4]] : Padding;
output[i++] = (numCharsToOutput > 5) ? alphabet[alphabetKeys[5]] : Padding;
output[i++] = (numCharsToOutput > 6) ? alphabet[alphabetKeys[6]] : Padding;
output[i++] = (numCharsToOutput > 7) ? alphabet[alphabetKeys[7]] : Padding;
}
return i;
}
@@ -66,6 +77,11 @@ public static class Base32
input = input.TrimEnd(Padding);
var outputLength = 5 * ((input.Length + 7) / 8);
if (output.Length < outputLength)
{
throw new ArgumentException("Decoded input can not fit in output span.", nameof(output));
}
output = output[..outputLength];
output.Clear();
@@ -119,7 +135,7 @@ public static class Base32
// returns the number of bytes that were output
[Pure]
private static int GetNextGroup(ReadOnlySpan<byte> input, ref int offset, out byte a, out byte b, out byte c, out byte d, out byte e, out byte f, out byte g, out byte h)
private static int GetNextGroup(ReadOnlySpan<byte> input, ref int offset, Span<byte> alphabetKeys)
{
var retVal = (input.Length - offset) switch
{
@@ -135,14 +151,14 @@ public static class Base32
uint b4 = (offset < input.Length) ? input[offset++] : 0U;
uint b5 = (offset < input.Length) ? input[offset++] : 0U;
a = (byte)(b1 >> 3);
b = (byte)(((b1 & 0x07) << 2) | (b2 >> 6));
c = (byte)((b2 >> 1) & 0x1f);
d = (byte)(((b2 & 0x01) << 4) | (b3 >> 4));
e = (byte)(((b3 & 0x0f) << 1) | (b4 >> 7));
f = (byte)((b4 >> 2) & 0x1f);
g = (byte)(((b4 & 0x3) << 3) | (b5 >> 5));
h = (byte)(b5 & 0x1f);
alphabetKeys[0] = (byte)(b1 >> 3);
alphabetKeys[1] = (byte)(((b1 & 0x07) << 2) | (b2 >> 6));
alphabetKeys[2] = (byte)((b2 >> 1) & 0x1f);
alphabetKeys[3] = (byte)(((b2 & 0x01) << 4) | (b3 >> 4));
alphabetKeys[4] = (byte)(((b3 & 0x0f) << 1) | (b4 >> 7));
alphabetKeys[5] = (byte)((b4 >> 2) & 0x1f);
alphabetKeys[6] = (byte)(((b4 & 0x3) << 3) | (b5 >> 5));
alphabetKeys[7] = (byte)(b5 & 0x1f);
return retVal;
}

View File

@@ -1,24 +1,41 @@
using System.Runtime.InteropServices;
namespace Just.Core;
public static class Base64Url
{
private const char Padding = '=';
[Pure] public static long DecodeLong(ReadOnlySpan<char> value)
{
ArgumentOutOfRangeException.ThrowIfNotEqual(value.Length, 11);
Span<byte> longBytes = stackalloc byte[8];
Span<char> chars = stackalloc char[12];
value.CopyTo(chars);
chars[^1] = Padding;
ReplaceNonUrlChars(chars);
if (!Convert.TryFromBase64Chars(chars, longBytes, out int _))
throw new FormatException("Invalid Base64 string.");
return MemoryMarshal.Read<long>(longBytes);
}
[Pure] public static Guid DecodeGuid(ReadOnlySpan<char> value)
{
ArgumentOutOfRangeException.ThrowIfNotEqual(value.Length, 22);
Span<byte> guidBytes = stackalloc byte[16];
Span<char> chars = stackalloc char[24];
value.CopyTo(chars);
for (int i = 0; i < value.Length; i++)
{
switch (value[i])
{
case '-': chars[i] = '+'; continue;
case '_': chars[i] = '/'; continue;
default: continue;
}
}
chars[^2..].Fill('=');
chars[^2..].Fill(Padding);
ReplaceNonUrlChars(chars);
if (!Convert.TryFromBase64Chars(chars, guidBytes, out int _))
throw new FormatException("Invalid Base64 string.");
@@ -28,9 +45,9 @@ public static class Base64Url
[Pure] public static byte[] Decode(ReadOnlySpan<char> input)
{
if (input.IsEmpty) return [];
Span<byte> output = stackalloc byte[3 * ((input.Length + 3) / 4)];
var size = Decode(input, output);
return output[..size].ToArray();
@@ -40,28 +57,33 @@ public static class Base64Url
{
var padding = (4 - (value.Length & 3)) & 3;
var charlen = value.Length + padding;
var outputBytes = charlen / 4;
var outputBytes = 3 * (charlen / 4);
ArgumentOutOfRangeException.ThrowIfLessThan(output.Length, outputBytes);
Span<char> chars = stackalloc char[charlen];
value.CopyTo(chars);
for (int i = 0; i < value.Length; i++)
{
switch (value[i])
{
case '-': chars[i] = '+'; continue;
case '_': chars[i] = '/'; continue;
default: continue;
}
}
chars[^padding..].Fill('=');
chars[^padding..].Fill(Padding);
ReplaceNonUrlChars(chars);
if (!Convert.TryFromBase64Chars(chars, output, out outputBytes))
throw new FormatException("Invalid Base64 string.");
return outputBytes;
}
[Pure] public static string Encode(in long id)
{
Span<byte> longBytes = stackalloc byte[8];
MemoryMarshal.Write(longBytes, id);
Span<char> chars = stackalloc char[12];
Convert.TryToBase64Chars(longBytes, chars, out int _);
ReplaceUrlChars(chars[..^1]);
return new string(chars[..^1]);
}
[Pure] public static string Encode(in Guid id)
{
Span<byte> guidBytes = stackalloc byte[16];
@@ -69,15 +91,7 @@ public static class Base64Url
Span<char> chars = stackalloc char[24];
Convert.TryToBase64Chars(guidBytes, chars, out int _);
for (int i = 0; i < chars.Length - 2; i++)
{
switch (chars[i])
{
case '+': chars[i] = '-'; continue;
case '/': chars[i] = '_'; continue;
default: continue;
}
}
ReplaceUrlChars(chars[..^2]);
return new string(chars[..^2]);
}
@@ -86,7 +100,7 @@ public static class Base64Url
{
if (input.IsEmpty) return string.Empty;
int outLength = 8 * ((input.Length + 5) / 6);
int outLength = 4 * ((input.Length + 2) / 3);
Span<char> output = stackalloc char[outLength];
int strlen = Encode(input, output);
@@ -97,24 +111,47 @@ public static class Base64Url
{
if (input.IsEmpty) return 0;
var charlen = 8 * ((input.Length + 5) / 6);
var charlen = 4 * ((input.Length + 2) / 3);
Span<char> chars = stackalloc char[charlen];
Convert.TryToBase64Chars(input, chars, out int charsWritten);
int i;
for (i = 0; i < charsWritten; i++)
int i = ReplaceUrlChars(chars[..charsWritten]);
chars[..i].CopyTo(output);
return i;
}
private static int ReplaceUrlChars(Span<char> chars)
{
int i = 0;
for (; i < chars.Length; i++)
{
switch (chars[i])
{
case '+': chars[i] = '-'; continue;
case '/': chars[i] = '_'; continue;
case '=': goto exitLoop;
case Padding: goto break_loop;
default: continue;
}
}
exitLoop:
chars[..i].CopyTo(output);
break_loop:
return i;
}
private static int ReplaceNonUrlChars(Span<char> chars)
{
int i = 0;
for (; i < chars.Length; i++)
{
switch (chars[i])
{
case '-': chars[i] = '+'; continue;
case '_': chars[i] = '/'; continue;
case Padding: goto break_loop;
default: continue;
}
}
break_loop:
return i;
}
}

View File

@@ -8,18 +8,12 @@ public class ImmutableSequence<T> :
IReadOnlyList<T>,
IEquatable<ImmutableSequence<T>>
{
private static readonly int InitialHash = typeof(ImmutableSequence<T>).GetHashCode();
private static readonly Func<T?, T?, bool> CompareItem = EqualityComparer<T>.Default.Equals;
private readonly ImmutableList<T> _values;
public ImmutableSequence() => _values = [];
public ImmutableSequence(ImmutableList<T> values) => _values = values;
public ImmutableSequence() : this(ImmutableArray<T>.Empty)
{
}
public ImmutableSequence(IEnumerable<T> values)
{
_values = [..values];
}
public ImmutableSequence(IEnumerable<T> values) => _values = [..values];
public ImmutableSequence(ReadOnlySpan<T> values) : this(ImmutableList.Create(values))
{
}
@@ -37,10 +31,10 @@ public class ImmutableSequence<T> :
}
}
protected virtual ImmutableSequence<T> ConstructNew(ImmutableList<T> values) => new(values);
protected virtual ImmutableSequence<T> ConstructNew(ImmutableList<T> values) => [..values];
public ImmutableSequence<T> Add(T value) => ConstructNew([.._values, value]);
public ImmutableSequence<T> AddFront(T value) => ConstructNew([value, .._values]);
public ImmutableSequence<T> Add(T value) => ConstructNew(_values.Add(value));
public ImmutableSequence<T> AddFront(T value) => ConstructNew(_values.Insert(0, value));
public ImmutableList<T>.Enumerator GetEnumerator() => _values.GetEnumerator();
IEnumerator<T> IEnumerable<T>.GetEnumerator() => ((IEnumerable<T>)_values).GetEnumerator();
@@ -75,7 +69,6 @@ public class ImmutableSequence<T> :
public override int GetHashCode()
{
HashCode hash = new();
hash.Add(InitialHash);
foreach (var value in _values)
{

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Just.Core</AssemblyName>

View File

@@ -1,38 +1,42 @@
using System.Runtime.InteropServices;
using System.Security.Cryptography;
namespace Just.Core;
public static class GuidV8
{
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
private const long TicksPrecision = TimeSpan.TicksPerMillisecond / 10;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Guid NewGuid(RngEntropy entropy = RngEntropy.Strong) => NewGuid(DateTime.UtcNow, entropy);
[Pure]
public static Guid NewGuid(DateTime dateTime, RngEntropy entropy = RngEntropy.Strong)
{
var epoch = dateTime.Subtract(DateTime.UnixEpoch);
var timestamp = epoch.Ticks / (TimeSpan.TicksPerMillisecond / 10);
var timestamp = epoch.Ticks / TicksPrecision;
Span<byte> ts = stackalloc byte[8];
MemoryMarshal.Write(ts, timestamp);
uint tsHigh = (uint)((timestamp >> 16) & 0xFFFFFFFF);
ushort tsLow = (ushort)(timestamp & 0x0000FFFF);
Span<byte> bytes = stackalloc byte[16];
ts[0..2].CopyTo(bytes[4..6]);
ts[2..6].CopyTo(bytes[..4]);
Span<byte> bytes = stackalloc byte[10];
if (entropy == RngEntropy.Strong)
{
RandomNumberGenerator.Fill(bytes[6..]);
RandomNumberGenerator.Fill(bytes);
}
else
{
Random.Shared.NextBytes(bytes[6..]);
Random.Shared.NextBytes(bytes);
}
bytes[7] = (byte)((bytes[7] & 0x0F) | 0x80);
bytes[0] = (byte)((bytes[0] & 0x0F) | 0x80); // Version 8
bytes[2] = (byte)((bytes[2] & 0x1F) | 0x80); // Variant 0b1000
return new Guid(bytes);
ushort version = (ushort)((bytes[0] << 8) | bytes[1]);
return new Guid(
tsHigh,
tsLow,
version,
bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9]);
}
}