Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<RepositoryType>git</RepositoryType>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageTags>autopoco;testdata;test-data;poco;generator;fluent;fake-data</PackageTags>
<PackageReleaseNotes>6.0: Lambda data sources (FuncSource, Setup(...).From(() => ...), Source(expr, () => ...) at generation time). Opt-in nullability-aware null generation via RespectNullableAnnotations(threshold). Generic NumberSource&lt;T&gt; and NullableNumberSource&lt;T&gt; with min/max for every built-in numeric type (integers uniform and inclusive, floating point and decimal overflow safe). Records and immutable types are constructed through their constructor, parameters are fed from the registered members with the same name, no parameterless constructor needed anymore; unconfigured struct parameters stay default instead of throwing. IDataSource&lt;T&gt; is covariant. Bug fixes: Nullable* sources honor an explicit null creation threshold (null positions of stable sequences changed), DateTime/DateOnly/TimeOnly/DateOfBirth sources cover their whole range including the upper bound (sequences changed), RandomUtfTextSource no longer loops forever. Build and packaging modernized: GitHub Actions CI, trusted publishing to nuget.org, MinVer tag based versioning, central package management, xunit.v3 tests on net8/9/10. FluentAssertions and Moq are no longer dependencies of the library. See README for details.</PackageReleaseNotes>
<PackageReleaseNotes>6.0: Lambda data sources (FuncSource, Setup(...).From(() => ...), Source(expr, () => ...) at generation time). Opt-in nullability-aware null generation via RespectNullableAnnotations(threshold). Generic NumberSource&lt;T&gt; and NullableNumberSource&lt;T&gt; with min/max for every built-in numeric type (integers uniform and inclusive, floating point and decimal overflow safe). Records and immutable types are constructed through their constructor, parameters are fed from the registered members with the same name, no parameterless constructor needed anymore; unconfigured struct parameters stay default instead of throwing. Seeds: UseSeed(seed) per factory, CreateSession(recursionLimit, seed) per session, every member gets its own stream derived from the seed. Own deterministic generator (StableRandom, xoshiro256**) instead of System.Random, sequences are stable across .NET versions. Breaking: AutoPocoGlobalSettings removed (AutoPocoDefaults constants instead), every generated sequence changed once. IDataSource&lt;T&gt; is covariant. Bug fixes: Nullable* sources honor an explicit null creation threshold (null positions of stable sequences changed), DateTime/DateOnly/TimeOnly/DateOfBirth sources cover their whole range including the upper bound (sequences changed), RandomUtfTextSource no longer loops forever. Build and packaging modernized: GitHub Actions CI, trusted publishing to nuget.org, MinVer tag based versioning, central package management, xunit.v3 tests on net8/9/10. FluentAssertions and Moq are no longer dependencies of the library. See README for details.</PackageReleaseNotes>
</PropertyGroup>

