Files
troupe-standup/wizard-work/beam/test_jsonl_validate.py
T
volt dc461001b9 volt: Q104 done -- beam jsonl_validate's exit-code contract pinned by a 25-test file, and the decode path returns 2 not 1
wizard-work/beam/test_jsonl_validate.py (new, 25 tests) pins the module's own
docstring contract as process exit codes: 0 valid, 1 bad rows, 2 usage/IO/schema/
decode. wizard-work/beam/jsonl_validate.py repaired in three places:
  - undecodable input (and schema) now returns 2 with an `input error:` line and
    no summary line, instead of a UnicodeDecodeError traceback and rc 1;
  - utf-8-sig on both files, so a leading BOM is stripped not graded as bad data;
  - a required field present as null yields one error, not "missing required
    field" plus "null not allowed".

Measured: the new test file against the shipped module (md5 f53b6288...) fails
6 of 25 (3 decode, 2 BOM, 1 null); against the repair, 25/25 pass. bash CHECK:
rc 0 with Ran 243 -> Ran 268; with the shipped module swapped back in, rc 1
FAILED (failures=6) -- the gate was rc 0 and blind before. No edit to CHECK:
the file is a unittest.TestCase, so the existing -p 'test_*.py' leg collects it.
The 242-checks line is toposort's import-time counter and does not move (Q105).

verify.py --repo . reads 8 legs, 6 pass, 2 fault (checkin, path-claims) here and
identically at the parent cd5f5c9. path_claim_check's tolerated NOT_ADDED row
for the new file reads CLEARED on this tree. Q105's 27 uncollected files / 131
tests remain open and untouched.
2026-10-28 05:01:10 +00:00

278 lines
12 KiB
Python

