diff --git a/Directory.Build.props b/Directory.Build.props
index 834a17f..05bae2b 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -25,7 +25,7 @@
git
README.md
autopoco;testdata;test-data;poco;generator;fluent;fake-data
- 6.0: Lambda data sources (FuncSource, Setup(...).From(() => ...), Source(expr, () => ...) at generation time). Opt-in nullability-aware null generation via RespectNullableAnnotations(threshold). Generic NumberSource<T> and NullableNumberSource<T> 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, the max of IntegerSource/LongSource/RandomNumberSource and their nullable variants is inclusive now (it was exclusive), Int128Source/NullableInt128Source pick uniformly instead of clamping to the bounds (they never produced a negative value before). Index-aware Impose on lists and selections (value from the position, or from position and item), Impose(member, item => value) on a single generator. Invoke arguments: Use.From(lambda), constants and variables arrive as values. Context lambdas get a non-nullable IGenerationContext. Argument validation over the public API, meaningful exception messages, XML documentation file. IDataSource<T> 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.
+ 6.0: Lambda data sources (FuncSource, Setup(...).From(() => ...), Source(expr, () => ...) at generation time). Opt-in nullability-aware null generation via RespectNullableAnnotations(threshold). Generic NumberSource<T> and NullableNumberSource<T> 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, the max of IntegerSource/LongSource/RandomNumberSource and their nullable variants is inclusive now (it was exclusive), Int128Source/NullableInt128Source pick uniformly instead of clamping to the bounds (they never produced a negative value before), RandomStringSource and UrlSource reach the last character resp. the last top level domain, ColorSource/RandomTextSource/RandomUtfTextSource treat their maximum as inclusive, ColorSource applies the alpha of SetColorRange, and UrlSource and ExtendedEmailAddressSource seed their nested sources (the URL host ignored the session seed completely). Index-aware Impose on lists and selections (value from the position, or from position and item), Impose(member, item => value) on a single generator. Invoke arguments: Use.From(lambda), constants and variables arrive as values. Context lambdas get a non-nullable IGenerationContext. Argument validation over the public API, meaningful exception messages, XML documentation file. IDataSource<T> 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.
diff --git a/README.md b/README.md
index 2d1bd1e..ffdda8c 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,9 @@ holonsoft ported this famous lib to newest version of dotnet
* Every generated sequence changed once, mainly because every member has its own random stream now. Tests that pin generated values need new expectations. This is the last time, see "Why the sequences changed" below.
* `IntegerSource`, `NullableIntegerSource`, `LongSource`, `NullableLongSource`, `RandomNumberSource` and `NullableRandomNumberSource` treat `max` as **inclusive** now, it used to be exclusive. `new IntegerSource(1, 3)` produces 3 as well. This matches `NumberSource` and the date sources: the maximum of every integer and date source is inclusive now.
* `Int128Source` and `NullableInt128Source` pick uniformly from their range instead of generating a value and clamping it to the bounds. The old implementation never produced a negative value and returned the upper bound almost always on a restricted range.
+* `RandomStringSource` and `UrlSource` never used the last character of the allowed character set and the last top level domain of their list. `ColorSource`, `RandomTextSource` and `RandomUtfTextSource` treat their maximum as inclusive now, so `new ColorSource(0, 255)` reaches 255 and a text source asked for 3 to 6 paragraphs can produce 6.
+* `ColorSource` applies the alpha given to `SetColorRange(alpha, ...)`, it used to be silently dropped and every color came back fully opaque.
+* `UrlSource` follows the session seed with its host part as well. The nested string source kept the default seed, so every session produced the same host names no matter what `UseSeed` said. `ExtendedEmailAddressSource` gives its first and last name source separate streams, they used to share one and repeated the same name pairs.
* `AutoPocoGlobalSettings` is gone, the defaults are read-only constants in `AutoPocoDefaults`.
* The never implemented `Ctor(...)` stub on the type builder is gone.
* The lambdas receiving the generation context get a non-nullable `IGenerationContext`, no more `ctx!`.
@@ -133,6 +136,8 @@ x.Include()
* Hardening: the public configuration and generation API validates its arguments (`ArgumentNullException`, `ArgumentOutOfRangeException` for negative counts, `ArgumentException` for unknown members, non-source types and non-convention types) and every internal failure carries the type and member it happened at. The never implemented `Ctor(...)` stub on the type builder is gone. The package ships an XML documentation file.
* `IDataSource` is covariant now, so a source of `string` (e.g. `CitySource`) can be used for a `string?` member without a nullability warning.
* Bug fix: the integer sources that existed before `NumberSource` never produced their maximum, `new IntegerSource(1, 3)` gave 1 or 2 only. `IntegerSource`, `LongSource`, `RandomNumberSource` and their nullable variants draw uniformly from the inclusive range now, both bounds can be produced. `Int128Source` and `NullableInt128Source` were worse: they built a value from two non-negative draws and clamped it to the range, so the default source never produced a negative value and `new Int128Source(1, 5)` returned 5 almost every time. All of them share the pick of `NumberSource` now. `LongSourceBase.SetMinMax` takes `long` arguments at last, and all four throw an `ArgumentOutOfRangeException` when the maximum lies below the minimum. The generated sequences of the long and Int128 sources changed, the ones of the integer sources did not.
+* Bug fix: a handful of sources could not reach the last element of what they draw from. `RandomStringSource` never used the last character of its allowed character set, `UrlSource` never used the last top level domain of its list, and `ColorSource`, `RandomTextSource` and `RandomUtfTextSource` treated their maximum as exclusive, so a colour channel range of 0 to 255 never gave 255 and a text source asked for 3 to 6 paragraphs never gave 6. On top of that `ColorSource` dropped the alpha of `SetColorRange(alpha, ...)` and always returned an opaque colour.
+* Bug fix: sources that drive another source internally seed it now. `UrlSource` builds its host with a nested `RandomStringSource` that kept the default seed, so `UseSeed` and `CreateSession(limit, seed)` changed the top level domain but every session saw the same host names. `ExtendedEmailAddressSource` passed one seed to both its first and last name source, so the two followed the same sequence of draws and the same first name came back with the same last name. Both give their nested sources a derived stream of their own now, inside a session and standalone.
* 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.
* Bug fix: `RandomUtfTextSource` could loop forever when it hit a Unicode block without any allowed character.
diff --git a/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Business/ExtendedEmailAddressSourceTests.cs b/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Business/ExtendedEmailAddressSourceTests.cs
index 5a2a6cb..6e5c4b9 100644
--- a/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Business/ExtendedEmailAddressSourceTests.cs
+++ b/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Business/ExtendedEmailAddressSourceTests.cs
@@ -1,4 +1,5 @@
-using Xunit;
+using Shouldly;
+using Xunit;
using holonsoft.AutoPoco.DataSources.Business;
using holonsoft.AutoPoco.Tests.Functionality.Tests.DataSources.Base;
@@ -8,9 +9,27 @@ public class ExtendedEmailAddressSourceTests : TestBase {
[Fact]
public void NextReturnsStableEmailListInTermsOfTestability()
=> NextReturnsStableElementListInTermsOfTestability(
- 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" });
+ new ExtendedEmailAddressSource(), new string[] { "Ethan.Brown@heise.example", "Brian.Mason@yahoo.test", "Mila.Ortiz@hotmail.invalid", "Robert.Lopez@google.example", "Ellie.Murphy@golem.test", "Michael.Patterson@hotmail.test", "Sophie.Mason@microsoft.test", "Hannah.Fox@golem.test", "Isaac.Parker@aol.example", "Ariana.Walker@google.invalid" });
[Fact]
public void NextReturnsStableEmailListInTermsOfTestabilityAndListCanContainNull() => NextReturnsStableElementListInTermsOfTestability(
- 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" });
+ new NullableExtendedEmailAddressSource()!, new string[] { "Ethan.Brown@heise.example", "Brian.Mason@yahoo.test", "Mila.Ortiz@hotmail.invalid", "Robert.Lopez@google.example", "Ellie.Murphy@golem.test", "Michael.Patterson@hotmail.test", "Sophie.Mason@microsoft.test", "Hannah.Fox@golem.test", "Isaac.Parker@aol.example", "Ariana.Walker@google.invalid" });
+
+ [Fact]
+ public void TheFirstAndTheLastNameDoNotFollowTheSameStream() {
+ // both nested sources used to share one seed, so the same first name came back with the same last name
+ var source = new ExtendedEmailAddressSource();
+ var pairs = Enumerable.Range(0, 200).Select(_ => source.Next(null).Split('@')[0]).ToList();
+ var firstNames = pairs.Select(p => p.Split('.')[0]).ToList();
+
+ var repeatedFirstName = firstNames
+ .GroupBy(x => x)
+ .FirstOrDefault(g => g.Count() > 1)?.Key;
+
+ repeatedFirstName.ShouldNotBeNull("the sample is too small to say anything about the pairing");
+ pairs.Where(p => p.StartsWith($"{repeatedFirstName}.", StringComparison.Ordinal))
+ .Distinct()
+ .Count()
+ .ShouldBeGreaterThan(1);
+ }
}
\ No newline at end of file
diff --git a/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Business/RandomTextTests.cs b/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Business/RandomTextTests.cs
index 3303d33..44dc12e 100644
--- a/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Business/RandomTextTests.cs
+++ b/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Business/RandomTextTests.cs
@@ -14,6 +14,19 @@ public void NextReturnsAParagraph() {
value.ShouldNotBeNullOrWhiteSpace();
}
+ [Fact]
+ public void NextReachesTheMaximumParagraphCount() {
+ // no truncation, so every paragraph survives and can be counted
+ var source = new RandomTextSource(int.MaxValue, 1, 3, 1, 1, "abc".ToCharArray());
+ var counts = Enumerable.Range(0, 200)
+ .Select(_ => source.Next(null).Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries).Length)
+ .ToList();
+
+ counts.ShouldAllBe(c => c >= 1 && c <= 3);
+ counts.ShouldContain(3);
+ counts.ShouldContain(1);
+ }
+
[Fact]
public void NextReturnsTwoParagraph() {
var source = new RandomTextSource();
diff --git a/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Business/RandomUtfTextTest.cs b/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Business/RandomUtfTextTest.cs
index 9188df1..46cd5a3 100644
--- a/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Business/RandomUtfTextTest.cs
+++ b/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Business/RandomUtfTextTest.cs
@@ -13,6 +13,19 @@ public void NextReturnsAParagraph() {
value.ShouldNotBeNullOrWhiteSpace();
}
+ [Fact]
+ public void NextReachesTheMaximumParagraphCount() {
+ // no truncation, so every paragraph survives and can be counted
+ var source = new RandomUtfTextSource(int.MaxValue, 1, 3, 1, 1);
+ var counts = Enumerable.Range(0, 200)
+ .Select(_ => source.Next(null).Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries).Length)
+ .ToList();
+
+ counts.ShouldAllBe(c => c >= 1 && c <= 3);
+ counts.ShouldContain(3);
+ counts.ShouldContain(1);
+ }
+
[Fact]
public void NextTerminatesForManyDrawsEvenWhenABlockHasNoAllowedCharacters() {
// regression: a block made only of excluded categories used to loop forever
diff --git a/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Business/UrlSourceTest.cs b/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Business/UrlSourceTest.cs
index fb0821a..f741ad3 100644
--- a/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Business/UrlSourceTest.cs
+++ b/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Business/UrlSourceTest.cs
@@ -1,5 +1,7 @@
-using Xunit;
+using Shouldly;
+using Xunit;
using holonsoft.AutoPoco.DataSources.Business;
+using holonsoft.AutoPoco.Engine.Interfaces;
using holonsoft.AutoPoco.Tests.Functionality.Tests.DataSources.Base;
namespace holonsoft.AutoPoco.Tests.Functionality.Tests.DataSources.Business;
@@ -8,11 +10,33 @@ public class UrlSourceTest() : TestBase {
[Fact]
public void NextReturnsStableUrlListInTermsOfTestability()
=> NextReturnsStableElementListInTermsOfTestability(
- new UrlSource(), new string[] { "http://www.pdutnrt.ua", "http://www.axc.iq", "http://www.mgriq.pe", "http://www.jgdowudn.in", "http://www.siowk.om", "http://www.faqdogfvk.tr", "http://www.wqj.nz", "http://www.scfmvsendy.by", "http://www.bmey.ee", "http://www.ksxgwj.pl", "http://www.nxud.ua", "http://www.qgoxobql.ke", "http://www.aqfbywdw.vn", "http://www.vxwhakt.me", "http://www.ebdiqmt.mv", "http://www.oqmorhrsed.ru", "http://www.idqhtrremq.si", "http://www.qswnqa.al", "http://www.aop.pk", "http://www.hbvimbnfj.mx" });
+ new UrlSource(), new string[] { "http://www.varh.ua", "http://www.pislmnk.iq", "http://www.stmbxgpy.pe", "http://www.kqva.in", "http://www.xyxismavc.om", "http://www.jzbwg.tr", "http://www.pkd.nz", "http://www.ikcoogs.by", "http://www.aozzhwbm.ee", "http://www.hsck.pl", "http://www.pdlfxj.ua", "http://www.puckcn.ke", "http://www.nixcw.vn", "http://www.wflid.me", "http://www.sobnyuv.mv", "http://www.esmovka.ru", "http://www.zqvpyte.si", "http://www.bdfztuua.al", "http://www.hapccijl.pk", "http://www.ggrfehefpp.mx" });
[Fact]
public void NextReturnsStableUrlListInTermsOfTestabilityAndListCanContainNull()
=> NextReturnsStableElementListInTermsOfTestability(
- new NullableUrlSource()!, new string[] { "http://www.pdutnrt.ua", "http://www.axc.iq", "http://www.mgriq.pe", "http://www.jgdowudn.in", "http://www.siowk.om", "http://www.faqdogfvk.tr", "http://www.wqj.nz", "http://www.scfmvsendy.by", "http://www.bmey.ee", "http://www.ksxgwj.pl", "http://www.nxud.ua", "http://www.qgoxobql.ke", "http://www.aqfbywdw.vn", "http://www.vxwhakt.me", "http://www.ebdiqmt.mv", "http://www.oqmorhrsed.ru", "http://www.idqhtrremq.si", "http://www.qswnqa.al", "http://www.aop.pk", "http://www.hbvimbnfj.mx", "http://www.elp.pe", "http://www.hjxmtllbs.ir" });
+ new NullableUrlSource()!, new string[] { "http://www.varh.ua", "http://www.pislmnk.iq", "http://www.stmbxgpy.pe", "http://www.kqva.in", "http://www.xyxismavc.om", "http://www.jzbwg.tr", "http://www.pkd.nz", "http://www.ikcoogs.by", "http://www.aozzhwbm.ee", "http://www.hsck.pl", "http://www.pdlfxj.ua", "http://www.puckcn.ke", "http://www.nixcw.vn", "http://www.wflid.me", "http://www.sobnyuv.mv", "http://www.esmovka.ru", "http://www.zqvpyte.si", "http://www.bdfztuua.al", "http://www.hapccijl.pk", "http://www.ggrfehefpp.mx", "http://www.mdztzkew.pe", "http://www.fymd.ir" });
+
+ [Fact]
+ public void TheHostFollowsTheSessionSeedAndNotOnlyTheTopLevelDomain() {
+ // the nested string source used to keep the default seed, so every session produced the same hosts
+ static string[] Hosts(int seed) {
+ var source = new UrlSource();
+ ((ISessionSeedable) source).ApplySessionSeed(seed);
+ return Enumerable.Range(0, 5)
+ .Select(_ => source.Next(null).Split('.')[1])
+ .ToArray();
+ }
+
+ Hosts(1337).ShouldNotBe(Hosts(4711));
+ }
+
+ [Fact]
+ public void NextUsesTheLastTopLevelDomainAsWell() {
+ var source = new UrlSource();
+ var tlds = Enumerable.Range(0, 5000).Select(_ => source.Next(null).Split('.')[^1]).ToHashSet();
+
+ tlds.ShouldContain("ye");
+ }
}
diff --git a/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Primitives/ColorSourceTests.cs b/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Primitives/ColorSourceTests.cs
index c5c4966..1c7a6fd 100644
--- a/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Primitives/ColorSourceTests.cs
+++ b/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Primitives/ColorSourceTests.cs
@@ -1,4 +1,5 @@
using System.Drawing;
+using Shouldly;
using Xunit;
using holonsoft.AutoPoco.DataSources.Primitives;
using holonsoft.AutoPoco.Tests.Functionality.Tests.DataSources.Base;
@@ -19,4 +20,36 @@ public void NextReturnsStableColorListInTermsOfTestabilityAndListCanContainNull(
NextReturnsStableElementListInTermsOfTestability(source, new Color?[] { Color.FromArgb(255, 220, 239, 67), Color.FromArgb(255, 148, 115, 109), Color.FromArgb(255, 177, 83, 248), Color.FromArgb(255, 96, 23, 220), Color.FromArgb(255, 194, 98, 44), Color.FromArgb(255, 230, 122, 17), Color.FromArgb(255, 168, 48, 125), Color.FromArgb(255, 233, 70, 67), Color.FromArgb(255, 110, 27, 246), Color.FromArgb(255, 148, 163, 90) });
}
+
+ [Fact]
+ public void NextReachesBothEndsOfAChannelRange() {
+ var source = new ColorSource(0, 1);
+ var values = Enumerable.Range(0, 300).Select(_ => source.Next(null)).ToList();
+
+ values.ShouldAllBe(c => c.R <= 1 && c.G <= 1 && c.B <= 1);
+ values.ShouldContain(c => c.R == 1);
+ values.ShouldContain(c => c.R == 0);
+ }
+
+ [Fact]
+ public void NextReachesTheFullChannelValue() {
+ var source = new ColorSource(254, 255);
+
+ Enumerable.Range(0, 300).Select(_ => source.Next(null)).ShouldContain(c => c.R == 255);
+ }
+
+ [Fact]
+ public void NextAppliesTheAlphaOfSetColorRange() {
+ var source = new ColorSource();
+ source.SetColorRange(128, 0, 255, 0, 255, 0, 255);
+
+ Enumerable.Range(0, 10).Select(_ => source.Next(null)).ShouldAllBe(c => c.A == 128);
+ }
+
+ [Fact]
+ public void NextUsesAnOpaqueAlphaByDefault() {
+ var source = new ColorSource();
+
+ Enumerable.Range(0, 10).Select(_ => source.Next(null)).ShouldAllBe(c => c.A == 255);
+ }
}
diff --git a/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Primitives/RandomStringSourceTests.cs b/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Primitives/RandomStringSourceTests.cs
index 0db3ec3..6d2168d 100644
--- a/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Primitives/RandomStringSourceTests.cs
+++ b/holonsoft.AutoPoco.Tests/Functionality.Tests/DataSources/Primitives/RandomStringSourceTests.cs
@@ -23,12 +23,30 @@ public void NextReturnsStringOfCorrectSize(int min, int max) {
[Fact]
public void NextReturnsStableStringsInTermsOfTestability() {
var source = new RandomStringSource(5, 7, 'A', (char) 123);
- NextReturnsStableElementListInTermsOfTestability(source, new string[] { "pDUtn", "TyaX]C", "mgRiqjG", "\\wUd[[n", "[SIowk_", "aQD^oG", "VKa^WQ", "`ScFmV", "End`yZB", "E]yDK" });
+ NextReturnsStableElementListInTermsOfTestability(source, new string[] { "pDUtn", "TyaX]C", "mg{Riqj", "Do\\wUd[", "ns[SIow", "_FaQD^o", "FVKa^WQ", "`ScFmV", "End`yZB", "E]yDK" });
}
[Fact]
public void NextReturnsStableStringsInTermsOfTestabilityAndListCanContainNull() {
var source = new NullableRandomStringSource(5, 7, 'A', (char) 123);
- NextReturnsStableElementListInTermsOfTestability(source!, new string?[] { "pDUtn", "TyaX]C", "mgRiqjG", "\\wUd[[n", "[SIowk_", "aQD^oG", "VKa^WQ", "`ScFmV", "End`yZB", "E]yDK" });
+ NextReturnsStableElementListInTermsOfTestability(source!, new string?[] { "pDUtn", "TyaX]C", "mg{Riqj", "Do\\wUd[", "ns[SIow", "_FaQD^o", "FVKa^WQ", "`ScFmV", "End`yZB", "E]yDK" });
+ }
+
+ [Fact]
+ public void NextUsesTheLastAllowedCharacterAsWell() {
+ var source = new RandomStringSource(20, 20, 'a', 'c');
+ var used = string.Concat(Enumerable.Range(0, 50).Select(_ => source.Next(null)));
+
+ used.ShouldContain('a');
+ used.ShouldContain('b');
+ used.ShouldContain('c');
+ }
+
+ [Fact]
+ public void NextUsesTheLastCharacterOfAnExplicitCharacterSet() {
+ var source = new RandomStringSource(20, 20, new[] { 'x', 'y', 'z' });
+ var used = string.Concat(Enumerable.Range(0, 50).Select(_ => source.Next(null)));
+
+ used.ShouldContain('z');
}
}
diff --git a/holonsoft.AutoPoco/DataSources/Business/ExtendedEmailAddressSource.cs b/holonsoft.AutoPoco/DataSources/Business/ExtendedEmailAddressSource.cs
index ce64ec9..6c8d9a3 100644
--- a/holonsoft.AutoPoco/DataSources/Business/ExtendedEmailAddressSource.cs
+++ b/holonsoft.AutoPoco/DataSources/Business/ExtendedEmailAddressSource.cs
@@ -1,11 +1,16 @@
using holonsoft.AutoPoco.Configuration;
using holonsoft.AutoPoco.Engine;
using holonsoft.AutoPoco.Engine.Interfaces;
+using holonsoft.AutoPoco.Util;
namespace holonsoft.AutoPoco.DataSources.Business;
public abstract class ExtendedEmailAddressSourceBase(int? nullCreationThreshold = null, params string[] domains) : DataSourceBase(nullCreationThreshold) {
- private readonly FirstNameSource _firstNameSource = new();
- private readonly LastNameSource _lastNameSource = new();
+ private const string _firstNamePurpose = "ExtendedEmailAddressSource#firstName";
+ private const string _lastNamePurpose = "ExtendedEmailAddressSource#lastName";
+
+ // own streams from the start, otherwise both names follow the same sequence of indices into their catalog
+ private readonly FirstNameSource _firstNameSource = NestedSource.Seeded(new FirstNameSource(), _firstNamePurpose);
+ private readonly LastNameSource _lastNameSource = NestedSource.Seeded(new LastNameSource(), _lastNamePurpose);
private readonly string[] _domainsOfCaller = domains;
@@ -17,8 +22,8 @@ public override void SetSeedToRandomValue() {
public override void SetSeedToRandomValue(int seed) {
base.SetSeedToRandomValue(seed);
- _firstNameSource.SetSeedToRandomValue(seed);
- _lastNameSource.SetSeedToRandomValue(seed);
+ _firstNameSource.SetSeedToRandomValue(NestedSource.Seed(seed, _firstNamePurpose));
+ _lastNameSource.SetSeedToRandomValue(NestedSource.Seed(seed, _lastNamePurpose));
}
///
diff --git a/holonsoft.AutoPoco/DataSources/Business/RandomTextSource.cs b/holonsoft.AutoPoco/DataSources/Business/RandomTextSource.cs
index cc36e93..3ea0296 100644
--- a/holonsoft.AutoPoco/DataSources/Business/RandomTextSource.cs
+++ b/holonsoft.AutoPoco/DataSources/Business/RandomTextSource.cs
@@ -8,6 +8,7 @@
using holonsoft.AutoPoco.Configuration;
using holonsoft.AutoPoco.Engine;
using holonsoft.AutoPoco.Engine.Interfaces;
+using holonsoft.AutoPoco.Util;
namespace holonsoft.AutoPoco.DataSources.Business;
@@ -20,7 +21,7 @@ protected override string GetNextValue(IGenerationContext? context) {
StringBuilder builder = new();
- var paragraphCount = Random.Next(minParagraphCount, maxParagraphCount);
+ var paragraphCount = Random.NextInclusive(minParagraphCount, maxParagraphCount);
for (var i = 0; i < paragraphCount; i++) {
for (var j = minSentenceCount; j <= maxSentenceCount; j++) {
diff --git a/holonsoft.AutoPoco/DataSources/Business/RandomUtfTextSource.cs b/holonsoft.AutoPoco/DataSources/Business/RandomUtfTextSource.cs
index 5fd4a2c..f8f7a29 100644
--- a/holonsoft.AutoPoco/DataSources/Business/RandomUtfTextSource.cs
+++ b/holonsoft.AutoPoco/DataSources/Business/RandomUtfTextSource.cs
@@ -1,6 +1,7 @@
using holonsoft.AutoPoco.Configuration;
using holonsoft.AutoPoco.Engine.Interfaces;
using holonsoft.AutoPoco.Engine;
+using holonsoft.AutoPoco.Util;
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -23,7 +24,7 @@ protected override string GetNextValue(IGenerationContext? context) {
?? new HashSet() { UnicodeCategory.Control, UnicodeCategory.OtherNotAssigned, UnicodeCategory.Surrogate, UnicodeCategory.LineSeparator, UnicodeCategory.PrivateUse };
StringBuilder builder = new();
- var paragraphCount = Random.Next(minParagraphCount, maxParagraphCount);
+ var paragraphCount = Random.NextInclusive(minParagraphCount, maxParagraphCount);
for (var i = 0; i < paragraphCount; i++) {
for (var j = minSentenceCount; j <= maxSentenceCount; j++) {
diff --git a/holonsoft.AutoPoco/DataSources/Business/UrlSource.cs b/holonsoft.AutoPoco/DataSources/Business/UrlSource.cs
index 09de783..8ce9d0b 100644
--- a/holonsoft.AutoPoco/DataSources/Business/UrlSource.cs
+++ b/holonsoft.AutoPoco/DataSources/Business/UrlSource.cs
@@ -2,11 +2,26 @@
using holonsoft.AutoPoco.DataSources.Base;
using holonsoft.AutoPoco.DataSources.Primitives;
using holonsoft.AutoPoco.Engine.Interfaces;
+using holonsoft.AutoPoco.Util;
namespace holonsoft.AutoPoco.DataSources.Business;
public abstract class UrlSourceBase(int? nullCreationThreshold = null) : FixedArrayWithStringsSourceBase(nullCreationThreshold) {
- private readonly RandomStringSource _randomStringSource = new(3, 10, 'a', 'z');
+ private const string _hostPurpose = "UrlSource#host";
+
+ // an own stream, otherwise the host would follow the same sequence of draws as the top level domain
+ private readonly RandomStringSource _randomStringSource = NestedSource.Seeded(new RandomStringSource(3, 10, 'a', 'z'), _hostPurpose);
+
+ public override void SetSeedToRandomValue() {
+ base.SetSeedToRandomValue();
+ _randomStringSource.SetSeedToRandomValue();
+ }
+
+ public override void SetSeedToRandomValue(int seed) {
+ base.SetSeedToRandomValue(seed);
+ // the nested source used to keep the default seed, so every session produced the same hosts
+ _randomStringSource.SetSeedToRandomValue(NestedSource.Seed(seed, _hostPurpose));
+ }
private static readonly string[] _tlds = {
"com", "org", "net", "gov", "edu", "mil", "int", "eu", "biz", "co.uk",
@@ -25,7 +40,7 @@ protected override string GetNextValue(IGenerationContext? context) {
return null!;
}
- return $"http://www.{_randomStringSource.Next(null)}.{_tlds[Random.Next(0, _tlds.Length - 1)]}";
+ return $"http://www.{_randomStringSource.Next(null)}.{_tlds[Random.Next(_tlds.Length)]}";
}
}
diff --git a/holonsoft.AutoPoco/DataSources/Primitives/ColorSource.cs b/holonsoft.AutoPoco/DataSources/Primitives/ColorSource.cs
index 6c8f9b0..2aa9a24 100644
--- a/holonsoft.AutoPoco/DataSources/Primitives/ColorSource.cs
+++ b/holonsoft.AutoPoco/DataSources/Primitives/ColorSource.cs
@@ -2,6 +2,7 @@
using holonsoft.AutoPoco.Configuration;
using holonsoft.AutoPoco.Engine;
using holonsoft.AutoPoco.Engine.Interfaces;
+using holonsoft.AutoPoco.Util;
namespace holonsoft.AutoPoco.DataSources.Primitives;
@@ -31,9 +32,10 @@ public ColorSourceBase SetColorRange(byte rangeStart, byte rangeEnd)
protected override T GetNextValue(IGenerationContext? context)
=> (T) (object)
Color.FromArgb(
- Random.Next(RangeStartRed, RangeEndRed),
- Random.Next(RangeStartGreen, RangeEndGreen),
- Random.Next(RangeStartBlue, RangeEndBlue));
+ Alpha,
+ Random.NextInclusive(RangeStartRed, RangeEndRed),
+ Random.NextInclusive(RangeStartGreen, RangeEndGreen),
+ Random.NextInclusive(RangeStartBlue, RangeEndBlue));
}
///
diff --git a/holonsoft.AutoPoco/DataSources/Primitives/RandomStringSource.cs b/holonsoft.AutoPoco/DataSources/Primitives/RandomStringSource.cs
index 4c83ba1..1cd1453 100644
--- a/holonsoft.AutoPoco/DataSources/Primitives/RandomStringSource.cs
+++ b/holonsoft.AutoPoco/DataSources/Primitives/RandomStringSource.cs
@@ -47,7 +47,7 @@ protected override T GetNextValue(IGenerationContext? context) {
}
var result = Enumerable.Range(0, Random.Next(_minLength, _maxLength + 1))
- .Select(x => _allowedChars[Random.Next(0, _allowedChars.Length - 1)])
+ .Select(x => _allowedChars[Random.Next(_allowedChars.Length)])
.Aggregate(new StringBuilder(), (builder, c) => builder.Append(c))
.ToString();
diff --git a/holonsoft.AutoPoco/Util/NestedSource.cs b/holonsoft.AutoPoco/Util/NestedSource.cs
new file mode 100644
index 0000000..7b384b3
--- /dev/null
+++ b/holonsoft.AutoPoco/Util/NestedSource.cs
@@ -0,0 +1,31 @@
+using holonsoft.AutoPoco.Configuration;
+using holonsoft.AutoPoco.Engine.Interfaces;
+
+namespace holonsoft.AutoPoco.Util;
+
+///
+/// Helpers for data sources that drive another data source internally, such as
+/// UrlSource (the host) or ExtendedEmailAddressSource (first and last name).
+///
+///
+/// A nested source needs a stream of its own. Sharing the stream of the outer source, or of a
+/// sibling, makes the parts of a generated value follow the same sequence of draws, which is the
+/// very thing the per-member seeds of 6.0 got rid of.
+///
+internal static class NestedSource {
+ ///
+ /// Derives the seed of a nested source from the seed of the outer source and what the nested one is used for.
+ ///
+ public static int Seed(int outerSeed, string purpose)
+ => outerSeed ^ SeedDerivation.Fnv1a(purpose);
+
+ ///
+ /// Puts a freshly created nested source on its own stream, derived from the default seed. Keeps a
+ /// source used outside of a session consistent with the same source inside one.
+ ///
+ public static TSource Seeded(TSource source, string purpose)
+ where TSource : ISessionSeedable {
+ source.ApplySessionSeed(Seed(AutoPocoDefaults.Seed, purpose));
+ return source;
+ }
+}