Skip to content

Commit b3378af

Browse files
committed
Prep for release
1 parent d1c3a31 commit b3378af

13 files changed

Lines changed: 432 additions & 180 deletions

File tree

CHANGELOG.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Changelog
2+
3+
## 3.0.x - 2026-08-28
4+
5+
### Breaking
6+
- The `System.Random` subclass formerly named `Antithesis.SDK.Random` is now named `Antithesis.SDK.AntithesisRandom`.
7+
This allows us to more closely align with other Antithesis SDKs that include a `Random` class for providing simple
8+
Antithesis sourced entropy (see New section below).
9+
10+
### Bug Fixes
11+
- Fixed the bug truncating strings containing non-ASCII characters when writing to `FFISink`.
12+
13+
### New
14+
- Added `Assert.Raw`, a low-level assertion entry point for third-party frameworks that supply their own catalog metadata.
15+
- Added `Antithesis.SDK.Random` with `GetRandom()` to source entropy directly from the Antithesis platform and
16+
`RandomChoice<T>(IReadOnlyList<T>)` to randomly pick an element of a list.
17+
18+
## 2.0.4 - 2026-04-22
19+
20+
### Breaking
21+
- In order to improve performance, "numeric guidance" Assert methods no longer write when the difference between its two
22+
operands does not change (i.e. they only write the first occurrence of a given difference):
23+
AlwaysGreaterThan(OrEqualTo), AlwaysLessThan(OrEqualTo), SometimesGreaterThan(OrEqualTo), SometimesLessThan(OrEqualTo).
24+
This is only breaking if you rely on ANTITHESIS_SDK_LOCAL_OUTPUT when testing locally.
25+
26+
### Bug Fixes
27+
- None
28+
29+
### New
30+
- None
31+
32+
## 1.0.31 - 2025-06-10
33+
34+
First official release.

README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
# Antithesis .NET SDK
22

