Project

latticerb

0.0
The project is in a healthy, maintained state
Lattice generates deterministic pairwise/t-way covering arrays from a constrained schema of parameters and values. Use it as a library in test suites (RSpec, Minitest, Rails fixtures) or via the bundled `lattice` CLI. Output is row-for-row identical to the Python lattice-cli package for the same schema and seed.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 3.0
 Project Readme

Lattice (Ruby)

Deterministic combinatorial coverage for constrained variant surfaces — roles, permissions, feature flags, modes, providers, optional fields, component props, config matrices — as a Ruby library and CLI.

Lattice takes a schema of parameters, values, and constraints and generates a minimal covering array: the smallest practical set of scenarios in which every valid pair (or triple, at strength 3) of values appears at least once. Constraints are honored during generation, so no scenario is ever invalid.

This is the Ruby package of Lattice (lattice-cli on PyPI). Both packages generate scenarios with the same Rust engine, so they give byte-identical output for the same schema, seed and pool size. The test suite checks that by running both and comparing their output. Use whichever runtime your project already has.

Install

# Gemfile
gem "latticerb"

or standalone:

gem install latticerb

The gem ships precompiled for macOS (Apple Silicon and Intel), Linux (x86-64 and ARM64, glibc and musl) and Windows (x86-64). On other platforms it compiles the engine on install, which needs a Rust toolchain. JRuby isn't supported, because it can't load native extensions. If your Gemfile.lock was created on a Mac and you deploy to Linux, add the platform once: bundle lock --add-platform x86_64-linux.

Library usage

The primary API is designed for test suites:

require "lattice"

run = Lattice.generate(file: "spec/fixtures/checkout.yaml")

run.scenarios.each do |scenario|
  # { "payment_method" => "credit_card", "region" => "US", "card_type" => "visa", ... }
  it "checks out with #{scenario.values.join(', ')}" do
    # drive the flow with scenario values
  end
end

Schemas can also be inline Hashes or YAML/JSON strings:

run = Lattice.generate(
  "parameters" => {
    "role" => %w[admin member guest],
    "plan" => %w[free pro enterprise],
    "beta_flag" => %w[on off]
  },
  "constraints" => [
    { "type" => "invalid_pair",
      "if" => { "parameter" => "role", "value" => "guest" },
      "then_not" => { "parameter" => "plan", "value" => "enterprise" } }
  ]
)

run.scenarios        # array of { name => value } hashes
run.coverage_curve   # cumulative coverage per scenario
run.to_table         # human-readable table (also: to_json_output, to_yaml_output, to_csv, to_summary)

Lattice.scenarios(file: "...")  # shortcut straight to the hashes
Lattice.validate!(schema)       # raises Lattice::ValidationError / Lattice::ConstraintError

Generation options: strength: (default 2 = pairwise), seed: (default 42), pool_size: (candidate rows per step, default 16), max_rows:, stop_after_coverage:.

CLI usage

The gem installs a lattice executable mirroring the Python CLI:

cat schema.yaml | lattice validate
cat schema.yaml | lattice generate                 # JSON by default
lattice generate schema.yaml --format table
lattice generate schema.yaml --format yaml         # same content as JSON, for hand-edited files
lattice generate schema.yaml --strength 3 --seed 7
lattice agent instructions                          # coding-agent integration guidance

Schema format

model_name: checkout
strength: 2
parameters:
  payment_method: [credit_card, paypal, apple_pay]
  region: [US, UK, DE]
  browser:
    values: [chrome, safari, firefox]
    weights: [0.6, 0.3, 0.1]
constraints:
  - type: invalid_pair
    if: { parameter: payment_method, value: apple_pay }
    then_not: { parameter: browser, value: firefox }
  - type: conditional
    parameter: card_type
    values: [visa, mastercard]
    parent: payment_method
    applies_when: [credit_card]

Constraint types: invalid_pair, higher_order, forward_dep, bidirectional, conditional, forced. Every constraint needs a type key. Run lattice agent instructions for an example of each type.

Values keep their text

Every value in a schema (the model name, parameter names and values, and constraint references) is used exactly as written. In YAML, unquoted 1.10, 0x1F, .inf or 2026-10-26T10:00:00Z stay as typed rather than being turned into numbers or dates. The exceptions:

  • Unquoted yes, no, on, off, true and false (lowercase, Capitalized or UPPERCASE) become true / false, and an empty value, ~ or null becomes null. Quote them ("yes") to keep the word.
  • JSON integers are written in plain decimal form (-0 becomes 0).

strength must be a whole number, and weights decimal numbers such as 0.7 or 1e-3. Anchors, aliases and << merge keys work; a key written twice, more than one YAML document, or a tag other than YAML's standard ones is an error.

Changed in 0.3.0: earlier versions converted unquoted numbers and dates, so 0x1F became 31, 1.10 became 1.1, and some values (.inf, timestamps) came out differently in the Ruby and Python implementations.

Performance

Generation runs in a Rust engine (the lattice-engine crate), in two phases:

  1. Build. Rows are added greedily. Each row is the best of several candidates (pool_size:, default 16), and each candidate picks, value by value, whatever covers the most combinations still uncovered.
  2. Shrink. A row is removed and the gaps are repaired with tabu search, which changes values in the remaining rows without breaking any constraint or forced combination. This repeats while repairs succeed within a fixed step budget.

The budget is a number of steps, not a time limit, so the same schema and seed give the same rows on every machine. The rows come out ordered so each adds the most new coverage, which is why max_rows: and stop_after_coverage: cut the set short with as much coverage as possible.

Scenario counts against PICT 3.7, with default settings for both:

Model PICT Lattice 0.4 Lattice 0.3
13 parameters × 3 values 20 15 (best known) 22
100 × 2 16 10 (optimal) over 60 s
61 mixed (4^15 3^17 2^29) 40 30 over 60 s
12 × 6 70 60 88 (32 s)
30 × 5 66 55 over 60 s
20 × 10 218 192 over 60 s
Strength 3, 10 × 4 158 140 176 (27 s)

Each Lattice 0.4 run above takes 0.04–1.2 s on an Apple M-series laptop. PICT is faster (about 0.02 s) because it stops after the build phase.

Changed in 0.4.0: rows differ from 0.3.x for the same seed, and every example needs the same number of scenarios or fewer. pool_size: now sets the candidates per row; its default went from 50 to 16.

Parity with the Python implementation

Both packages parse schemas in their own language and hand the engine the same compiled constraints (ConstraintEngine#engine_constraints here, lattice.ipog in Python), so the engine gets identical input. test/python_parity_test.rb shells out to the Python implementation (checkout via LATTICE_PYTHON_SRC, or an installed lattice-cli) and asserts byte-equal output across formats, seeds, strengths and stopping rules; it skips when no Python implementation is available.

Schemas are read with Psych here and PyYAML in Python, but both implementations build values from the parsed text with the same rules (see "Values keep their text"), so a schema means the same thing in either. test/fixtures/scalar_text.* pins those rules for both.

Development

Building the extension needs a Rust toolchain (rustup); the engine comes from crates.io (lattice-engine).

bundle install
bundle exec rake compile     # builds lib/lattice/lattice_native
bundle exec rake test        # unit, golden-parity, and live Python-parity suites (compiles first)
bundle exec mutant run       # mutation testing (see docs/mutation-testing.md)

Golden fixtures are authored by the reference Python implementation; regenerate them with ruby test/support/generate_goldens.rb after intentional behavior changes (requires the Python checkout, see the script header).

License

Apache-2.0