diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d38531..97e0344 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,15 @@ releases; only 1.0.0 carries a release date. ## [Unreleased] +### Changed + +- **YAML structures are the documented form for JSON expectations.** The README + now writes step examples in block YAML and shows `expect_json`'s `equals` + taking a nested YAML map/list, which deserializes to the equivalent typed + JSON value for whole-structure equality. The nested form was already + accepted; it is now documented in the README and `SCHEMA.md` and locked in by + a parser test. + ## [1.2.1] - 2026-06-08 ### Added diff --git a/README.md b/README.md index 84f1566..a59d50c 100644 --- a/README.md +++ b/README.md @@ -86,18 +86,28 @@ workspace: cwd: . # run dir, relative to the scenario file temp: true # OR run in a fresh temp dir (0700 on Unix) steps: - - spawn: bash # or {command, cwd, env} + - spawn: bash - send: echo ${username} # stdin line; \r (CR) appended; ${var} expanded - send_raw: "y" # raw bytes, no newline - key: enter # named key -> control bytes - wait: 2s # or 500ms - - expect: {contains: hello, timeout: 30s} # wait for substring (timeout optional) - - expect_regex: {pattern: "hello.*world"} # wait for regex (regex::bytes) - - expect_not: {contains: error} # immediate: must NOT be present now - - expect_file_exists: {path: result.txt} - - expect_file_contains: {path: result.txt, contains: success} - - expect_file_not_contains: {path: result.txt, contains: error} - - expect_file_changed: {path: src/auth.ts} # content differs vs. spawn time + - expect: # wait for substring + contains: hello + timeout: 30s # optional + - expect_regex: # wait for regex (regex::bytes) + pattern: 'hello.*world' + - expect_not: # immediate: must NOT be present now + contains: error + - expect_file_exists: + path: result.txt + - expect_file_contains: + path: result.txt + contains: success + - expect_file_not_contains: + path: result.txt + contains: error + - expect_file_changed: # content differs vs. spawn time + path: src/auth.ts - expect_exit: 0 - expect_running: true ``` @@ -211,14 +221,38 @@ session directly.) `expect_json` extracts a JSON value and asserts on a field addressed by a path: ```yaml -- expect_json: {path: result.status, equals: success} -- expect_json: {path: result.message, contains: "expired"} -- expect_json: {path: result.items, exists: true} -- expect_json: {path: result.items.0.name, equals: first} # dotted array index -- expect_json: {path: 'result.items[0].name', equals: first} # bracket array index -- expect_json: {path: 'result["a.b"].value', equals: 7} # key containing a dot -- expect_json: {path: status, equals: passed, source: {file: report.json}} -- expect_json: {path: status, equals: ok, timeout: 5s} # wait for output JSON +- expect_json: + path: result.status + equals: success +- expect_json: + path: result.message + contains: expired +- expect_json: + path: result.items + exists: true +- expect_json: + path: result.items.0.name # dotted array index + equals: first +- expect_json: + path: 'result.items[0].name' # bracket array index + equals: first +- expect_json: + path: 'result["a.b"].value' # key containing a dot + equals: 7 +- expect_json: + path: status + equals: passed + source: + file: report.json +- expect_json: + path: status + equals: ok + timeout: 5s # wait for output JSON +- expect_json: # YAML value, JSON comparison + path: result + equals: + status: success + items: [1, 2] ``` - **Source.** With no `source` (the default `output`, which may also be written diff --git a/SCHEMA.md b/SCHEMA.md index dc6c60f..a17de05 100644 --- a/SCHEMA.md +++ b/SCHEMA.md @@ -149,6 +149,8 @@ delay, precede `expect_not` with a `wait`. **exactly one** of: - `equals: ` — typed equality (`200` is a number, `"200"` a string); + `` may be a nested YAML map or list, which deserializes to the + equivalent typed JSON structure and compares the whole value at `path`; - `contains: ` — substring of the value's string form; - `exists: true` — the path resolves to a value. diff --git a/e2e/scenarios/positive/expect-json.yaml b/e2e/scenarios/positive/expect-json.yaml index 4b8195d..0a8ff4a 100644 --- a/e2e/scenarios/positive/expect-json.yaml +++ b/e2e/scenarios/positive/expect-json.yaml @@ -1,27 +1,45 @@ # Dogfood: expect_json over a real PTY. A shell prints a JSON object after some # log noise; expect_json must extract the tail JSON block and assert on it via -# the dotted path grammar (equals, contains, exists). Proves tail-block -# extraction and path navigation work end to end when pitty runs itself. +# whole-document equality, dotted path navigation, contains, and exists. Proves +# tail-block extraction and path navigation work end to end when pitty runs +# itself. name: expect-json workspace: temp: true steps: - spawn: bash - # Emit a line of noise, then a single-line JSON object. The expect below - # ensures the JSON has been written before the assertions read the buffer. - - send: 'echo "LOG: starting"; echo "{\"status\": \"ok\", \"items\": [1,2,3], \"message\": \"token expired\"}"' + # Emit a line of noise, then a single-line JSON object, then a sentinel built + # by shell arithmetic. The PTY echoes the typed command line verbatim, so the + # literal JSON above also appears in the buffer as echo; gating on a substring + # of that JSON (e.g. "status") would match the echo before bash runs and let + # expect_json read a half-emitted buffer. The sentinel is `$((40+2))` in the + # command but `42` only in real output, so JSON_READY_42 is reached only after + # bash has finished printing the JSON. + - send: >- + echo "LOG: starting"; + printf '%s\n' '{"result":{"status":"ok","items":[1,2,3],"message":"token expired"}}'; + echo "JSON_READY_$((40+2))" - expect: - contains: status + contains: JSON_READY_42 timeout: 10s - expect_json: - path: status + path: result + equals: + status: ok + items: + - 1 + - 2 + - 3 + message: token expired + - expect_json: + path: result.status equals: ok - expect_json: - path: items.0 + path: result.items.0 equals: 1 - expect_json: - path: message + path: result.message contains: expired - expect_json: - path: items + path: result.items exists: true diff --git a/src/config/step.rs b/src/config/step.rs index 6c6c3d8..0f21d0f 100644 --- a/src/config/step.rs +++ b/src/config/step.rs @@ -932,6 +932,28 @@ mod tests { } } + #[test] + fn expect_json_equals_accepts_yaml_structures() { + // Nested YAML maps and lists must deserialize to the equivalent typed + // JSON value for whole-structure equality checks. + let s = step( + "expect_json:\n path: result\n equals:\n status: ok\n items:\n - 1\n - true", + ); + match s { + Step::ExpectJson(spec) => match spec.check { + JsonCheck::Equals(v) => assert_eq!( + v, + serde_json::json!({ + "status": "ok", + "items": [1, true] + }) + ), + other => panic!("expected Equals object, got {other:?}"), + }, + other => panic!("expected ExpectJson, got {other:?}"), + } + } + #[test] fn expect_json_contains_with_file_source_and_timeout() { // contains plus a file source and a timeout must all parse.