<PropertyGroup Label="Package build">
Expand Down
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,23 @@ var factory = AutoPocoContainer.Configure(x => {
```

The rules: the constructor that takes the most members which cannot be set otherwise wins, then the one with the fewest parameters, so ordinary classes keep using their parameterless constructor and their setters. With the default conventions get-only properties that match a constructor parameter are registered too, so an unconfigured `Money` gets the default member values through its constructor. A parameter without a matching member gets its declared default value, the default of its value type, or for other reference types an object generated by the session. A struct without any configured member stays at its default, so an unconfigured `DateOnly` parameter no longer throws. A get-only property with a data source but no matching constructor parameter throws an `InvalidOperationException` that names the property. `CtorSource<T>` is the class behind it; `new CtorSource<T>(constructorInfo)` pins a constructor and still takes the matching members. Constructor arguments honor `RespectNullableAnnotations` and the recursion limit, so a self referencing record such as `record Node(string Name, Node? Child)` terminates.
* Seeds: `x.UseSeed(seed)` sets the seed of a factory, `factory.CreateSession(recursionLimit, seed)` gives a single session another seed. Every data source in a session gets its own random stream derived from the session seed and the member it feeds (`Type.Member`), so two members with the same source type no longer produce the same values, the nullable sources of one object no longer become null together, and the values of a member do not depend on which other members are configured. A seed set on a source in the configuration (`Use<IntegerSource>(s => s.SetSeedToRandomValue(7))`) wins over the session seed. The null patterns of `RespectNullableAnnotations`, the shuffle behind `Random(count)` and constructor arguments follow the seed as well.

```CSHARP
var factory = AutoPocoContainer.Configure(x => {
x.UseSeed(2024); // default is AutoPocoDefaults.Seed (1337)
x.Include<SimpleUser>()
.Setup(c => c.FirstName).Use<FirstNameSource>()
.Setup(c => c.LastName).Use<LastNameSource>();
});

var session = factory.CreateSession(); // seed 2024
var other = factory.CreateSession(5, 4711); // seed 4711, same configuration
```

* Own random number generator: every source draws from `StableRandom` (xoshiro256** seeded through SplitMix64), a `Random` subclass owned by AutoPoco, instead of `System.Random`. The sequence for a seed is defined by AutoPoco's code alone, so test data stays the same across .NET versions; Microsoft explicitly does not promise that for a seeded `System.Random`. The generator is pinned by tests. Rule from now on: catalogs (names, cities, zip codes, ...) and source algorithms are frozen within a major version, a change to either is a major version bump.
* Breaking: `AutoPocoGlobalSettings` is gone, it was mutable process-wide state that leaked between test fixtures. The defaults are read-only constants in `AutoPocoDefaults` (`Seed`, `NullCreationThreshold`, `RecursionLimit`). Set a seed with `UseSeed`, null thresholds per source or with `RespectNullableAnnotations(threshold)`.
* Breaking: because of the own generator and the per-member seeds, every generated sequence changed once with 6.0. Tests that pin generated values need new expectations, this is the last time.
* `IDataSource<T>` is covariant now, so a source of `string` (e.g. `CitySource`) can be used for a `string?` member without a nullability warning.
* Bug fix: every `Nullable*` source ignored an explicit null creation threshold. The fixed array and dictionary based sources (names, companies, cities, capitals, countries, states, zip codes, urls) even used the threshold as their random seed. The threshold is honored now. As a consequence the null positions in the stable sequences of these sources changed, the data itself comes in the same order as before.
* Bug fix: `DateTimeSource`, `DateOnlySource`, `TimeOnlySource` and `DateOfBirthSource` never produced the upper end of their ranges: no December, no 31st, no 23:00, no minute or second 59, and the maximum year of a date of birth was never reached. Ranges within a single year could produce values outside the range. All four sources now pick uniformly from the whole range, both bounds inclusive, and throw an `ArgumentOutOfRangeException` when the maximum lies before the minimum. `TimeOnlySource` supports ranges that wrap around midnight (22:00 to 02:00). `DateTimeSource` keeps the `DateTimeKind` of the minimum date. The generated sequences for a given seed changed.
Expand Down Expand Up @@ -166,12 +183,12 @@ We plan to add more datasources in future and so we want to provide a better ove


# Behavior change (maybe, only some datasources)
All datasources reproduce same results within same seed - important for testing. Additionally you can use
All datasources reproduce same results within same seed - important for testing. Since 6.0 the seed of a factory is set with `x.UseSeed(seed)` and a single session can get another one with `CreateSession(recursionLimit, seed)`, see "New in 6.0.0". Additionally you can use

* DataSource.SetSeedToRandomValue()
* DataSource.SetSeedToRandomValue(seed)

to change behavior or get other values as the internal seed constant generates.
on a single source; a seed set this way wins over the session seed.

# AutoPoco.NetCore
AutoPoco is a highly configurable framework for the purpose of fluently building readable test data from Plain Old CLR Objects. This is a modernized (i.e. dotnet core 2.0.0) port of the excellent AutoPoco project which hasn't been maintained since 2014. Thanks to [@robashton](https://twitter.com/robashton) for graciously agreeing to let me pull this fossil out of the amber and give it a light dusting.
Expand Down
8 changes: 8 additions & 0 deletions holonsoft.AutoPoco.Tests/Common/TwoNullableNumbersClass.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace holonsoft.AutoPoco.Tests.Common;

public class TwoNullableNumbersClass {
public int? First { get; set; }
public int? Second { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
}
12 changes: 6 additions & 6 deletions holonsoft.AutoPoco.Tests/Demo/DemoAutoPoco.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ public void SomeDemonstrationOfComplexGenerations()
.Single<SimpleUser>()
.Get();

user.FirstName.ShouldBe("Olivia");
user.LastName.ShouldBe("Turner");
user.RevealedPassword.ShouldBe("GRvqwwLW");
user.FirstName.ShouldBe("Alexa");
user.LastName.ShouldBe("Wilson");
user.RevealedPassword.ShouldBe("DPj`I`FMBd");
user.ExternalId.ShouldNotBe(0);
user.Id.ShouldNotBe(0);
user.City.ShouldNotBeNullOrWhiteSpace();
Expand Down Expand Up @@ -161,9 +161,9 @@ public void SomeDemonstrationOfComplexGenerationsWithRecords()
.Single<SimpleUserRecord>()
.Get();

user.FirstName.ShouldBe("Olivia");
user.LastName.ShouldBe("Turner");
user.RevealedPassword.ShouldBe("GRvqwwLW");
user.FirstName.ShouldBe("Arthur");
user.LastName.ShouldBe("Walker");
user.RevealedPassword.ShouldBe("_AqPqoJvdRvaHXmjJK");

// Create three roles
// Create 100 users
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.Drawing;
using System.Globalization;
using System.Text;

namespace holonsoft.AutoPoco.Tests.Functionality.Tests.DataSources.Base;

/// <summary>
/// Formats generated values as C# source, so a stable sequence test that fails after an intended
/// change prints the new expected array ready to paste.
/// </summary>
public static class CSharpLiteral {
public static string Array<T>(IEnumerable<T> values) {
var type = typeof(T);
var elementType = Nullable.GetUnderlyingType(type) ?? type;
var list = values.ToList();
var nullable = type != elementType || (!type.IsValueType && list.Any(v => v is null));
var typeName = TypeName(elementType) + (nullable ? "?" : "");
return $"new {typeName}[] {{ {string.Join(", ", list.Select(v => Value(v)))} }}";
}

public static string Value(object? value)
=> value switch {
null => "null",
string s => Quote(s),
bool b => b ? "true" : "false",
int or long or short or byte or sbyte or ushort or uint or ulong => Convert.ToString(value, CultureInfo.InvariantCulture)!,
Int128 or UInt128 => $"{TypeName(value.GetType())}.Parse(\"{value}\")",
decimal m => m.ToString(CultureInfo.InvariantCulture) + "M",
double d => d.ToString("R", CultureInfo.InvariantCulture) + "D",
float f => f.ToString("R", CultureInfo.InvariantCulture) + "F",
DateTime dt => $"new DateTime({dt.Ticks}, DateTimeKind.{dt.Kind})",
DateOnly d => $"new DateOnly({d.Year}, {d.Month}, {d.Day})",
TimeOnly t => $"new TimeOnly({t.Ticks})",
TimeSpan ts => $"new TimeSpan({ts.Ticks})",
Guid g => $"new Guid(\"{g}\")",
Color c => $"Color.FromArgb({c.A}, {c.R}, {c.G}, {c.B})",
Enum e => $"{e.GetType().Name}.{e}",
_ => value.ToString() ?? "null"
};

private static string TypeName(Type type)
=> type switch {
_ when type == typeof(int) => "int",
_ when type == typeof(long) => "long",
_ when type == typeof(string) => "string",
_ when type == typeof(bool) => "bool",
_ when type == typeof(decimal) => "decimal",
_ when type == typeof(double) => "double",
_ when type == typeof(float) => "float",
_ => type.Name
};

private static string Quote(string value) {
var builder = new StringBuilder("\"");
foreach (var c in value)
builder.Append(c switch {
'"' => "\\\"",
'\\' => "\\\\",
'\n' => "\\n",
'\r' => "\\r",
'\t' => "\\t",
_ when c < ' ' => $"\\u{(int) c:x4}",
_ => c.ToString()
});

return builder.Append('"').ToString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ public static void NextReturnsStableElementListInTermsOfTestability<T>(DataSourc
for (var i = 0; i < expectedValues.Length; i++)
generated.Add(source.Next(null));

// same count and same order as the expected values
generated.ShouldBe(expectedValues);
// same count and same order as the expected values; on failure the message carries the actual
// values as a C# literal, ready to paste into the test after an intended sequence change
try {
generated.ShouldBe(expectedValues);
} catch (ShouldAssertException e) {
throw new ShouldAssertException($"{e.Message}\nREGENERATE: {CSharpLiteral.Array(generated)}");
}

if (typeof(T) == typeof(bool) || (typeof(T) == typeof(bool?)))
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,10 @@ public class CompanySourceTest : TestBase {
[Fact]
public void NextReturnsStableCompanyListInTermsOfTestability()
=> NextReturnsStableElementListInTermsOfTestability(
new CompanySource(),
new[] {
"Sto Plains Holdings", "Ankh-Sto Associates", "Gringotts", "Minuteman Cafe", "Niagular", "Moes Tavern", "Moes Tavern",
"Praxis Corporation", "Kumatsu Motors", "Mammoth Pictures"
});
new CompanySource(), new string[] { "Charles Townsend Agency", "Chotchkies", "Carrys Candles", "LexCorp", "Monks Diner", "Big T Burgers and Fries", "Keedsler Motors", "Wernham Hogg", "Moes Tavern", "C.H. Lavatory and Sons" });

[Fact]
public void NextReturnsStableCompanyListInTermsOfTestabilityAndListCanContainNull()
=> NextReturnsStableElementListInTermsOfTestability(
new NullableCompanySource()!,
"Sto Plains Holdings", null, "Ankh-Sto Associates", "Gringotts", "Minuteman Cafe", "Niagular", "Moes Tavern", "Moes Tavern", "Praxis Corporation", "Kumatsu Motors",
"Mammoth Pictures", "Mammoth Pictures", "Water and Power", null, "Rouster and Sideways", null, "ABC Telecom", "Niagular", "Widget Corp", null);
new NullableCompanySource()!, new string[] { "Charles Townsend Agency", "Chotchkies", "Carrys Candles", "LexCorp", "Monks Diner", "Big T Burgers and Fries", "Keedsler Motors", "Wernham Hogg", "Moes Tavern", "C.H. Lavatory and Sons", "Omni Consimer Products", "Charles Townsend Agency", "Barrytron", "The New Firm", "Transworld Consortium", "Gizmonic Institute", "Chasers", "Globo-Chem", "Thrift Bank", "Kumatsu Motors" });
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,10 @@ public void NextReturnsRandomCreditCardNumber() {
[Fact]
public void NextReturnsStableCreditCardListInTermsOfTestability()
=> NextReturnsStableElementListInTermsOfTestability(
new CreditCardSource(),
new[] {
"5139 8991 3232 7084", "5898 0489 1744 1883", "3695 475531 91051", "3224 797328 93124", "5358 3556 3251 4365", "3027 325203 91438",
"5615 3395 0850 1892", "3644 657657 96148", "4440 3264 5197 1047", "5316 2174 7772 0575"
});
new CreditCardSource(), new string[] { "5343 1380 7226 1800", "4963 6432 2865 0035", "4655 0609 2255 2430", "5914 8329 7669 1747", "3306 710005 18638", "3576 703413 80305", "5171 2438 3073 1141", "5030 2600 8079 1583", "5159 0467 9793 1279", "5238 6720 4807 5702" });

[Fact]
public void NextReturnsStableCreditCardListInTermsOfTestabilityAndListCanContainNull()
=> NextReturnsStableElementListInTermsOfTestability(
new NullableCreditCardSource()!,
new string?[] {
"5139 8991 3232 7084", null, "5898 0489 1744 1883", "3695 475531 91051", "3224 797328 93124", "5358 3556 3251 4365", "3027 325203 91438",
"5615 3395 0850 1892", "3644 657657 96148", "4440 3264 5197 1047"
});
new NullableCreditCardSource()!, new string[] { "5343 1380 7226 1800", "4963 6432 2865 0035", "4655 0609 2255 2430", "5914 8329 7669 1747", "3306 710005 18638", "3576 703413 80305", "5171 2438 3073 1141", "5030 2600 8079 1583", "5159 0467 9793 1279", "5238 6720 4807 5702" });
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,5 @@ public void NextReturnsStableEmailListInTermsOfTestability()
[Fact]
public void NextReturnsStableEmailListInTermsOfTestabilityAndListCanContainNull()
=> NextReturnsStableElementListInTermsOfTestability(
new NullableEmailAddressSource()!,
"eg0@example.test", null, "eg1@example.test", "eg2@example.test", "eg3@example.test", "eg4@example.test", "eg5@example.test", "eg6@example.test",
"eg7@example.test", "eg8@example.test", "eg9@example.test", "eg10@example.test", "eg11@example.test", null,
"eg12@example.test", null, "eg13@example.test", "eg14@example.test", "eg15@example.test"
);
new NullableEmailAddressSource()!, new string[] { "eg0@example.test", "eg1@example.test", "eg2@example.test", "eg3@example.test", "eg4@example.test", "eg5@example.test", "eg6@example.test", "eg7@example.test", "eg8@example.test", "eg9@example.test", "eg10@example.test", "eg11@example.test", "eg12@example.test", "eg13@example.test", "eg14@example.test", "eg15@example.test", "eg16@example.test", "eg17@example.test", "eg18@example.test" });
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,9 @@ public class ExtendedEmailAddressSourceTests : TestBase {
[Fact]
public void NextReturnsStableEmailListInTermsOfTestability()
=> NextReturnsStableElementListInTermsOfTestability(
new ExtendedEmailAddressSource(),
"Olivia.Turner@holonsoft.invalid", "Christopher.Martin@hotmail.invalid", "Tyler.Murphy@golem.invalid", "Jason.Pierce@microsoft.example",
"Zoe.Wells@yahoo.example", "Eric.Scott@heise.example", "Eric.Scott@heise.example", "Sophia.Baker@yahoo.invalid", "Abigail.Perez@msn.test",
"Ethan.Evans@heise.invalid"
);
new ExtendedEmailAddressSource(), new string[] { "Daniel.Knight@heise.example", "Zoey.Harrison@yahoo.test", "Sophia.Baker@hotmail.invalid", "Victoria.Diaz@google.example", "Aubrey.Harper@golem.test", "George.Dean@hotmail.test", "Ava.Thomas@microsoft.test", "Daniel.Knight@golem.test", "Nora.Long@aol.example", "Scott.Gordon@google.invalid" });

[Fact]
public void NextReturnsStableEmailListInTermsOfTestabilityAndListCanContainNull() => NextReturnsStableElementListInTermsOfTestability(
new NullableExtendedEmailAddressSource()!,
"Olivia.Turner@holonsoft.invalid", null, "Christopher.Martin@hotmail.invalid", "Tyler.Murphy@golem.invalid", "Jason.Pierce@microsoft.example",
"Zoe.Wells@yahoo.example", "Eric.Scott@heise.example", "Eric.Scott@heise.example", "Sophia.Baker@yahoo.invalid", "Abigail.Perez@msn.test"
);
new NullableExtendedEmailAddressSource()!, new string[] { "Daniel.Knight@heise.example", "Zoey.Harrison@yahoo.test", "Sophia.Baker@hotmail.invalid", "Victoria.Diaz@google.example", "Aubrey.Harper@golem.test", "George.Dean@hotmail.test", "Ava.Thomas@microsoft.test", "Daniel.Knight@golem.test", "Nora.Long@aol.example", "Scott.Gordon@google.invalid" });
}
Loading