3-
This package contains types for .NET applications to use to integrate with [Antithesis](https://antithesis.com).
4-
* The `Assert` class enables defining [test properties](https://antithesis.com/docs/properties_assertions/properties/)
5-
about your program or [workload](https://antithesis.com/docs/test_templates/first_test/).
6-
* The `Lifecycle` class contains methods used to inform Antithesis that particular test phases or milestones have been reached.
7-
* The `Random` class is a subclass of `System.Random` that encapsulates Antithesis's deterministic and reproducible
8-
random number generator.
3+
This package contains types for .NET applications to integrate with [Antithesis](https://antithesis.com).
4+
* The `Assert` class methods define [test properties](https://antithesis.com/docs/introduction/how_antithesis_works/#how-does-antithesis-identify-bugs/)
5+
about your software or [workload](https://antithesis.com/docs/product/writing_tests/test_templates/first_test/).
6+
* The `Random` class methods source entropy directly from the Antithesis platform: raw 64-bit values via `GetRandom()`
7+
and structured list picks via `RandomChoice()`.
8+
* The `AntithesisRandom` class is a subclass of `System.Random` that encapsulates Antithesis's steering choices,
9+
making it easy for .NET code that uses `System.Random` to be included in Antithesis-guided exploration.
10+
* The `Lifecycle` class methods inform the Antithesis environment that particular test phases or milestones have been
11+
reached.
912

1013
For general usage guidance see the [Antithesis .NET SDK Documentation](https://antithesis.com/docs/using_antithesis/sdk/dotnet/).
1114

samples/HelloAntithesis/WebService/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
var app = builder.Build();
1010

11-
var random = Antithesis.SDK.Random.SharedFallbackToSystem;
11+
var random = Antithesis.SDK.AntithesisRandom.SharedFallbackToSystem;
1212

1313
app.MapGet("/", async () =>
1414
{
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
namespace Antithesis.SDK;
2+
3+
using System.Diagnostics;
4+
5+
// Methods adapted or inspired by:
6+
// https://github.com/dotnet/runtime/blob/v9.0.5/src/libraries/System.Private.CoreLib/src/System/Random.ImplBase.cs
7+
// https://github.com/dotnet/runtime/blob/v9.0.5/src/libraries/System.Private.CoreLib/src/System/Random.Xoshiro256StarStarImpl.cs
8+
9+
/// <summary>
10+
/// AntithesisRandom is a subclass of <see cref="System.Random">System.Random</see> that encapsules Antithesis's
11+
/// deterministic and reproducible random number generator. Obtain an instance via
12+
/// <see cref="SharedFallbackToSystem"/>; for raw 64-bit draws, see <see cref="Random.GetRandom"/>.
13+
/// </summary>
14+
/// <remarks>
15+
/// Regarding the methods which have to be overriden with Sample when subclassing System.Random:
16+
/// <a href="https://learn.microsoft.com/en-us/dotnet/api/system.random.sample?view=net-9.0#notes-to-inheritors">Microsoft Learn</a>.
17+
/// We also chose to override NextBytes(Span&lt;Byte&gt;) because we used it for NextBytes(Byte[]) which had to be overridden.
18+
/// </remarks>
19+
public class AntithesisRandom : System.Random
20+
{
21+
// Force the Sink's static constructor to write out SDK info in case this is the first use of the SDK.
22+
static AntithesisRandom() => Sink.Touch();
23+
24+
/// <inheritdoc />
25+
[Obsolete("Please use Antithesis.SDK.AntithesisRandom.SharedFallbackToSystem which explicitly defines its behavior when outside Antithesis.", true)]
26+
new public static System.Random Shared => throw new NotSupportedException();
27+
28+
/// <summary>
29+
/// Returns a Singleton of this class when executing within Antithesis; else, falls back to System.Random.Shared.
30+
/// </summary>
31+
public static System.Random SharedFallbackToSystem { get; } =
32+
FFI.FileExists ? new AntithesisRandom(new FFIRandomUInt64Provider()) : System.Random.Shared;
33+
34+
internal AntithesisRandom(IRandomUInt64Provider randomUInt64) =>
35+
_randomUInt64 = randomUInt64 ?? throw new ArgumentNullException(nameof(randomUInt64));
36+
37+
private readonly IRandomUInt64Provider _randomUInt64;
38+
39+
/// <inheritdoc />
40+
protected override double Sample()
41+
{
42+
// Regarding converting the ulong to double between [0.0, 1.0) :
43+
// https://github.com/dotnet/runtime/blob/cc803458ab4dabf020b462873be5bf56f1640b1e/src/libraries/System.Private.CoreLib/src/System/Random.Xoshiro256StarStarImpl.cs#L182
44+
//
45+
// As described in http://prng.di.unimi.it/:
46+
// "A standard double (64-bit) floating-point number in IEEE floating point format has 52 bits of significand,
47+
// plus an implicit bit at the left of the significand. Thus, the representation can actually store numbers with
48+
// 53 significant binary digits. Because of this fact, in C99 a 64-bit unsigned integer x should be converted to
49+
// a 64-bit double using the expression
50+
// (x >> 11) * 0x1.0p-53"
51+
return (_randomUInt64.Next() >> 11) * (1.0 / (1ul << 53));
52+
}
53+
54+
/// <inheritdoc />
55+
public override int Next()
56+
{
57+
while (true)
58+
{
59+
// Regarding the loop :
60+
// https://github.com/dotnet/runtime/blob/cc803458ab4dabf020b462873be5bf56f1640b1e/src/libraries/System.Private.CoreLib/src/System/Random.Xoshiro256StarStarImpl.cs#L76
61+
//
62+
// Get top 31 bits to get a value in the range [0, int.MaxValue], but try again
63+
// if the value is actually int.MaxValue, as the method is defined to return a value
64+
// in the range [0, int.MaxValue).
65+
ulong result = _randomUInt64.Next() >> 33;
66+
67+
if (result != int.MaxValue)
68+
return (int)result;
69+
}
70+
}
71+
72+
/// <inheritdoc />
73+
public override int Next(int minValue, int maxValue)
74+
{
75+
if (minValue > maxValue)
76+
throw new ArgumentOutOfRangeException(nameof(minValue));
77+
78+
if (minValue == maxValue)
79+
return minValue;
80+
81+
return (int)NextUInt32((uint)(maxValue - minValue)) + minValue;
82+
}
83+
84+
private uint NextUInt32(uint maxValue)
85+
{
86+
// Regarding this algorithm:
87+
// https://github.com/dotnet/runtime/blob/cc803458ab4dabf020b462873be5bf56f1640b1e/src/libraries/System.Private.CoreLib/src/System/Random.ImplBase.cs#L36
88+
//
89+
// NextUInt32/64 algorithms based on https://arxiv.org/pdf/1805.10941.pdf and https://github.com/lemire/fastrange.
90+
91+
ulong randomProduct = (ulong)maxValue * (_randomUInt64.Next() >> 32);
92+
uint lowPart = (uint)randomProduct;
93+
94+
if (lowPart < maxValue)
95+
{
96+
uint remainder = (0u - maxValue) % maxValue;
97+
98+
while (lowPart < remainder)
99+
{
100+
randomProduct = (ulong)maxValue * (_randomUInt64.Next() >> 32);
101+
lowPart = (uint)randomProduct;
102+
}
103+
}
104+
105+
return (uint)(randomProduct >> 32);
106+
}
107+
108+
/// <inheritdoc />
109+
public override void NextBytes(byte[] buffer)
110+
{
111+
if (buffer == null)
112+
throw new ArgumentNullException(nameof(buffer));
113+
114+
NextBytes((Span<byte>)buffer);
115+
}
116+
117+
/// <inheritdoc />
118+
public override void NextBytes(Span<byte> buffer)
119+
{
120+
while (buffer.Length >= sizeof(ulong))
121+
{
122+
ulong random = _randomUInt64.Next();
123+
BitConverter.GetBytes(random).CopyTo(buffer);
124+
125+
buffer = buffer.Slice(sizeof(ulong));
126+
}
127+
128+
if (!buffer.IsEmpty)
129+
{
130+
ulong random = _randomUInt64.Next();
131+
byte[] bytes = BitConverter.GetBytes(random);
132+
133+
Debug.Assert(buffer.Length < bytes.Length);
134+
135+
for (int i = 0; i < buffer.Length && i < bytes.Length; i++)
136+
buffer[i] = bytes[i];
137+
}
138+
}
139+
}

src/Antithesis.SDK/Assert.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,68 @@ private static void NoGuidanceHelper(AssertionMethodType methodType, bool condit
9999

100100
#endregion
101101

102+
#region Raw
103+
104+
/// <summary>
105+
/// A low-level method designed for use by third-party frameworks that produce Antithesis-style
106+
/// assertions with caller-supplied catalog metadata; regular users should call the assertion
107+
/// methods above instead.
108+
/// <br/><br/>
109+
/// Catalog entries (<c>hit</c> = false) are always written. Hit entries are written only for
110+
/// the first passing and the first failing call per <c>id</c>.
111+
/// </summary>
112+
/// <param name="assertType">One of <c>"always"</c>, <c>"sometimes"</c>, or <c>"reachability"</c>.</param>
113+
/// <param name="displayType">One of <c>"Always"</c>, <c>"AlwaysOrUnreachable"</c>, <c>"Sometimes"</c>, <c>"Reachable"</c>, or <c>"Unreachable"</c>.</param>
114+
/// <param name="className">The name of the class containing the assertion.</param>
115+
/// <param name="functionName">The name of the method containing the assertion.</param>
116+
/// <param name="filePath">The path of the file containing the assertion.</param>
117+
/// <param name="beginLine">The 1-indexed line number where the assertion begins.</param>
118+
/// <param name="beginColumn">The 1-indexed column number where the assertion begins.</param>
119+
/// <param name="id">The unique identifier of the assertion, used to aggregate assertions and gate emission.</param>
120+
/// <param name="condition">The condition being asserted.</param>
121+
/// <param name="message">The human-readable name of the corresponding test property.</param>
122+
/// <param name="details">Optional additional details to provide greater context for assertion passes and failures.</param>
123+
/// <param name="hit">Whether the assertion was evaluated (<c>true</c>) or is being cataloged (<c>false</c>).</param>
124+
/// <param name="mustHit">Whether the corresponding test property fails if the assertion is never encountered.</param>
125+
public static void Raw(
126+
string assertType,
127+
string displayType,
128+
string className,
129+
string functionName,
130+
string filePath,
131+
int beginLine,
132+
int beginColumn,
133+
string id,
134+
bool condition,
135+
string message,
136+
JsonObject? details,
137+
bool hit,
138+
bool mustHit)
139+
{
140+
if (string.IsNullOrEmpty(id))
141+
throw new ArgumentNullException(nameof(id));
142+
143+
if (Sink.IsNoop)
144+
return;
145+
146+
if (hit && !AssertionTracker.ShouldWrite(id, condition))
147+
return;
148+
149+
var location = new LocationInfo
150+
{
151+
ClassName = className,
152+
MethodName = functionName,
153+
FilePath = filePath,
154+
BeginLine = beginLine,
155+
BeginColumn = beginColumn
156+
};
157+
158+
Sink.Write(AssertionInfo.ConstructForRawWrite(
159+
assertType, displayType, id, message, location, mustHit, hit, condition, details));
160+
}
161+
162+
#endregion
163+
102164
#region Numeric Guidance
103165

104166
/// <summary>

src/Antithesis.SDK/Internal/AssertionAndGuidanceTypes.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ AssertionDisplayType.Always or
5050
AssertionDisplayType.Sometimes or
5151
AssertionDisplayType.Reachable =>
5252
true,
53-
53+
5454
AssertionDisplayType.AlwaysOrUnreachable or
5555
AssertionDisplayType.Unreachable =>
5656
false,
@@ -61,6 +61,9 @@ AssertionDisplayType.AlwaysOrUnreachable or
6161

6262
internal static class AssertionMethodTypeExtensions
6363
{
64+
internal static string GetAssertTypeRaw(this AssertionMethodType methodType) =>
65+
methodType.GetAssertType().ToString().ToLowerInvariant();
66+
6467
internal static AssertionAssertType GetAssertType(this AssertionMethodType methodType) => methodType switch
6568
{
6669
AssertionMethodType.Always or
@@ -87,6 +90,9 @@ AssertionMethodType.Unreachable or
8790
_ => throw new NotImplementedException(methodType.ToString())
8891
};
8992

93+
internal static string GetDisplayTypeRaw(this AssertionMethodType methodType) =>
94+
methodType.GetDisplayType().ToString();
95+
9096
internal static AssertionDisplayType GetDisplayType(this AssertionMethodType methodType) => methodType switch
9197
{
9298
AssertionMethodType.Always or
@@ -128,7 +134,7 @@ AssertionMethodType.SometimesGreaterThanOrEqualTo or
128134
AssertionMethodType.SometimesLessThan or
129135
AssertionMethodType.SometimesLessThanOrEqualTo =>
130136
GuidanceType.Numeric,
131-
137+
132138
AssertionMethodType.AlwaysSome or
133139
AssertionMethodType.SometimesAll =>
134140
GuidanceType.Boolean,

0 commit comments

Comments
 (0)