diff --git a/Dummies.UnitTests/AnyCollectionTests.cs b/Dummies.UnitTests/AnyCollectionTests.cs index 6b1e2de8..6fac877c 100644 --- a/Dummies.UnitTests/AnyCollectionTests.cs +++ b/Dummies.UnitTests/AnyCollectionTests.cs @@ -158,9 +158,8 @@ public void ContainingOutsideDomainExtendsCardinality() { } // The same reasoning holds for a distinct list and for several out-of-domain values at once. Dictionary keys - // run through the very same CollectionState path, so the correction reaches them too — but AnyDictionary has - // no Containing surface, so its keys are gated purely by count (see DictionaryOfBehaves) and cannot exercise - // the out-of-domain case directly. + // run through the very same CollectionState path, so the correction reaches them too, now exercised directly + // through AnyDictionary.ContainingKey (see DictionaryContainingKeyOutsideDomainExtendsCardinality). for (int i = 0; i < SampleCount; i++) { HashSet list = new(Any.ListOf(Any.Int32().OneOf(1, 2)).Containing(3).WithCount(3).Distinct().Generate()); Check.That(list).Contains(1, 2, 3); @@ -321,6 +320,37 @@ public void DictionaryOfBehaves() { Check.ThatCode(() => Any.DictionaryOf(null!, Any.Int32())).Throws(); } + [Fact(DisplayName = "ContainingKey: a key outside the key domain extends the effective cardinality (issue #225).")] + public void DictionaryContainingKeyOutsideDomainExtendsCardinality() { + // Mirrors ContainingOutsideDomainExtendsCardinality on the dictionary surface: {1, 2} is all the key + // generator can produce, and 3 is supplied directly from outside that domain, so a three-entry dictionary is + // satisfiable — the out-of-domain cardinality-credit path AnyDictionary could not exercise before. + for (int i = 0; i < SampleCount; i++) { + Dictionary dictionary = + Any.DictionaryOf(Any.Int32().OneOf(1, 2), Any.String().NonEmpty()).ContainingKey(3).WithCount(3).Generate(); + Check.That(dictionary.Keys).Contains(1, 2, 3); + Check.That(dictionary.Count).IsEqualTo(3); + Check.That(dictionary.Values).ContainsOnlyElementsThatMatch(value => value.Length > 0); + } + } + + [Fact(DisplayName = "ContainingKey: a within-domain key is present, an out-of-capacity one still conflicts eagerly.")] + public void DictionaryContainingKeyInsideDomainDoesNotInflate() { + // A within-domain fixed key is present and adds no capacity of its own. + for (int i = 0; i < SampleCount; i++) { + Dictionary dictionary = + Any.DictionaryOf(Any.Int32().Between(1, 9), Any.String().NonEmpty()).WithCount(5).ContainingKey(7).Generate(); + Check.That(dictionary.ContainsKey(7)).IsTrue(); + Check.That(dictionary.Count).IsEqualTo(5); + } + + // 1 is already producible, so three distinct keys over {1, 2} remain impossible and still fail eagerly, + // naming the shortfall — exactly as ContainingInsideDomainDoesNotInflate asserts for a set. + ConflictingAnyConstraintException conflict = Assert.Throws( + () => Any.DictionaryOf(Any.Int32().OneOf(1, 2), Any.String().NonEmpty()).ContainingKey(1).WithCount(3)); + Check.That(conflict.Message).Contains("2 distinct value"); + } + [Fact(DisplayName = "PairOf and TripleOf assemble value tuples from constrained parts.")] public void PairAndTriple() { for (int i = 0; i < SampleCount; i++) { diff --git a/Dummies/AnyDictionary.cs b/Dummies/AnyDictionary.cs index c2f16240..38933ae1 100644 --- a/Dummies/AnyDictionary.cs +++ b/Dummies/AnyDictionary.cs @@ -80,6 +80,21 @@ public AnyDictionary WithCountBetween(int minimum, int maximum) { return With(CountConstraints.WithCountBetween(_keys, minimum, maximum)); } + /// + /// Requires the dictionary to contain an entry for . May be declared several times; + /// each required key takes one entry's room and the required keys must be distinct. A key outside the key + /// generator's domain extends the effective cardinality exactly as 's containment + /// does, so an otherwise impossible entry count becomes reachable; the entry's value is generated like any + /// other. Named ContainingKey rather than a bare Containing so the surface reads unambiguously + /// on a dictionary, whose elements are key/value pairs. + /// + /// The key the generated dictionary must contain. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDictionary ContainingKey(TKey key) { + return With(_keys.WithContaining(key, $"ContainingKey({AnyDerivation.Display(key)})")); + } + /// public Dictionary Generate() { List keys = _keys.Materialize(_source ?? AmbientRandomSource.Instance); diff --git a/Dummies/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/Dummies/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 2d64bbdb..913325c5 100644 --- a/Dummies/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/Dummies/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -116,6 +116,7 @@ Dummies.AnyDecimal.OneOf(params decimal[]! values) -> Dummies.AnyDecimal! Dummies.AnyDecimal.Positive() -> Dummies.AnyDecimal! Dummies.AnyDecimal.Zero() -> Dummies.AnyDecimal! Dummies.AnyDictionary +Dummies.AnyDictionary.ContainingKey(TKey key) -> Dummies.AnyDictionary! Dummies.AnyDictionary.Empty() -> Dummies.AnyDictionary! Dummies.AnyDictionary.Generate() -> System.Collections.Generic.Dictionary! Dummies.AnyDictionary.NonEmpty() -> Dummies.AnyDictionary! diff --git a/Dummies/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/Dummies/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 9f341225..5bc982ea 100644 --- a/Dummies/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/Dummies/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -101,6 +101,7 @@ Dummies.AnyDecimal.OneOf(params decimal[]! values) -> Dummies.AnyDecimal! Dummies.AnyDecimal.Positive() -> Dummies.AnyDecimal! Dummies.AnyDecimal.Zero() -> Dummies.AnyDecimal! Dummies.AnyDictionary +Dummies.AnyDictionary.ContainingKey(TKey key) -> Dummies.AnyDictionary! Dummies.AnyDictionary.Empty() -> Dummies.AnyDictionary! Dummies.AnyDictionary.Generate() -> System.Collections.Generic.Dictionary! Dummies.AnyDictionary.NonEmpty() -> Dummies.AnyDictionary!