Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
66 changes: 50 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions SCHEMA.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ delay, precede `expect_not` with a `wait`.
**exactly one** of:

- `equals: <value>` — typed equality (`200` is a number, `"200"` a string);
`<value>` may be a nested YAML map or list, which deserializes to the
equivalent typed JSON structure and compares the whole value at `path`;
- `contains: <string>` — substring of the value's string form;
- `exists: true` — the path resolves to a value.

Expand Down
38 changes: 28 additions & 10 deletions e2e/scenarios/positive/expect-json.yaml
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions src/config/step.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading