CaseSpace: Deterministic Combinatorics for Testing Without Randomness
When fuzzing meets formalism — and every bug gets a number.
There’s a certain kind of satisfaction that comes from a test failure that doesn’t say
“sometimes this breaks”
but instead says:
CASE 729 failed. Re-run with id=729.
That’s not randomness.
That’s mathematics pointing directly at the problem.
Today I want to introduce a small but powerful testing utility I’ve been using across several projects — a mixed-radix case generator that turns large combinatorial test spaces into a deterministic, reproducible “case lattice.”
It’s fast, portable, thread-safe, and friendly to both humans and machines.
I call it CaseSpace.
What is CaseSpace?
CaseSpace is a tiny library that models a test domain as a sequence of symbolic digits, where each digit has its own radix.
Think of it as a number system where:
-
the first digit might be
Square | Wide | Tall -
the second digit might be
< | = | > -
the third digit might be
Whole | TopHalf | LeftHalf | RightHalf | Center -
the fourth digit might be
Whole | Within | TouchEdge | PartOff | Outside
Instead of base-10 or base-2, you get a jagged number system where each position has its own alphabet.
Every combination becomes a unique integer.
Every integer becomes a unique test case.
Why not just use random?
Random is great for exploration.
Deterministic is great for engineering.
Random says:
“Sometimes this breaks.”
CaseSpace says:
“CASE 729 breaks. Here is exactly what it was.”
And once you fix CASE 729, it never breaks again.
That’s the difference between fuzzing and verification.
The Core Idea
You define your test space as a sequence of named places:
var g = new CaseSpace()
.AddPlace("srcShape", "Square", "Wide", "Tall")
.AddPlace("dstShape", "Square", "Wide", "Tall")
.AddPlace("wRel", "<", "=", ">")
.AddPlace("hRel", "<", "=", ">")
.AddPlace("srcRect", "Whole", "TopHalf", "LeftHalf", "RightHalf", "Center")
.AddPlace("clip", "Whole", "Within", "TouchEdge", "PartOff", "Outside")
.Seal();
Now your test space is:
3 × 3 × 3 × 3 × 5 × 5 = 2025 unique cases
Each one has a stable integer ID:
var c = g[729];
Console.WriteLine(c.ToNamedString());
Output:
CASE 729: srcShape=Square dstShape=Square wRel=< hRel=> srcRect=Center clip=Within
And it round-trips:
long id = g.GetCaseId(c.Digits); // -> 729
What makes it special?
1) Mixed-radix, not base-N
Each place can have a different number of options.
This makes it ideal for real systems where:
-
some parameters are binary,
-
some are ternary,
-
some have five meaningful modes.
2) Deterministic and reproducible
Every case is an integer.
Every integer maps to exactly one configuration.
That means:
-
perfect repros
-
stable regression tests
-
portable failure reports
You can paste a single number into a bug report and regenerate the entire scenario.
3) Friendly to logs and humans
Each case supports:
-
ordered digits
-
named digits
-
compact string format
c.ToCompact();
// "Square|Square|<|>|Center|Within"
c.ToNamedString();
// CASE 729: srcShape=Square dstShape=Square wRel=< hRel=> srcRect=Center clip=Within
That makes failures readable even outside your test runner.
4) Thread-safe by design
Once constructed, the generator is immutable.
You can safely do:
Parallel.For(0, g.CaseCount, id => RunCase(g[id]));
Which turns CaseSpace into a seam verifier for multithreaded systems.
Where is this useful?
CaseSpace shines anywhere the input space is combinatorial:
Graphics pipelines
-
scaling modes
-
clipping modes
-
coordinate spaces
-
threading seams
Parsers and compilers
-
grammar tiers
-
token classes
-
precedence modes
-
error recovery strategies
Serialization formats
-
endianness
-
versioning
-
optional fields
-
compression modes
Protocol stacks
-
framing modes
-
encoding modes
-
compression
-
encryption
Anywhere your test matrix looks like:
“This parameter times that parameter times those five modes times those three shapes…”
CaseSpace gives you:
-
full coverage
-
without randomness
-
without flakiness
-
without guesswork
The Philosophy
This is not just a test utility.
It’s a way of thinking about systems as finite symbolic spaces.
Instead of:
“Let’s throw random inputs at it.”
You say:
“Let’s enumerate the structure of the system itself.”
That’s closer to how compilers, renderers, and engines are actually designed.
And when something breaks?
You don’t get a shrug.
You get a number.
The Code
Library source:
// CaseSpace.cs
// .NET 8+ single-file utility for deterministic mixed-radix case generation.
//
// Goals:
// - Deterministic decode: caseId -> labeled digits (string tokens) across multiple "places"
// - Deterministic encode: labeled digits -> caseId
// - Mixed radices (each place can have different number of choices)
// - Human-friendly compact string repr for logging & repros
// - Fast enough for tight test loops; avoids allocations unless you ask for Named map.
//
// Example:
//
// var g = new CaseSpace()
// .AddPlace("srcShape", "Square", "Wide", "Tall")
// .AddPlace("dstShape", "Square", "Wide", "Tall")
// .AddPlace("wRel", "<", "=", ">")
// .AddPlace("hRel", "<", "=", ">")
// .AddPlace("srcRect", "Whole", "TopHalf", "LeftHalf", "RightHalf", "Center")
// .AddPlace("clip", "Whole", "Within", "TouchEdge", "PartOff", "Outside")
// .Seal();
//
// var c = g[729];
// Console.WriteLine($"{c}");
// Console.WriteLine(c.ToCompact());
// var id2 = g.GetCaseId(c.Digits);
// Console.WriteLine(id2);
//
// Thread-safety:
// - Builder methods mutate generator (not thread-safe).
// - After construction, decode/encode are thread-safe (immutable internal state).
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
public sealed class CaseSpace
{
private readonly List<Place> _places = new();
private bool _sealed;
private string[] _PlaceNames = null;
public IReadOnlyList<Place> Places => _places.AsReadOnly();
/// <summary>Total number of cases in the space (product of all radices).</summary>
public long CaseCount { get; private set; } = 1;
public string[] PlaceNames
{
get
{
if (!_sealed) return null;
if (_PlaceNames is null) _PlaceNames = _places.Select(place => place.Name).ToArray();
return _PlaceNames;
}
}
/// <summary>
/// Add a place with a name and ordered set of string labels (digits).
/// Digits must be unique within the place.
/// </summary>
public CaseSpace AddPlace(string name, params string[] digits)
{
if (_sealed) throw new InvalidOperationException("Generator is sealed; cannot add places.");
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Place name required.", nameof(name));
if (digits is null || digits.Length == 0) throw new ArgumentException("Place must have at least one digit.", nameof(digits));
// Validate uniqueness & non-null
var seen = new HashSet<string>(StringComparer.Ordinal);
for (int i = 0; i < digits.Length; i++)
{
if (digits[i] is null) throw new ArgumentException($"Digit at index {i} is null.", nameof(digits));
if (!seen.Add(digits[i]))
throw new ArgumentException($"Duplicate digit '{digits[i]}' in place '{name}'. Digits must be unique (Ordinal).", nameof(digits));
}
var place = new Place(name, digits);
_places.Add(place);
// Update case count (checked)
checked
{
CaseCount *= place.Radix;
}
return this;
}
/// <summary>
/// Optionally lock the generator configuration; decode/encode still work.
/// </summary>
public CaseSpace Seal()
{
_sealed = true;
return this;
}
/// <summary>
/// Decode caseId into a Case. If wrap==true, caseId is reduced modulo CaseCount.
/// </summary>
public Case Decode(long caseId, bool wrap = false)
{
EnsurePlaces();
long id = caseId;
if (wrap)
{
id = Mod(caseId, CaseCount);
}
else
{
if (caseId < 0 || caseId >= CaseCount)
throw new ArgumentOutOfRangeException(nameof(caseId), caseId,
$"caseId must be in [0, {CaseCount - 1}] for this generator.");
}
var digitIdx = new int[_places.Count];
var digitStr = new string[_places.Count];
long t = id;
for (int p = 0; p < _places.Count; p++)
{
var place = _places[p];
int d = (int)(t % place.Radix);
t /= place.Radix;
digitIdx[p] = d;
digitStr[p] = place.Digits[d];
}
return new Case(this, id, digitIdx, digitStr);
}
/// <summary>
/// Try decode caseId into a Case. If wrap==true, decodes modulo CaseCount.
/// </summary>
public bool TryDecode(long caseId, out Case result, bool wrap = false)
{
try
{
result = Decode(caseId, wrap);
return true;
}
catch
{
result = default!;
return false;
}
}
/// <summary>
/// Indexer sugar: g[caseId] == g.Decode(caseId).
/// </summary>
public Case this[long caseId] => Decode(caseId, wrap: false);
/// <summary>
/// Encode a case from an ordered array of digit labels (one per place) into a caseId.
/// </summary>
public long GetCaseId(IReadOnlyList<string> digits)
{
EnsurePlaces();
if (digits is null) throw new ArgumentNullException(nameof(digits));
if (digits.Count != _places.Count)
throw new ArgumentException($"Expected { _places.Count } digits (one per place), got {digits.Count}.", nameof(digits));
long id = 0;
long factor = 1;
checked
{
for (int p = 0; p < _places.Count; p++)
{
var place = _places[p];
string label = digits[p] ?? throw new ArgumentException($"Digit at index {p} is null.", nameof(digits));
int idx = place.IndexOf(label);
if (idx < 0)
throw new ArgumentException($"Digit '{label}' not valid for place '{place.Name}'.", nameof(digits));
id += factor * idx;
factor *= place.Radix;
}
}
return id;
}
/// <summary>
/// Encode from an ordered array of digit indices (one per place) into a caseId.
/// </summary>
public long GetCaseIdFromIndices(IReadOnlyList<int> digitIndices)
{
EnsurePlaces();
if (digitIndices is null) throw new ArgumentNullException(nameof(digitIndices));
if (digitIndices.Count != _places.Count)
throw new ArgumentException($"Expected { _places.Count } indices (one per place), got {digitIndices.Count}.", nameof(digitIndices));
long id = 0;
long factor = 1;
checked
{
for (int p = 0; p < _places.Count; p++)
{
var place = _places[p];
int idx = digitIndices[p];
if ((uint)idx >= (uint)place.Radix)
throw new ArgumentOutOfRangeException(nameof(digitIndices), idx,
$"Index {idx} out of range for place '{place.Name}' radix {place.Radix}.");
id += factor * idx;
factor *= place.Radix;
}
}
return id;
}
/// <summary>
/// Parse a compact string (e.g. "Square|Wide|<|>|Whole|Within") into a caseId.
/// </summary>
public long ParseCompact(string compact, string separator = "|")
{
EnsurePlaces();
if (compact is null) throw new ArgumentNullException(nameof(compact));
if (separator is null) throw new ArgumentNullException(nameof(separator));
if (separator.Length == 0) throw new ArgumentException("Separator cannot be empty.", nameof(separator));
var parts = compact.Split(separator, StringSplitOptions.None);
return GetCaseId(parts);
}
/// <summary>
/// Enumerate case ids [start, start+count) optionally filtered by a predicate over decoded cases.
/// If wrap==true, ids are modulo CaseCount.
/// </summary>
public IEnumerable<long> Enumerate(long start, long count, bool wrap = false, Func<Case, bool>? filter = null)
{
EnsurePlaces();
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count));
for (long i = 0; i < count; i++)
{
long id = start + i;
var c = Decode(id, wrap);
if (filter == null || filter(c))
yield return c.Id;
}
}
private void EnsurePlaces()
{
if (_places.Count == 0) throw new InvalidOperationException("No places defined. Call AddPlace(...) first.");
}
private static long Mod(long a, long m)
{
if (m <= 0) throw new ArgumentOutOfRangeException(nameof(m));
long r = a % m;
return r < 0 ? r + m : r;
}
// ---------------------- Nested Types ----------------------
public sealed class Place
{
private readonly Dictionary<string, int> _index;
public string Name { get; }
public IReadOnlyList<string> Digits { get; }
public int Radix => Digits.Count;
public Place(string name, IReadOnlyList<string> digits)
{
Name = name;
Digits = digits.ToArray(); // defensive copy
_index = new Dictionary<string, int>(StringComparer.Ordinal);
for (int i = 0; i < Digits.Count; i++)
_index[Digits[i]] = i;
}
public int IndexOf(string digit) => _index.TryGetValue(digit, out var i) ? i : -1;
public override string ToString() => $"{Name}[{Radix}]";
}
public readonly struct Case
{
private readonly CaseSpace _space;
private readonly int[] _digitIndices;
private readonly string[] _digits;
public long Id { get; }
/// <summary>Ordered digit labels, one per place (same order as generator.Places).</summary>
public IReadOnlyList<string> Digits => _digits;
/// <summary>Ordered digit indices, one per place.</summary>
public IReadOnlyList<int> DigitIndices => _digitIndices;
/// <summary>Lazy: map placeName -> digitLabel for debug / printing.</summary>
public IReadOnlyDictionary<string, string> Named => BuildNamed();
internal Case(CaseSpace space, long id, int[] digitIndices, string[] digits)
{
_space = space;
Id = id;
_digitIndices = digitIndices;
_digits = digits;
}
public string ToCompact(string separator = "|") => string.Join(separator, _digits);
/// <summary>Human readable: "CASE 729: srcShape=\"Square\" dstShape=\"Wide\" ..."</summary>
public string ToNamedString(string kvSeparator = "=", string pairSeparator = " ")
{
if (_space is null) return $"CASE {Id}: <unbound>";
var places = _space._places;
var parts = new string[places.Count];
for (int i = 0; i < places.Count; i++)
parts[i] = places[i].Name + kvSeparator + "\"" + _digits[i].Replace("\"", "\\\"") + "\"";
return $"CASE {Id}: " + string.Join(pairSeparator, parts);
}
public override string ToString() => ToNamedString();
private IReadOnlyDictionary<string, string> BuildNamed()
{
if (_space is null) return new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
var dict = new Dictionary<string, string>(_space._places.Count, StringComparer.Ordinal);
for (int i = 0; i < _space._places.Count; i++)
dict[_space._places[i].Name] = _digits[i];
return new ReadOnlyDictionary<string, string>(dict);
}
}
}
// Optional tiny demo you can delete:
//
// public static class Demo
// {
// public static void Main()
// {
// var g = new CaseSpace()
// .AddPlace("srcShape", "Square", "Wide", "Tall")
// .AddPlace("dstShape", "Square", "Wide", "Tall")
// .AddPlace("wRel", "<", "=", ">")
// .AddPlace("hRel", "<", "=", ">")
// .AddPlace("srcRect", "Whole", "TopHalf", "LeftHalf", "RightHalf", "Center")
// .AddPlace("clip", "Whole", "Within", "TouchEdge", "PartOff", "Outside")
// .Seal();
//
// var c = g[729];
// Console.WriteLine(c.ToNamedString());
// Console.WriteLine("Compact: " + c.ToCompact());
// Console.WriteLine("Round-trip id: " + g.GetCaseId(c.Digits));
//
// long id2 = g.ParseCompact("Square|Square|<|>|Center|Within");
// Console.WriteLine("Parsed id: " + id2);
// }
// }
Unit tests:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Xunit;
public class CaseSpaceTests
{
private static CaseSpace CreateTestGenerator()
{
return new CaseSpace()
.AddPlace("srcShape", "Square", "Wide", "Tall")
.AddPlace("dstShape", "Square", "Wide", "Tall")
.AddPlace("wRel", "<", "=", ">")
.AddPlace("hRel", "<", "=", ">")
.AddPlace("srcRect", "Whole", "TopHalf", "LeftHalf", "RightHalf", "Center")
.AddPlace("clip", "Whole", "Within", "TouchEdge", "PartOff", "Outside")
.Seal();
}
[Fact]
public void BasicDecoding_ReturnsCorrectCase()
{
var g = CreateTestGenerator();
// Test case 0 (all first choices)
var c0 = g[0];
Assert.Equal(0, c0.Id);
Assert.Equal("Square", c0.Digits[0]);
Assert.Equal("Square", c0.Digits[1]);
Assert.Equal("<", c0.Digits[2]);
Assert.Equal("<", c0.Digits[3]);
Assert.Equal("Whole", c0.Digits[4]);
Assert.Equal("Whole", c0.Digits[5]);
// Test case 729
var c729 = g[729];
Assert.Equal(729, c729.Id);
Assert.Equal(6, c729.Digits.Count);
Assert.Equal(6, c729.DigitIndices.Count);
// Test total case count (3*3*3*3*5*5 = 2025)
Assert.Equal(2025, g.CaseCount);
}
[Fact]
public void EncodingFromDigits_RoundTripsCorrectly()
{
var g = CreateTestGenerator();
var c = g[729];
var id2 = g.GetCaseId(c.Digits);
Assert.Equal(729, id2);
// Test specific combination
var digits = new[] { "Square", "Wide", "<", ">", "Center", "Within" };
var id = g.GetCaseId(digits);
var decoded = g[id];
Assert.Equal(digits, decoded.Digits);
}
[Fact]
public void EncodingFromIndices_RoundTripsCorrectly()
{
var g = CreateTestGenerator();
var c = g[729];
var id2 = g.GetCaseIdFromIndices(c.DigitIndices);
Assert.Equal(729, id2);
// Test specific indices
var indices = new[] { 0, 1, 0, 2, 4, 1 }; // Square, Wide, <, >, Center, Within
var id = g.GetCaseIdFromIndices(indices);
var decoded = g[id];
for (int i = 0; i < indices.Length; i++)
{
Assert.Equal(indices[i], decoded.DigitIndices[i]);
}
}
[Fact]
public void CompactStringParsing_RoundTripsCorrectly()
{
var g = CreateTestGenerator();
var c = g[729];
var compact = c.ToCompact();
var id2 = g.ParseCompact(compact);
Assert.Equal(729, id2);
// Test specific compact string
var testCompact = "Square|Square|<|>|Center|Within";
var id = g.ParseCompact(testCompact);
var decoded = g[id];
Assert.Equal(testCompact, decoded.ToCompact());
// Test custom separator
var customCompact = "Wide-Square-<-<-Whole-Whole";
var id3 = g.ParseCompact(customCompact, "-");
Assert.Equal(1, id3);
}
[Fact]
public void Enumeration_ProducesCorrectSequence()
{
var g = CreateTestGenerator();
// Enumerate first 10 cases
var cases = g.Enumerate(0, 10).ToList();
Assert.Equal(10, cases.Count);
Assert.Equal(0, cases[0]);
Assert.Equal(9, cases[9]);
// Enumerate with filter
var filtered = g.Enumerate(0, 100, filter: c => c.Digits[0] == "Square").ToList();
Assert.All(filtered, id => Assert.Equal("Square", g[id].Digits[0]));
}
[Fact]
public void WrapMode_HandlesOutOfRangeCaseIds()
{
var g = CreateTestGenerator();
// Negative wrap
var c1 = g.Decode(-1, wrap: true);
Assert.Equal(g.CaseCount - 1, c1.Id);
// Overflow wrap
var c2 = g.Decode(g.CaseCount, wrap: true);
Assert.Equal(0, c2.Id);
// Multiple wraps
var c3 = g.Decode(g.CaseCount + 729, wrap: true);
Assert.Equal(729, c3.Id);
// Without wrap should throw
Assert.Throws<ArgumentOutOfRangeException>(() => g.Decode(-1, wrap: false));
Assert.Throws<ArgumentOutOfRangeException>(() => g.Decode(g.CaseCount, wrap: false));
}
[Fact]
public void Validation_ThrowsOnInvalidInputs()
{
var g = CreateTestGenerator();
// Invalid digit in GetCaseId
Assert.Throws<ArgumentException>(() => g.GetCaseId(new[] { "Invalid", "Square", "<", "<", "Whole", "Whole" }));
// Wrong number of digits
Assert.Throws<ArgumentException>(() => g.GetCaseId(new[] { "Square", "Wide" }));
// Invalid index
Assert.Throws<ArgumentOutOfRangeException>(() => g.GetCaseIdFromIndices(new[] { 0, 0, 0, 0, 0, 99 }));
// Null digits
Assert.Throws<ArgumentException>(() => g.GetCaseId(new string[] { null!, "Square", "<", "<", "Whole", "Whole" }));
// Sealed generator
var _sealed = CreateTestGenerator();
Assert.Throws<InvalidOperationException>(() => _sealed.AddPlace("extra", "A", "B"));
// Empty generator
var empty = new CaseSpace();
Assert.Throws<InvalidOperationException>(() => empty[0]);
// Duplicate digits in place
var builder = new CaseSpace();
Assert.Throws<ArgumentException>(() => builder.AddPlace("test", "A", "B", "A"));
}
[Fact]
public void NamedDictionary_ProvidesCorrectMapping()
{
var g = CreateTestGenerator();
var c = g[729];
var named = c.Named;
Assert.Equal(6, named.Count);
Assert.Equal(c.Digits[0], named["srcShape"]);
Assert.Equal(c.Digits[1], named["dstShape"]);
Assert.Equal(c.Digits[2], named["wRel"]);
Assert.Equal(c.Digits[3], named["hRel"]);
Assert.Equal(c.Digits[4], named["srcRect"]);
Assert.Equal(c.Digits[5], named["clip"]);
// Test ToNamedString output
var str = c.ToNamedString();
Assert.Contains("CASE 729:", str);
Assert.Contains("srcShape=", str);
Assert.Contains("clip=", str);
}
}
Closing
CaseSpace was born while building a multithreaded integer blitter.
It found real bugs.
It verified real fixes.
And now it lives as a small, sharp tool I reach for whenever a system starts to grow dimensions.
It’s not fancy.
It’s not big.
But it turns complexity into something you can count.
And sometimes, that’s exactly what engineering needs.