Check tables and doc strings
This guide shows you how to check tabular and multi-line expectations. In Varar a
step may return a value; Varar compares it against what the Markdown claims and
anchors any mismatch to the exact cell or character span. There are three
shapes.
Check a table row by row
Section titled “Check a table row by row”Use this when each row of a table is an independent example. Write the table under a sentence that names its columns:
Each row gives an example of a decimal and a roman number:
| decimal | roman || ------: | :---- || 3 | III || 9 | IX || 40 | XL |Bind a sensor to that sentence. It receives one row at a time as an object keyed by the header, and returns the computed columns:
import { toRoman } from './roman-numerals'
sensor('a decimal and a roman number', (_state, row: { decimal: string; roman: string }) => { return { decimal: row.decimal, roman: toRoman(Number(row.decimal)) }})s.sensor("a decimal and a roman number", (Ctx ctx, Map<String, String> row) -> Map.of( "decimal", row.get("decimal"), "roman", RomanNumerals.toRoman(Integer.parseInt(row.get("decimal")))));sensor("a decimal and a roman number") { row: Map<String, String> -> mapOf("decimal" to row.getValue("decimal"), "roman" to toRoman(row.getValue("decimal").toInt()))}from roman_numerals_example import to_roman
@sensor("a decimal and a roman number")def _(state, row): return {"decimal": row["decimal"], "roman": to_roman(int(row["decimal"]))}sensor('a decimal and a roman number') do |_state, row| { 'decimal' => row['decimal'], 'roman' => RomanNumerals.to_roman(row['decimal'].to_i) }ends.sensor( "a decimal and a roman number", file!(), line!() as usize, Handler::sync1(|_state, row| { let m = smap(&row); let decimal = as_str(&m["decimal"]); let roman = to_roman(decimal.parse().expect("decimal")); Ok(Some(vmap(vec![ ("decimal", Value::from(decimal)), ("roman", Value::from(roman)), ]))) }),);s.Sensor("a decimal and a roman number", (state, row) =>{ var decimal = SMap(row)["decimal"].AsString(); var roman = RomanNumerals.ToRoman(int.Parse(decimal)); return VMap(("decimal", Value.Of(decimal)), ("roman", Value.Of(roman)));});s.Sensor("a decimal and a roman number", func(state varar.Value, args []varar.Value) (*varar.Value, error) { row := args[0].CloneMap() decimal := row["decimal"].MustString() n, _ := strconv.Atoi(decimal) return varar.Ptr(varar.MapValue(map[string]varar.Value{ "decimal": varar.StrValue(decimal), "roman": varar.StrValue(toRoman(n)), })), nil})Each row runs as its own check. If toRoman(9) returned "VIIII", only the
IX cell fails — with expected and actual, anchored to that cell.
Check a whole table at once
Section titled “Check a whole table at once”Use this when the table is one expectation, not a list of independent rows. The sensor receives the full table as a list of rows (header row first) and returns the reproduced table:
Uppercase each one:
| before | after || ------ | ----- || vár | VÁR || bdd | BDD |sensor('Uppercase each one:', (_state, rows: ReadonlyArray<ReadonlyArray<string>>) => { return rows.slice(1).map(([before]) => ({ before, after: (before ?? '').toUpperCase() }))})s.sensor("Uppercase each one:", (Ctx ctx, List<List<String>> rows) -> { List<Map<String, String>> out = new ArrayList<>(); for (List<String> row : rows.subList(1, rows.size())) { out.add(Map.of("before", row.get(0), "after", row.get(0).toUpperCase(Locale.ROOT))); } return out;});sensor("Uppercase each one:") { rows: List<List<String>> -> rows.drop(1).map { row -> mapOf("before" to row[0], "after" to row[0].uppercase()) }}@sensor("Uppercase each one:")def _(state, rows): return [{"before": before, "after": before.upper()} for before, *_ in rows[1:]]sensor('Uppercase each one:') do |_state, rows| rows[1..].map { |before, *| { 'before' => before, 'after' => before.upcase } }ends.sensor( "Uppercase each one:", file!(), line!() as usize, Handler::sync1(|_state, table| { let Value::List(rows) = table else { panic!("expected a table") }; let out: Vec<Value> = rows .iter() .skip(1) // drop the header row .map(|row| { let Value::List(cells) = row else { panic!("expected a row") }; let before = as_str(&cells[0]); vmap(vec![ ("before", Value::from(before.clone())), ("after", Value::from(before.to_uppercase())), ]) }) .collect(); Ok(Some(Value::List(out))) }),);s.Sensor("Uppercase each one:", (state, table) =>{ var computed = ((VList)table).Items.Skip(1).Select(row => { var before = ((VList)row).Items[0].AsString(); return VMap(("before", Value.Of(before)), ("after", Value.Of(before.ToUpperInvariant()))); }); return Value.List(computed);});s.Sensor("Uppercase each one:", func(state varar.Value, args []varar.Value) (*varar.Value, error) { rows, _ := args[0].AsList() out := []varar.Value{} for _, row := range rows[1:] { // drop the header row cells, _ := row.AsList() before := cells[0].MustString() out = append(out, varar.MapValue(map[string]varar.Value{ "before": varar.StrValue(before), "after": varar.StrValue(strings.ToUpper(before)), })) } return varar.Ptr(varar.ListOf(out)), nil})The table fills this sensor’s only slot, so it is returned bare. Varar compares every cell of the returned table against the source, as exact strings.
Check a doc string
Section titled “Check a doc string”Use this for multi-line text: rendered output, error messages, generated files. Write the expected text as a fenced code block:
Greet Bob:
```textHello, Bob!```The step receives the block’s text as a trailing string argument, and returns the text the software actually produces:
sensor('Greet {word}:', (_state, name, _body: string) => { return [name, `Hello, ${name}!\n`]})s.sensor("Greet {word}:", (Ctx ctx, String name, String doc) -> List.of(name, "Hello, " + name + "!\n"));sensor("Greet {word}:") { name: String, _: String -> listOf(name, "Hello, $name!\n")}@sensor("Greet {word}:")def _(state, name, doc): return [name, f"Hello, {name}!\n"]sensor('Greet {word}:') { |_state, name, _doc| [name, "Hello, #{name}!\n"] }s.sensor( "Greet {word}:", file!(), line!() as usize, Handler::sync2(|_state, name, _doc| { let name = as_str(&name); Ok(Some(Value::List(vec![ Value::from(name.clone()), Value::from(format!("Hello, {name}!\n")), ]))) }),);s.Sensor("Greet {word}:", (state, name, doc) =>{ var n = name.AsString(); return Value.List([Value.Of(n), Value.Of($"Hello, {n}!\n")]);});s.Sensor("Greet {word}:", func(state varar.Value, args []varar.Value) (*varar.Value, error) { name := args[0].MustString() return varar.Ptr(varar.ListValue( varar.StrValue(name), varar.StrValue("Hello, "+name+"!\n"), )), nil})The comparison is exact equality, including the trailing newline. This step
has two slots — the captured {word} and the doc string — so it
returns an array with one element per value, in order: [name, text]. A step
whose doc string fills its only slot returns the text bare:
sensor('Greet Bob:', (_state, _body: string) => { return 'Hello, Bob!\n'})s.sensor("Greet Bob:", (Ctx ctx, String doc) -> "Hello, Bob!\n");sensor("Greet Bob:") { _: String -> "Hello, Bob!\n" }@sensor("Greet Bob:")def _(state, doc): return "Hello, Bob!\n"sensor('Greet Bob:') { |_state, _doc| "Hello, Bob!\n" }s.sensor( "Greet Bob:", file!(), line!() as usize, Handler::sync1(|_state, _doc| Ok(Some(Value::from("Hello, Bob!\n")))),);s.Sensor("Greet Bob:", (state, doc) => Value.Of("Hello, Bob!\n"));s.Sensor("Greet Bob:", func(state varar.Value, args []varar.Value) (*varar.Value, error) { return varar.Ptr(varar.StrValue("Hello, Bob!\n")), nil})See the sensors reference for the full return-value rules.
How failures are reported
Section titled “How failures are reported”- A differing table cell fails with a
CellMismatchError: each wrong cell gets its ownexpected/actual, anchored to the cell’s source span — editors redden exactly the failing characters. - A differing doc string fails with a
CellMismatchErrortoo: a doc string is one cell, compared whole. - Returning a value of the wrong shape is a
ReturnShapeError. - A table or doc string is itself a slot, so returning
undefined(None/null/nil) from one of these steps is aReturnShapeError, not a pass. Throw if you want to assert by hand instead.