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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ target/
.direnv/
result

# mq-test snapshot store (assert_snapshot); __snapshots__/ itself IS checked in
.mq-test-store/

**/*.rs.bk
*.pdb

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"users": [{"name": "Alice", "id": 1, "email": "alice@example.com", "roles": ["admin", "user"]}, {"name": "Bob", "id": 2, "email": "bob@example.com", "roles": ["user"]}, {"name": "Charlie", "id": 3, "email": "charlie@example.com", "roles": ["editor", "user"]}], "meta": {"count": 3, "generated_at": "2024-06-01T12:00:00Z"}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
| name | id | email | roles |
| --- | --- | --- | --- |
| Alice | 1 | alice@example.com | ["admin", "user"] |
| Bob | 2 | bob@example.com | ["user"] |
| Charlie | 3 | charlie@example.com | ["editor", "user"] |
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
This is the introduction section.

## Advanced Topics
This section covers advanced topics.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Introduction
This is the introduction section.

## Advanced Topics
This section covers advanced topics.

# Conclusion
This is the conclusion section.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
| Name | Age | City |
| ----- | --- | ----- |
| Alice | 30 | Tokyo |
| Bob | 25 | Osaka |
16 changes: 16 additions & 0 deletions crates/mq-lang/modules/__snapshots__/toon_test/toon_stringify.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
context:
task: Our favorite hikes together
location: Boulder
season: spring_2025
friends[3]: ana,luis,sam
hikes[3]{name,id,distanceKm,elevationGain,companion,wasSunny}:
Blue Lake Trail,1,7.5,320,ana,true
Ridge Overlook,2,9.2,540,luis,false
Wildflower Loop,3,5.1,180,sam,true
items[3]:
- 1
- a: 1
- text
items2[2]{name,price}:
Laptop,999
Mouse,29
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
| Index | Tag | Attributes | Text |
| --- | --- | --- | --- |
| 0 | user | id=1 | |
| 1 | user | id=2 | |
| 2 | user | id=3 | |

## Children of user:

| Index | Tag | Attributes | Text |
| --- | --- | --- | --- |
| 0 | name | | Alice |
| 1 | email | | alice@example.com |
| 2 | roles | | |

### Children of name:

| Tag | Attributes | Text | Children |
| --- | --- | --- | --- |
| name | | Alice | 0 |

### Children of email:

| Tag | Attributes | Text | Children |
| --- | --- | --- | --- |
| email | | alice@example.com | 0 |

### Children of roles:

| Index | Tag | Attributes | Text |
| --- | --- | --- | --- |
| 0 | role | | admin |
| 1 | role | | user |

#### Children of role:

| Tag | Attributes | Text | Children |
| --- | --- | --- | --- |
| role | | admin | 0 |

#### Children of role:

| Tag | Attributes | Text | Children |
| --- | --- | --- | --- |
| role | | user | 0 |

## Children of user:

| Index | Tag | Attributes | Text |
| --- | --- | --- | --- |
| 0 | name | | Bob |
| 1 | email | | bob@example.com |
| 2 | roles | | |

### Children of name:

| Tag | Attributes | Text | Children |
| --- | --- | --- | --- |
| name | | Bob | 0 |

### Children of email:

| Tag | Attributes | Text | Children |
| --- | --- | --- | --- |
| email | | bob@example.com | 0 |

### Children of roles:

| Index | Tag | Attributes | Text |
| --- | --- | --- | --- |
| 0 | role | | user |

#### Children of role:

| Tag | Attributes | Text | Children |
| --- | --- | --- | --- |
| role | | user | 0 |

## Children of user:

| Index | Tag | Attributes | Text |
| --- | --- | --- | --- |
| 0 | name | | Charlie |
| 1 | email | | charlie@example.com |
| 2 | roles | | |

### Children of name:

| Tag | Attributes | Text | Children |
| --- | --- | --- | --- |
| name | | Charlie | 0 |

### Children of email:

| Tag | Attributes | Text | Children |
| --- | --- | --- | --- |
| email | | charlie@example.com | 0 |

### Children of roles:

| Index | Tag | Attributes | Text |
| --- | --- | --- | --- |
| 0 | role | | editor |
| 1 | role | | user |

#### Children of role:

| Tag | Attributes | Text | Children |
| --- | --- | --- | --- |
| role | | editor | 0 |

#### Children of role:

| Tag | Attributes | Text | Children |
| --- | --- | --- | --- |
| role | | user | 0 |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"array": ["item1", "item2", "item3"], "number": 42, "float": 3.14, "bool_true": true, "bool_false": false, "null_value": null, "object": {"key1": "value1", "key2": "value2"}, "nested": {"arr": ["a", "b"], "obj": {"subkey": "subval"}}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
| Key | Value |
| --- | --- |
| array | ["item1", "item2", "item3"] |
| number | 42 |
| float | 3.14 |
| bool_true | true |
| bool_false | false |
| null_value | |
| object | {"key1": "value1", "key2": "value2"} |
| nested | {"arr": ["a", "b"], "obj": {"subkey": "subval"}} |
4 changes: 2 additions & 2 deletions crates/mq-lang/modules/json_test.mq
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ end

def test_json_stringify():
let result = json::json_stringify(json::json_parse(json_input))
| assert_eq(result, "{\"users\": [{\"name\": \"Alice\", \"id\": 1, \"email\": \"alice@example.com\", \"roles\": [\"admin\", \"user\"]}, {\"name\": \"Bob\", \"id\": 2, \"email\": \"bob@example.com\", \"roles\": [\"user\"]}, {\"name\": \"Charlie\", \"id\": 3, \"email\": \"charlie@example.com\", \"roles\": [\"editor\", \"user\"]}], \"meta\": {\"count\": 3, \"generated_at\": \"2024-06-01T12:00:00Z\"}}")
| assert_snapshot("json_stringify", result)
end

def test_json_to_markdown_table():
let result = json::json_parse(json_input)
| let result = json::json_to_markdown_table(result["users"])
| assert_eq(result, "| name | id | email | roles |\n| --- | --- | --- | --- |\n| Alice | 1 | alice@example.com | [\"admin\", \"user\"] |\n| Bob | 2 | bob@example.com | [\"user\"] |\n| Charlie | 3 | charlie@example.com | [\"editor\", \"user\"] |")
| assert_snapshot("json_to_markdown_table", result)
end
6 changes: 3 additions & 3 deletions crates/mq-lang/modules/section_test.mq
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def test_section_content():
let sections = do test_sections | section::split(1);
| let first_section = first(sections)
| let content_nodes = section::content(first_section)
| assert(len(content_nodes) > 0)
| assert_snapshot("section_content", to_markdown_string(content_nodes))
end

def test_section_level():
Expand All @@ -68,7 +68,7 @@ end
def test_section_flatten():
let sections = do test_sections | section::split(1);
| let flattened = section::collect(sections)
| assert(is_array(flattened))
| assert_snapshot("section_flatten", to_markdown_string(flattened))
end

def test_section_section_pattern():
Expand Down Expand Up @@ -106,7 +106,7 @@ end
def test_section_bodies_pipe():
let body_list = do test_sections | section::section("Introduction") | section::bodies();
| assert_eq(len(body_list), 1)
| assert(is_array(first(body_list)))
| assert_eq(to_markdown_string(first(body_list)), "This is the introduction section.\n")
end

# @parametrize([[1, 2, 1], [2, 1, 2], [1..2, 3, 1]])
Expand Down
3 changes: 1 addition & 2 deletions crates/mq-lang/modules/table_test.mq
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,7 @@ end
def test_table_to_markdown():
let t = first(table::tables(table_nodes))
| let md_result = table::to_markdown(t)
| assert(is_array(md_result))
| assert(len(md_result) > 0)
| assert_snapshot("table_to_markdown", to_markdown_string(md_result))
end

def test_table_map_rows():
Expand Down
2 changes: 1 addition & 1 deletion crates/mq-lang/modules/toon_test.mq
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ end

def test_toon_stringify():
let result = toon::toon_stringify(toon::toon_parse(toon_input))
| assert_eq(result, "context:\n task: Our favorite hikes together\n location: Boulder\n season: spring_2025\nfriends[3]: ana,luis,sam\nhikes[3]{name,id,distanceKm,elevationGain,companion,wasSunny}:\n Blue Lake Trail,1,7.5,320,ana,true\n Ridge Overlook,2,9.2,540,luis,false\n Wildflower Loop,3,5.1,180,sam,true\nitems[3]:\n - 1\n - a: 1\n - text\nitems2[2]{name,price}:\n Laptop,999\n Mouse,29")
| assert_snapshot("toon_stringify", result)
end

def test_toon_round_trip():
Expand Down
2 changes: 1 addition & 1 deletion crates/mq-lang/modules/xml_test.mq
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ end

def test_xml_to_markdown_table():
let result = xml::xml_to_markdown_table(xml::xml_parse(xml_input))
| assert_eq(result, "| Index | Tag | Attributes | Text |\n| --- | --- | --- | --- |\n| 0 | user | id=1 | |\n| 1 | user | id=2 | |\n| 2 | user | id=3 | |\n\n## Children of user:\n\n| Index | Tag | Attributes | Text |\n| --- | --- | --- | --- |\n| 0 | name | | Alice |\n| 1 | email | | alice@example.com |\n| 2 | roles | | |\n\n### Children of name:\n\n| Tag | Attributes | Text | Children |\n| --- | --- | --- | --- |\n| name | | Alice | 0 |\n\n### Children of email:\n\n| Tag | Attributes | Text | Children |\n| --- | --- | --- | --- |\n| email | | alice@example.com | 0 |\n\n### Children of roles:\n\n| Index | Tag | Attributes | Text |\n| --- | --- | --- | --- |\n| 0 | role | | admin |\n| 1 | role | | user |\n\n#### Children of role:\n\n| Tag | Attributes | Text | Children |\n| --- | --- | --- | --- |\n| role | | admin | 0 |\n\n#### Children of role:\n\n| Tag | Attributes | Text | Children |\n| --- | --- | --- | --- |\n| role | | user | 0 |\n\n## Children of user:\n\n| Index | Tag | Attributes | Text |\n| --- | --- | --- | --- |\n| 0 | name | | Bob |\n| 1 | email | | bob@example.com |\n| 2 | roles | | |\n\n### Children of name:\n\n| Tag | Attributes | Text | Children |\n| --- | --- | --- | --- |\n| name | | Bob | 0 |\n\n### Children of email:\n\n| Tag | Attributes | Text | Children |\n| --- | --- | --- | --- |\n| email | | bob@example.com | 0 |\n\n### Children of roles:\n\n| Index | Tag | Attributes | Text |\n| --- | --- | --- | --- |\n| 0 | role | | user |\n\n#### Children of role:\n\n| Tag | Attributes | Text | Children |\n| --- | --- | --- | --- |\n| role | | user | 0 |\n\n## Children of user:\n\n| Index | Tag | Attributes | Text |\n| --- | --- | --- | --- |\n| 0 | name | | Charlie |\n| 1 | email | | charlie@example.com |\n| 2 | roles | | |\n\n### Children of name:\n\n| Tag | Attributes | Text | Children |\n| --- | --- | --- | --- |\n| name | | Charlie | 0 |\n\n### Children of email:\n\n| Tag | Attributes | Text | Children |\n| --- | --- | --- | --- |\n| email | | charlie@example.com | 0 |\n\n### Children of roles:\n\n| Index | Tag | Attributes | Text |\n| --- | --- | --- | --- |\n| 0 | role | | editor |\n| 1 | role | | user |\n\n#### Children of role:\n\n| Tag | Attributes | Text | Children |\n| --- | --- | --- | --- |\n| role | | editor | 0 |\n\n#### Children of role:\n\n| Tag | Attributes | Text | Children |\n| --- | --- | --- | --- |\n| role | | user | 0 |")
| assert_snapshot("xml_to_markdown_table", result)
end
4 changes: 2 additions & 2 deletions crates/mq-lang/modules/yaml_test.mq
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ end

def test_yaml_to_json():
let result = yaml::yaml_to_json(yaml::yaml_parse(yaml_input))
| assert_eq(result, "{\"array\": [\"item1\", \"item2\", \"item3\"], \"number\": 42, \"float\": 3.14, \"bool_true\": true, \"bool_false\": false, \"null_value\": null, \"object\": {\"key1\": \"value1\", \"key2\": \"value2\"}, \"nested\": {\"arr\": [\"a\", \"b\"], \"obj\": {\"subkey\": \"subval\"}}}")
| assert_snapshot("yaml_to_json", result)
end

def test_yaml_to_markdown_table():
let result = yaml::yaml_to_markdown_table(yaml::yaml_parse(yaml_input))
| assert_eq(result, "| Key | Value |\n| --- | --- |\n| array | [\"item1\", \"item2\", \"item3\"] |\n| number | 42 |\n| float | 3.14 |\n| bool_true | true |\n| bool_false | false |\n| null_value | |\n| object | {\"key1\": \"value1\", \"key2\": \"value2\"} |\n| nested | {\"arr\": [\"a\", \"b\"], \"obj\": {\"subkey\": \"subval\"}} |")
| assert_snapshot("yaml_to_markdown_table", result)
end

def test_to_front_matter():
Expand Down
1 change: 1 addition & 0 deletions crates/mq-test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mq-lang = {workspace = true, features = ["cst", "debugger", "file-io", "http", "
rayon = {workspace = true}
rustc-hash = {workspace = true}
serde_json = {workspace = true}
similar = {workspace = true}

[dev-dependencies]
rstest = {workspace = true}
Expand Down
57 changes: 44 additions & 13 deletions crates/mq-test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ mq-test --tag smoke

# Run test files in parallel once more than 4 files are discovered
mq-test --parallel-threshold 4

# Accept the current output of every assert_snapshot(...) call as the new golden snapshot
mq-test --update-snapshots
```

## Coverage
Expand Down Expand Up @@ -201,29 +204,57 @@ function via a separate comment line placed right above it.
merged safely across parallel files, and each file's report prints
atomically so concurrent files' output never interleaves.

A failing test in one file no longer stops other files from running: every
discovered file always runs to completion, and `mq-test` exits non-zero if
any test in any file failed.
A failing test in one file no longer stops other files from running: every discovered file always runs to completion, and `mq-test` exits non-zero if any test in any file failed.

### Test Helpers

Tests use the built-in `assert_eq` and related helpers from the `test` module:

| Function | Description |
| --------------------------- | ------------------------------ |
| `assert_eq(actual, expect)` | Fails if `actual != expect` |
| `assert(cond)` | Fails if `cond` is not `true` |
| `test_case(name, fn)` | Registers a named test case |
| `run_tests(cases)` | Runs all registered test cases |
| Function | Description |
| -------------------------------- | ------------------------------ |
| `assert_eq(actual, expect)` | Fails if `actual != expect` |
| `assert(cond)` | Fails if `cond` is not `true` |
| `assert_snapshot(name, actual)` | Fails if `actual` doesn't match the golden snapshot `name` |
| `test_case(name, fn)` | Registers a named test case |
| `run_tests(cases)` | Runs all registered test cases |

The runner automatically generates a `run_tests(flatten([...]))` call from all
discovered test functions β€” test files do not need to maintain a manual list.

### Snapshot Testing

`assert_snapshot(name, actual)` compares `actual` against a golden file, for outputs too large to usefully inline in an `assert_eq` diff (e.g. a rendered document):

```mq
include "test"
|

def test_renders_the_full_report():
assert_snapshot("report", render_report(data))
end
```

The layout follows the `suite`/`ref`/`store` split from
[typst's test runner](https://github.com/typst/typst/tree/main/tests): the test file
itself plus whatever it feeds into `assert_snapshot` is the "suite" (input), and:

- **ref** β€” `__snapshots__/<test file stem>/<name>.snap`, next to the test file. This is
the golden, checked-in expected value.

A snapshot that doesn't exist yet fails the test (it does not get created implicitly) β€” this is the one exception in `mq-test` where "no such thing" is a real failure, not an error to silently paper over. Run with `--update-snapshots` to create or overwrite golden snapshots from the current output:

```bash
mq-test --update-snapshots
```

`assert_snapshot` is real, unmocked disk I/O β€” the one deliberate exception to the
hermetic `Io` described below, since golden files must survive across runs and be
checked into version control.

### Mocking File and Network I/O

Each test file runs against an in-memory, hermetic `Io` β€” no real disk or network access β€”
so `read_file`/`write_file`/`http` are always allowed, regardless of the `--allow-read` /
`--allow-write` / `--allow-net` flags the CLI itself requires.
so `read_file`/`write_file`/`http` are always allowed, regardless of the `--allow-read` / `--allow-write` / `--allow-net` flags the CLI itself requires.

Files can be seeded from within a test simply by writing them first:

Expand All @@ -250,8 +281,7 @@ def test_reads_a_mocked_api_response():
end
```

State (files written, mocked responses) does not leak between test files β€” each gets a
fresh in-memory `Io`.
State (files written, mocked responses) does not leak between test files β€” each gets a fresh in-memory `Io`.

## Example

Expand Down Expand Up @@ -295,3 +325,4 @@ cargo build -p mq-test
## License

MIT

Loading