From 7003330b1ec7a431151eb68ae12f880198cd0071 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Jul 2026 15:32:09 +0000 Subject: [PATCH] feat(dummies): add ContainingEntry to pin a dictionary entry's value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ContainingKey(TKey) forces a key in, but its value was still drawn arbitrarily from the value generator, so a test could not pin a specific key/value pair. Unlike the other containment methods this needed new state: keys flow through CollectionState, whereas values are generated per key. Add ContainingEntry(TKey, TValue): it forces the key in exactly as ContainingKey does (inheriting the out-of-domain cardinality credit) and records key -> value in an immutable, comparer-aware map that Generate consults before falling back to the value generator; With carries the map across later constraints. A duplicate key — two entries, or an entry and a ContainingKey for the same key — conflicts eagerly through the existing distinct-key validation, so no new conflict rule is introduced. The factory keeps its three-argument constructor, which now delegates to a private four-argument one, so Any.DictionaryOf is untouched. Declare it in both per-TFM PublicAPI baselines and cover it with tests: the pinned value surfacing with an out-of-domain key, and the duplicate-key conflict. Refs: #288 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_018RduXUaM3V6tk6MuKxzVzJ --- Dummies.UnitTests/AnyCollectionTests.cs | 25 +++++++++ Dummies/AnyDictionary.cs | 52 +++++++++++++++---- .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 1 + .../netstandard2.0/PublicAPI.Unshipped.txt | 1 + 4 files changed, 70 insertions(+), 9 deletions(-) diff --git a/Dummies.UnitTests/AnyCollectionTests.cs b/Dummies.UnitTests/AnyCollectionTests.cs index c62d724..ffdf047 100644 --- a/Dummies.UnitTests/AnyCollectionTests.cs +++ b/Dummies.UnitTests/AnyCollectionTests.cs @@ -364,6 +364,31 @@ public void DictionaryContainingAnyKeyForcesADrawnKey() { Check.ThatCode(() => Any.DictionaryOf(Any.Int32(), Any.Int32()).ContainingAnyKey(null!)).Throws(); } + [Fact(DisplayName = "ContainingEntry: pins the value for a required key, extending cardinality out of domain (issue #288).")] + public void DictionaryContainingEntryPinsTheValue() { + for (int i = 0; i < SampleCount; i++) { + // Key 3 is outside the key domain {1, 2} (supplied directly, extends cardinality); value 99 is outside + // the value domain {1..9}, proving it is the pinned value rather than a generated one. + Dictionary dictionary = + Any.DictionaryOf(Any.Int32().OneOf(1, 2), Any.Int32().Between(1, 9)) + .ContainingEntry(3, 99) + .WithCount(3) + .Generate(); + Check.That(dictionary.Keys).Contains(1, 2, 3); + Check.That(dictionary[3]).IsEqualTo(99); + Check.That(dictionary.Count).IsEqualTo(3); + } + } + + [Fact(DisplayName = "ContainingEntry: pinning the same key twice — or an entry and a ContainingKey — conflicts.")] + public void DictionaryContainingEntryDuplicateKeyConflicts() { + Check.ThatCode(() => Any.DictionaryOf(Any.Int32(), Any.Int32()).ContainingEntry(1, 10).ContainingEntry(1, 20)) + .Throws(); + + Check.ThatCode(() => Any.DictionaryOf(Any.Int32(), Any.Int32()).ContainingKey(1).ContainingEntry(1, 20)) + .Throws(); + } + [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 4009c6c..3225fe9 100644 --- a/Dummies/AnyDictionary.cs +++ b/Dummies/AnyDictionary.cs @@ -12,18 +12,29 @@ namespace Dummies; public sealed class AnyDictionary : IAny>, IHasRandomSource where TKey : notnull { + #region Statics members declarations + + private static readonly IReadOnlyDictionary NoPinnedValues = new Dictionary(); + + #endregion + #region Fields declarations - private readonly CollectionState _keys; - private readonly RandomSource? _source; - private readonly IAny _values; + private readonly CollectionState _keys; + private readonly IReadOnlyDictionary _pinnedValues; + private readonly RandomSource? _source; + private readonly IAny _values; #endregion - internal AnyDictionary(RandomSource? source, CollectionState keys, IAny values) { - _source = source; - _keys = keys; - _values = values; + internal AnyDictionary(RandomSource? source, CollectionState keys, IAny values) + : this(source, keys, values, NoPinnedValues) { } + + private AnyDictionary(RandomSource? source, CollectionState keys, IAny values, IReadOnlyDictionary pinnedValues) { + _source = source; + _keys = keys; + _values = values; + _pinnedValues = pinnedValues; } RandomSource? IHasRandomSource.Source => _source; @@ -112,17 +123,40 @@ public AnyDictionary ContainingAnyKey(IAny generator) { return With(_keys.WithContaining(generator, "ContainingAnyKey()")); } + /// + /// Requires the dictionary to contain the entry : the key + /// is forced in exactly as does (inheriting the out-of-domain cardinality + /// credit), and its value is pinned to instead of being drawn from the value + /// generator; the other entries stay arbitrary. Declaring two entries for the same key — or an entry and a + /// for it — conflicts, since the keys must be distinct. + /// + /// The key the generated dictionary must contain. + /// The value pinned to . + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDictionary ContainingEntry(TKey key, TValue value) { + CollectionState keys = _keys.WithContaining(key, $"ContainingEntry({AnyDerivation.Display(key)}, {AnyDerivation.Display(value)})"); + + Dictionary pinned = new(_pinnedValues.Count + 1, _keys.Comparer); + foreach (KeyValuePair entry in _pinnedValues) { pinned[entry.Key] = entry.Value; } + pinned[key] = value; + + return new AnyDictionary(_source, keys, _values, pinned); + } + /// public Dictionary Generate() { List keys = _keys.Materialize(_source ?? AmbientRandomSource.Instance); Dictionary dictionary = new(keys.Count, _keys.Comparer); - foreach (TKey key in keys) { dictionary[key] = _values.Generate(); } + foreach (TKey key in keys) { + dictionary[key] = _pinnedValues.ContainsKey(key) ? _pinnedValues[key] : _values.Generate(); + } return dictionary; } private AnyDictionary With(CollectionState keys) { - return new AnyDictionary(_source, keys, _values); + return new AnyDictionary(_source, keys, _values, _pinnedValues); } } diff --git a/Dummies/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/Dummies/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 078d195..29ae224 100644 --- a/Dummies/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/Dummies/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -117,6 +117,7 @@ Dummies.AnyDecimal.Positive() -> Dummies.AnyDecimal! Dummies.AnyDecimal.Zero() -> Dummies.AnyDecimal! Dummies.AnyDictionary Dummies.AnyDictionary.ContainingAnyKey(Dummies.IAny! generator) -> Dummies.AnyDictionary! +Dummies.AnyDictionary.ContainingEntry(TKey key, TValue value) -> Dummies.AnyDictionary! Dummies.AnyDictionary.ContainingKey(TKey key) -> Dummies.AnyDictionary! Dummies.AnyDictionary.Empty() -> Dummies.AnyDictionary! Dummies.AnyDictionary.Generate() -> System.Collections.Generic.Dictionary! diff --git a/Dummies/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/Dummies/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index de183a7..bc33737 100644 --- a/Dummies/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/Dummies/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -102,6 +102,7 @@ Dummies.AnyDecimal.Positive() -> Dummies.AnyDecimal! Dummies.AnyDecimal.Zero() -> Dummies.AnyDecimal! Dummies.AnyDictionary Dummies.AnyDictionary.ContainingAnyKey(Dummies.IAny! generator) -> Dummies.AnyDictionary! +Dummies.AnyDictionary.ContainingEntry(TKey key, TValue value) -> Dummies.AnyDictionary! Dummies.AnyDictionary.ContainingKey(TKey key) -> Dummies.AnyDictionary! Dummies.AnyDictionary.Empty() -> Dummies.AnyDictionary! Dummies.AnyDictionary.Generate() -> System.Collections.Generic.Dictionary!