#!/usr/bin/env python3
"""test_jsonl_validate.py -- pins the exit-code and null-semantics contract of
`jsonl_validate.py` (Q104).
The contract under test is the module's own docstring, run as a process:
0 all records valid
1 one or more validation errors
2 usage / IO / schema / decode error
Why a test file exists at all: the module landed at `afb182a` with a commit
message claiming tests, and the lane's gate (`bash CHECK` -> `unittest
discover -p 'test_*.py'`) reached none of them because there were none. Every
assertion below is a process exit code, not an internal return value, so the
thing pinned is the thing a caller sees.
The defect this file was written to catch: hex `ff fe` in the input raised
`UnicodeDecodeError` out of the iteration and the interpreter exited **1** --
the code the docstring assigns to "the rows are bad" -- so a caller reported
"N rows rejected" for a file it never finished reading. It now exits 2, prints
no summary line, and dumps no traceback.
Run directly (`python3 test_jsonl_validate.py`) or as part of the lane gate.
"""
import json
import os
import subprocess
import sys
import tempfile
import unittest
HERE = os.path.dirname(os.path.abspath(__file__))
TOOL = os.path.join(HERE, "jsonl_validate.py")
SCHEMA = {
"required": ["id", "name"],
"fields": {
"id": "int",
"name": "str",
"score": "float?",
"tags": "list?",
"active": "bool?",
"kind": "str",
},
}
class JsonlValidateContract(unittest.TestCase):
"""One test per contract clause; every assertion is a real process run."""
def setUp(self):
self._td = tempfile.TemporaryDirectory()
self.dir = self._td.name
self.schema_path = self.write_text("schema.json", json.dumps(SCHEMA))
def tearDown(self):
self._td.cleanup()
# -- helpers ---------------------------------------------------------
def write_text(self, name, text, encoding="utf-8"):
path = os.path.join(self.dir, name)
with open(path, "w", encoding=encoding) as f:
f.write(text)
return path
def write_bytes(self, name, data):
path = os.path.join(self.dir, name)
with open(path, "wb") as f:
f.write(data)
return path
def write_records(self, name, records):
return self.write_text(name, "".join(json.dumps(r) + "\n" for r in records))
def run_tool(self, *args):
proc = subprocess.run(
[sys.executable, TOOL, *args],
capture_output=True,
text=True,
)
return proc.returncode, proc.stdout, proc.stderr
def assert_no_traceback(self, stderr):
self.assertNotIn("Traceback (most recent call last)", stderr)
def assert_not_counted(self, out):
self.assertNotIn("checked", out)
# -- exit code 0: everything valid -----------------------------------
def test_valid_file_exits_0(self):
path = self.write_records("ok.jsonl", [{"id": 1, "name": "a"}, {"id": 2, "name": "b"}])
rc, out, err = self.run_tool(self.schema_path, path)
self.assertEqual(rc, 0, out + err)
self.assertIn("checked 2 records, 0 invalid", out)
def test_extra_fields_are_allowed(self):
# The module is strict on declared fields and tolerant of unknown ones
# (its own comment: "allow extras"). Pinned so that stays a decision.
path = self.write_records("extra.jsonl", [{"id": 1, "name": "a", "unknown": [1, 2]}])
rc, out, _ = self.run_tool(self.schema_path, path)
self.assertEqual(rc, 0, out)
def test_blank_lines_are_skipped_and_not_counted(self):
path = self.write_text("blank.jsonl", '{"id": 1, "name": "a"}\n\n \n\n{"id": 2, "name": "b"}\n')
rc, out, _ = self.run_tool(self.schema_path, path)
self.assertEqual(rc, 0, out)
self.assertIn("checked 2 records, 0 invalid", out)
def test_float_field_accepts_int(self):
# Undocumented but intentional per Q104's "Not claimed": an int is a
# valid `float`. Pinned to make it visible, not to endorse it.
path = self.write_records("float.jsonl", [{"id": 1, "name": "a", "score": 3}])
rc, out, _ = self.run_tool(self.schema_path, path)
self.assertEqual(rc, 0, out)
# -- exit code 1: the rows are bad -----------------------------------
def test_type_mismatch_exits_1(self):
path = self.write_records("bad.jsonl", [{"id": "x", "name": "a"}])
rc, out, err = self.run_tool(self.schema_path, path)
self.assertEqual(rc, 1)
self.assertIn("field 'id': expected int, got str", out)
self.assert_no_traceback(err)
def test_malformed_json_line_exits_1(self):
path = self.write_text("broken.jsonl", '{"id": 1, "name": "a"}\n{not json}\n')
rc, out, err = self.run_tool(self.schema_path, path)
self.assertEqual(rc, 1)
self.assertIn("line 2: invalid JSON", out)
self.assertIn("checked 2 records, 1 invalid", out)
self.assert_no_traceback(err)
def test_count_separates_valid_from_invalid(self):
path = self.write_records(
"mixed.jsonl", [{"id": 1, "name": "a"}, {"id": "x", "name": "b"}, {"id": 2, "name": "c"}]
)
rc, out, _ = self.run_tool(self.schema_path, path)
self.assertEqual(rc, 1)
self.assertIn("checked 3 records, 1 invalid", out)
def test_non_object_record_exits_1(self):
path = self.write_text("list.jsonl", "[1, 2, 3]\n")
rc, out, _ = self.run_tool(self.schema_path, path)
self.assertEqual(rc, 1)
self.assertIn("record is not an object", out)
def test_bool_is_not_an_int(self):
# `isinstance(True, int)` is True in Python; a validator that forgets it
# accepts `true` for an int field. Documented behaviour, asserted here.
path = self.write_records("bool.jsonl", [{"id": True, "name": "a"}])
rc, out, _ = self.run_tool(self.schema_path, path)
self.assertEqual(rc, 1)
self.assertIn("field 'id': expected int, got bool", out)
def test_max_errors_truncates_after_the_first_error(self):
path = self.write_records("many.jsonl", [{"id": "a", "name": "x"}] * 4)
rc, out, _ = self.run_tool(self.schema_path, path, "--max-errors", "1")
self.assertEqual(rc, 1)
self.assertIn("(truncated)", out)
# -- required/optional null semantics --------------------------------
def test_required_field_null_is_exactly_one_error(self):
# Q104 measured two errors for one fact ("missing required field 'id'"
# and "field 'id': null not allowed"). One fact, one error.
path = self.write_records("nullreq.jsonl", [{"id": None, "name": "a"}])
rc, out, _ = self.run_tool(self.schema_path, path)
self.assertEqual(rc, 1)
error_lines = [ln for ln in out.splitlines() if ln.startswith("line 1:")]
self.assertEqual(error_lines, ["line 1: missing required field 'id'"], out)
def test_required_field_absent_exits_1(self):
path = self.write_records("absent.jsonl", [{"name": "a"}])
rc, out, _ = self.run_tool(self.schema_path, path)
self.assertEqual(rc, 1)
self.assertIn("missing required field 'id'", out)
def test_optional_field_null_is_valid(self):
# `score` is declared "float?" -- present-as-null is "missing", not bad.
path = self.write_records("nullopt.jsonl", [{"id": 1, "name": "a", "score": None}])
rc, out, _ = self.run_tool(self.schema_path, path)
self.assertEqual(rc, 0, out)
def test_declared_non_optional_field_null_exits_1(self):
# `kind` is declared "str" (not required, not optional): null is an
# error, while the same field absent from the record is fine.
absent = self.write_records("kind_absent.jsonl", [{"id": 1, "name": "a"}])
as_null = self.write_records("kind_null.jsonl", [{"id": 1, "name": "a", "kind": None}])
self.assertEqual(self.run_tool(self.schema_path, absent)[0], 0)
rc, out, _ = self.run_tool(self.schema_path, as_null)
self.assertEqual(rc, 1)
self.assertIn("field 'kind': null not allowed", out)
# -- BOM is an encoding marker, not bad data -------------------------
def test_leading_bom_is_stripped_not_graded(self):
# Before Q104's fix this was "line 1: invalid JSON: Unexpected UTF-8 BOM".
path = os.path.join(self.dir, "bom.jsonl")
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf" + json.dumps({"id": 1, "name": "a"}).encode("utf-8") + b"\n")
rc, out, err = self.run_tool(self.schema_path, path)
self.assertEqual(rc, 0, out + err)
self.assertIn("checked 1 records, 0 invalid", out)
def test_bom_in_schema_is_stripped(self):
path = os.path.join(self.dir, "bom_schema.json")
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf" + json.dumps(SCHEMA).encode("utf-8"))
records = self.write_records("ok2.jsonl", [{"id": 1, "name": "a"}])
rc, out, err = self.run_tool(path, records)
self.assertEqual(rc, 0, out + err)
# -- exit code 2: the RUN is bad, not the rows -----------------------
def test_missing_input_exits_2(self):
rc, out, err = self.run_tool(self.schema_path, os.path.join(self.dir, "nope.jsonl"))
self.assertEqual(rc, 2)
self.assertIn("input error:", err)
self.assert_no_traceback(err)
self.assertNotIn("checked", out)
def test_binary_input_exits_2_with_no_traceback(self):
# THE PIN for Q104. At afb182a: rc 1 + UnicodeDecodeError traceback.
path = self.write_bytes("bin.jsonl", b"\xff\xfe\n")
rc, out, err = self.run_tool(self.schema_path, path)
self.assertEqual(rc, 2, out + err)
self.assert_no_traceback(err)
self.assertIn("input error: not valid UTF-8 text", err)
def test_binary_input_after_good_records_still_exits_2(self):
# A run that dies mid-file must not leave a summary line a caller can
# read as the verdict on the rows it never reached.
path = self.write_bytes(
"bin2.jsonl",
json.dumps({"id": 1, "name": "a"}).encode("utf-8") + b"\n" + b"\xff\xfe\n",
)
rc, out, err = self.run_tool(self.schema_path, path)
self.assertEqual(rc, 2, out + err)
self.assert_no_traceback(err)
self.assertNotIn("checked", out)
def test_truncated_utf8_sequence_exits_2(self):
path = self.write_bytes("bin3.jsonl", b'{"id":1,"name":"\xc3"}')
rc, out, err = self.run_tool(self.schema_path, path)
self.assertEqual(rc, 2, out + err)
self.assert_no_traceback(err)
def test_missing_schema_exits_2(self):
records = self.write_records("ok3.jsonl", [{"id": 1, "name": "a"}])
rc, out, err = self.run_tool(os.path.join(self.dir, "nope.json"), records)
self.assertEqual(rc, 2)
self.assertIn("schema error:", err)
self.assert_not_counted(out)
def test_schema_not_json_exits_2(self):
bad_schema = self.write_text("bad_schema.json", "{schema: nope}")
records = self.write_records("ok4.jsonl", [{"id": 1, "name": "a"}])
rc, _, err = self.run_tool(bad_schema, records)
self.assertEqual(rc, 2)
self.assertIn("schema error:", err)
def test_schema_unknown_type_exits_2(self):
bad_schema = self.write_text("bad_type.json", json.dumps({"fields": {"a": "bogus"}}))
records = self.write_records("ok5.jsonl", [{"a": 1}])
rc, _, err = self.run_tool(bad_schema, records)
self.assertEqual(rc, 2)
self.assertIn("unknown type 'bogus'", err)
def test_unreadable_input_directory_exits_2(self):
rc, _, err = self.run_tool(self.schema_path, self.dir)
self.assertEqual(rc, 2)
self.assertIn("input error:", err)
def test_usage_error_exits_2(self):
rc, _, err = self.run_tool()
self.assertEqual(rc, 2)
self.assertNotIn("checked", err)
if __name__ == "__main__":
unittest.main(verbosity=2)