A human-friendly DSL for ATProto Lexicons
1

Configure Feed

Select the types of activity you want to include in your feed.

Switch to cargo insta

authored by stavola.xyz and committed by

Tangled (Apr 16, 2026, 6:48 AM +0300) 0b45eeea 3aac6e63

+562 -326
+5 -4
.config/nextest.toml
··· 1 1 # Default profile — used by `just test` and ad-hoc `cargo nextest run`. 2 + # `just test` applies `-E 'not binary(real_world_roundtrip)'` to filter 3 + # out network-dependent tests. Run them explicitly via 4 + # `just test-real-world` / `just test-all`. 2 5 [profile.default] 3 6 # Surface failing test output inline as it happens, then repeat the 4 7 # summary at the end. On success, stay quiet — use `just test-verbose` ··· 11 14 fail-fast = false 12 15 13 16 # Network-gated real-world roundtrip overrides. Absorb flaky DNS/HTTP 14 - # with a couple of retries, warn when a single attempt takes more than 15 - # 30s (a dead resolver, usually), and hard-kill after 3 minutes to 16 - # prevent wedged CI. The binary filter covers every `roundtrip_*` test 17 - # added via the `roundtrip_test!` macro. 17 + # with retries, warn on slow single attempts, hard-kill wedged runs. 18 + # Applies whenever the roundtrip binary runs, under any profile. 18 19 # 19 20 # No test-group here: nextest runs each test in its own OS process, so 20 21 # `std::env::set_current_dir` calls are isolated per test. Each of the
+37
Cargo.lock
··· 383 383 checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 384 384 385 385 [[package]] 386 + name = "console" 387 + version = "0.16.3" 388 + source = "registry+https://github.com/rust-lang/crates.io-index" 389 + checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" 390 + dependencies = [ 391 + "encode_unicode", 392 + "libc", 393 + "windows-sys 0.61.1", 394 + ] 395 + 396 + [[package]] 386 397 name = "core-foundation" 387 398 version = "0.9.4" 388 399 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 504 515 "quote", 505 516 "syn 2.0.106", 506 517 ] 518 + 519 + [[package]] 520 + name = "encode_unicode" 521 + version = "1.0.0" 522 + source = "registry+https://github.com/rust-lang/crates.io-index" 523 + checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 507 524 508 525 [[package]] 509 526 name = "encoding_rs" ··· 1108 1125 checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" 1109 1126 1110 1127 [[package]] 1128 + name = "insta" 1129 + version = "1.47.2" 1130 + source = "registry+https://github.com/rust-lang/crates.io-index" 1131 + checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" 1132 + dependencies = [ 1133 + "console", 1134 + "once_cell", 1135 + "serde", 1136 + "similar", 1137 + "tempfile", 1138 + ] 1139 + 1140 + [[package]] 1111 1141 name = "inventory" 1112 1142 version = "0.3.21" 1113 1143 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1443 1473 version = "0.1.0" 1444 1474 dependencies = [ 1445 1475 "datatest-stable", 1476 + "insta", 1446 1477 "mlf-cli", 1447 1478 "mlf-codegen", 1448 1479 "mlf-diagnostics", ··· 2190 2221 dependencies = [ 2191 2222 "libc", 2192 2223 ] 2224 + 2225 + [[package]] 2226 + name = "similar" 2227 + version = "2.7.0" 2228 + source = "registry+https://github.com/rust-lang/crates.io-index" 2229 + checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" 2193 2230 2194 2231 [[package]] 2195 2232 name = "slab"
+16 -5
justfile
··· 6 6 # Default: run all tests 7 7 default: test 8 8 9 - # Install developer tools (test runner, etc.) 9 + # Install developer tools (test runner, snapshot reviewer) 10 10 install-tools: 11 11 cargo install cargo-nextest --locked 12 + cargo install cargo-insta --locked 12 13 13 14 # Run every test across the workspace (excluding packages we can't build 14 - # locally). Nextest already parallelises across binaries, so a single 15 - # invocation is both faster and more comprehensive than chaining the 16 - # per-suite recipes below — those remain useful for targeted runs. 15 + # locally). Nextest parallelises across binaries, so a single invocation 16 + # is both faster and more comprehensive than chaining the per-suite 17 + # recipes below — those remain useful for targeted runs. Use 18 + # `just test-fast` to skip network-dependent real-world tests. 17 19 test: 18 20 @echo "Running all workspace tests..." 19 21 cargo nextest run --workspace --exclude tree-sitter-mlf --exclude mlf-wasm 22 + 23 + # Run everything except network-dependent real-world roundtrip tests 24 + test-fast: 25 + @echo "Running workspace tests (excluding real-world)..." 26 + cargo nextest run --workspace --exclude tree-sitter-mlf --exclude mlf-wasm -E 'not binary(real_world_roundtrip)' 20 27 21 28 # Run only language tests (mlf-lang crate) 22 29 test-lang: ··· 54 61 cargo nextest run -p mlf-integration-tests --test workspace_integration 55 62 56 63 # Run only the network-dependent real-world round-trip tests 57 - test-real-world: 64 + test-real: 58 65 @echo "\n🌐 Running real-world round-trip tests (fetches from network)..." 59 66 cargo nextest run -p mlf-integration-tests --test real_world_roundtrip 67 + 68 + # Review pending insta snapshot changes (accepts / rejects `.snap.new` files) 69 + review: 70 + cargo insta review 60 71 61 72 # Run tests with verbose output (stdout streamed live, tests run serially) 62 73 test-verbose:
+1
tests/Cargo.toml
··· 20 20 tokio = { version = "1", features = ["full"] } 21 21 tempfile = "3.8" 22 22 datatest-stable = "0.3" 23 + insta = { version = "1.43", features = ["json"] } 23 24 24 25 [dev-dependencies] 25 26 # Any additional test dependencies
+7 -1
tests/codegen/lexicon/annotations/expected.json tests/codegen/lexicon/annotations/lexicon@annotations.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: output.json 4 + --- 1 5 { 2 6 "$type": "com.atproto.lexicon.schema", 3 7 "lexicon": 1, ··· 8 12 "key": "custom-key", 9 13 "record": { 10 14 "type": "object", 11 - "required": ["text"], 15 + "required": [ 16 + "text" 17 + ], 12 18 "properties": { 13 19 "text": { 14 20 "type": "string"
+5
tests/codegen/lexicon/annotations/warnings@annotations.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + []
+8 -1
tests/codegen/lexicon/basic_record/expected.json tests/codegen/lexicon/basic_record/lexicon@basic_record.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: output.json 4 + --- 1 5 { 2 6 "$type": "com.atproto.lexicon.schema", 3 7 "lexicon": 1, ··· 8 12 "key": "tid", 9 13 "record": { 10 14 "type": "object", 11 - "required": ["text", "createdAt"], 15 + "required": [ 16 + "text", 17 + "createdAt" 18 + ], 12 19 "properties": { 13 20 "text": { 14 21 "type": "string"
+5
tests/codegen/lexicon/basic_record/warnings@basic_record.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + []
+10 -2
tests/codegen/lexicon/const_float_stringifies/expected.json tests/codegen/lexicon/const_float_stringifies/lexicon@const_float_stringifies.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: output.json 4 + --- 1 5 { 2 6 "$type": "com.atproto.lexicon.schema", 3 7 "lexicon": 1, ··· 10 14 "key": "tid", 11 15 "record": { 12 16 "type": "object", 13 - "required": ["name"], 17 + "required": [ 18 + "name" 19 + ], 14 20 "properties": { 15 - "name": {"type": "string"} 21 + "name": { 22 + "type": "string" 23 + } 16 24 } 17 25 } 18 26 }
-1
tests/codegen/lexicon/const_float_stringifies/expected_warnings.txt
··· 1 - com.example.const_float_stringifies: @const("x-threshold", 3.14): ATProto's data model has no floats; emitting as string "3.14" to stay spec-compliant
-4
tests/codegen/lexicon/const_float_stringifies/test.toml
··· 1 - [test] 2 - name = "const_float_stringifies" 3 - description = "@const with a fractional numeric value stringifies to stay spec-compliant and emits a warning" 4 - namespace = "com.example.const_float_stringifies"
+7
tests/codegen/lexicon/const_float_stringifies/warnings@const_float_stringifies.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + [ 6 + "com.example.const_float_stringifies: @const(\"x-threshold\", 3.14): ATProto's data model has no floats; emitting as string \"3.14\" to stay spec-compliant", 7 + ]
+4
tests/codegen/lexicon/doc_comments/expected.json tests/codegen/lexicon/doc_comments/lexicon@doc_comments.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: output.json 4 + --- 1 5 { 2 6 "$type": "com.atproto.lexicon.schema", 3 7 "lexicon": 1,
+5
tests/codegen/lexicon/doc_comments/warnings@doc_comments.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + []
+26 -16
tests/codegen/lexicon/implicit_main/expected.json tests/codegen/lexicon/implicit_main/lexicon@implicit_main.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: output.json 4 + --- 1 5 { 2 6 "$type": "com.atproto.lexicon.schema", 3 7 "lexicon": 1, 4 8 "id": "com.example.profile", 5 9 "defs": { 10 + "color": { 11 + "type": "object", 12 + "required": [ 13 + "red", 14 + "green", 15 + "blue" 16 + ], 17 + "properties": { 18 + "red": { 19 + "type": "integer" 20 + }, 21 + "green": { 22 + "type": "integer" 23 + }, 24 + "blue": { 25 + "type": "integer" 26 + } 27 + } 28 + }, 6 29 "main": { 7 30 "type": "record", 8 31 "key": "tid", 9 32 "record": { 10 33 "type": "object", 11 - "required": ["name"], 34 + "required": [ 35 + "name" 36 + ], 12 37 "properties": { 13 38 "name": { 14 39 "type": "string" ··· 17 42 "type": "ref", 18 43 "ref": "#color" 19 44 } 20 - } 21 - } 22 - }, 23 - "color": { 24 - "type": "object", 25 - "required": ["red", "green", "blue"], 26 - "properties": { 27 - "red": { 28 - "type": "integer" 29 - }, 30 - "green": { 31 - "type": "integer" 32 - }, 33 - "blue": { 34 - "type": "integer" 35 45 } 36 46 } 37 47 }
+5
tests/codegen/lexicon/implicit_main/warnings@implicit_main.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + []
+8 -1
tests/codegen/lexicon/integer_constraints/expected.json tests/codegen/lexicon/integer_constraints/lexicon@integer_constraints.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: output.json 4 + --- 1 5 { 2 6 "$type": "com.atproto.lexicon.schema", 3 7 "lexicon": 1, ··· 8 12 "key": "tid", 9 13 "record": { 10 14 "type": "object", 11 - "required": ["age", "temperature"], 15 + "required": [ 16 + "age", 17 + "temperature" 18 + ], 12 19 "properties": { 13 20 "age": { 14 21 "type": "integer",
-4
tests/codegen/lexicon/integer_constraints/test.toml
··· 1 - [test] 2 - name = "integer_constraints" 3 - description = "Test integer_constraints" 4 - namespace = "com.example.integer_constraints"
+5
tests/codegen/lexicon/integer_constraints/warnings@integer_constraints.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + []
+10 -2
tests/codegen/lexicon/item_extensions_codegen/expected.json tests/codegen/lexicon/item_extensions_codegen/lexicon@item_extensions_codegen.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: output.json 4 + --- 1 5 { 2 6 "$type": "com.atproto.lexicon.schema", 3 7 "lexicon": 1, ··· 8 12 "key": "tid", 9 13 "record": { 10 14 "type": "object", 11 - "required": ["name"], 15 + "required": [ 16 + "name" 17 + ], 12 18 "properties": { 13 - "name": {"type": "string"} 19 + "name": { 20 + "type": "string" 21 + } 14 22 } 15 23 }, 16 24 "x-deprecated": true,
-4
tests/codegen/lexicon/item_extensions_codegen/test.toml
··· 1 - [test] 2 - name = "item_extensions_codegen" 3 - description = "@const annotations on a record emit as extra JSON fields on the record def" 4 - namespace = "com.example.item_extensions_codegen"
+5
tests/codegen/lexicon/item_extensions_codegen/warnings@item_extensions_codegen.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + []
+23 -13
tests/codegen/lexicon/local_references/expected.json tests/codegen/lexicon/local_references/lexicon@local_references.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: output.json 4 + --- 1 5 { 2 6 "$type": "com.atproto.lexicon.schema", 3 7 "lexicon": 1, 4 8 "id": "com.example.document", 5 9 "defs": { 10 + "metadata": { 11 + "type": "object", 12 + "required": [ 13 + "createdAt", 14 + "updatedAt" 15 + ], 16 + "properties": { 17 + "createdAt": { 18 + "type": "string" 19 + }, 20 + "updatedAt": { 21 + "type": "string" 22 + } 23 + } 24 + }, 6 25 "main": { 7 26 "type": "record", 8 27 "key": "tid", 9 28 "record": { 10 29 "type": "object", 11 - "required": ["title", "meta"], 30 + "required": [ 31 + "title", 32 + "meta" 33 + ], 12 34 "properties": { 13 35 "title": { 14 36 "type": "string" ··· 17 39 "type": "ref", 18 40 "ref": "#metadata" 19 41 } 20 - } 21 - } 22 - }, 23 - "metadata": { 24 - "type": "object", 25 - "required": ["createdAt", "updatedAt"], 26 - "properties": { 27 - "createdAt": { 28 - "type": "string" 29 - }, 30 - "updatedAt": { 31 - "type": "string" 32 42 } 33 43 } 34 44 }
+5
tests/codegen/lexicon/local_references/warnings@local_references.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + []
+16 -4
tests/codegen/lexicon/nested_objects/expected.json tests/codegen/lexicon/nested_objects/lexicon@nested_objects.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: output.json 4 + --- 1 5 { 2 6 "$type": "com.atproto.lexicon.schema", 3 7 "lexicon": 1, ··· 8 12 "key": "tid", 9 13 "record": { 10 14 "type": "object", 11 - "required": ["displayName"], 15 + "required": [ 16 + "displayName" 17 + ], 12 18 "properties": { 13 19 "displayName": { 14 20 "type": "string" 15 21 }, 16 22 "metadata": { 17 23 "type": "object", 18 - "required": ["createdAt"], 24 + "required": [ 25 + "createdAt" 26 + ], 19 27 "properties": { 20 28 "createdAt": { 21 29 "type": "string" 22 30 }, 23 31 "settings": { 24 32 "type": "object", 25 - "required": ["theme"], 33 + "required": [ 34 + "theme" 35 + ], 26 36 "properties": { 27 37 "theme": { 28 38 "type": "string" 29 39 }, 30 40 "notifications": { 31 41 "type": "object", 32 - "required": ["email"], 42 + "required": [ 43 + "email" 44 + ], 33 45 "properties": { 34 46 "email": { 35 47 "type": "boolean"
-4
tests/codegen/lexicon/nested_objects/test.toml
··· 1 - [test] 2 - name = "nested_objects" 3 - description = "Test nested_objects" 4 - namespace = "com.example.nested_objects"
+5
tests/codegen/lexicon/nested_objects/warnings@nested_objects.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + []
+9 -1
tests/codegen/lexicon/record_key_any/expected.json tests/codegen/lexicon/record_key_any/lexicon@record_key_any.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: output.json 4 + --- 1 5 { 2 6 "$type": "com.atproto.lexicon.schema", 3 7 "lexicon": 1, ··· 8 12 "key": "any", 9 13 "record": { 10 14 "type": "object", 11 - "required": ["title", "content", "slug"], 15 + "required": [ 16 + "title", 17 + "content", 18 + "slug" 19 + ], 12 20 "properties": { 13 21 "title": { 14 22 "type": "string"
-4
tests/codegen/lexicon/record_key_any/test.toml
··· 1 - [test] 2 - name = "record_key_any" 3 - description = "Test record_key_any" 4 - namespace = "com.example.record_key_any"
+5
tests/codegen/lexicon/record_key_any/warnings@record_key_any.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + []
+7 -1
tests/codegen/lexicon/record_key_literal/expected.json tests/codegen/lexicon/record_key_literal/lexicon@record_key_literal.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: output.json 4 + --- 1 5 { 2 6 "$type": "com.atproto.lexicon.schema", 3 7 "lexicon": 1, ··· 8 12 "key": "literal:self", 9 13 "record": { 10 14 "type": "object", 11 - "required": ["displayName"], 15 + "required": [ 16 + "displayName" 17 + ], 12 18 "properties": { 13 19 "displayName": { 14 20 "type": "string"
-4
tests/codegen/lexicon/record_key_literal/test.toml
··· 1 - [test] 2 - name = "record_key_literal" 3 - description = "Test record_key_literal" 4 - namespace = "com.example.record_key_literal"
+5
tests/codegen/lexicon/record_key_literal/warnings@record_key_literal.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + []
+10 -2
tests/codegen/lexicon/self_item/expected.json tests/codegen/lexicon/self_item/lexicon@self_item.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: output.json 4 + --- 1 5 { 2 6 "$type": "com.atproto.lexicon.schema", 3 7 "lexicon": 1, ··· 9 13 "defs": { 10 14 "main": { 11 15 "type": "object", 12 - "required": ["value"], 16 + "required": [ 17 + "value" 18 + ], 13 19 "properties": { 14 - "value": {"type": "string"} 20 + "value": { 21 + "type": "string" 22 + } 15 23 } 16 24 } 17 25 }
-4
tests/codegen/lexicon/self_item/test.toml
··· 1 - [test] 2 - name = "self_item" 3 - description = "self {} with docs and @const/@reference extensions emit as top-level JSON fields" 4 - namespace = "com.example.self_item"
+5
tests/codegen/lexicon/self_item/warnings@self_item.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + []
+8 -1
tests/codegen/lexicon/string_constraints/expected.json tests/codegen/lexicon/string_constraints/lexicon@string_constraints.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: output.json 4 + --- 1 5 { 2 6 "$type": "com.atproto.lexicon.schema", 3 7 "lexicon": 1, ··· 8 12 "key": "tid", 9 13 "record": { 10 14 "type": "object", 11 - "required": ["username", "displayName"], 15 + "required": [ 16 + "username", 17 + "displayName" 18 + ], 12 19 "properties": { 13 20 "username": { 14 21 "type": "string",
-4
tests/codegen/lexicon/string_constraints/test.toml
··· 1 - [test] 2 - name = "string_constraints" 3 - description = "Test string_constraints" 4 - namespace = "com.example.string_constraints"
+5
tests/codegen/lexicon/string_constraints/warnings@string_constraints.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + []
-33
tests/codegen/lexicon/union_types/expected.json
··· 1 - { 2 - "$type": "com.atproto.lexicon.schema", 3 - "lexicon": 1, 4 - "id": "com.example.union_types", 5 - "defs": { 6 - "main": { 7 - "type": "record", 8 - "key": "tid", 9 - "record": { 10 - "type": "object", 11 - "required": ["content"], 12 - "properties": { 13 - "content": { 14 - "type": "union", 15 - "refs": [ 16 - {"type": "string"}, 17 - {"type": "integer"}, 18 - {"type": "boolean"} 19 - ] 20 - }, 21 - "media": { 22 - "type": "union", 23 - "refs": [ 24 - {"type": "string"}, 25 - {"type": "integer"} 26 - ], 27 - "closed": true 28 - } 29 - } 30 - } 31 - } 32 - } 33 - }
+49
tests/codegen/lexicon/union_types/lexicon@union_types.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: output.json 4 + --- 5 + { 6 + "$type": "com.atproto.lexicon.schema", 7 + "lexicon": 1, 8 + "id": "com.example.union_types", 9 + "defs": { 10 + "main": { 11 + "type": "record", 12 + "key": "tid", 13 + "record": { 14 + "type": "object", 15 + "required": [ 16 + "content" 17 + ], 18 + "properties": { 19 + "content": { 20 + "type": "union", 21 + "refs": [ 22 + { 23 + "type": "string" 24 + }, 25 + { 26 + "type": "integer" 27 + }, 28 + { 29 + "type": "boolean" 30 + } 31 + ] 32 + }, 33 + "media": { 34 + "type": "union", 35 + "refs": [ 36 + { 37 + "type": "string" 38 + }, 39 + { 40 + "type": "integer" 41 + } 42 + ], 43 + "closed": true 44 + } 45 + } 46 + } 47 + } 48 + } 49 + }
-4
tests/codegen/lexicon/union_types/test.toml
··· 1 - [test] 2 - name = "union_types" 3 - description = "Test union_types" 4 - namespace = "com.example.union_types"
+5
tests/codegen/lexicon/union_types/warnings@union_types.snap
··· 1 + --- 2 + source: tests/codegen_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + []
+26 -50
tests/codegen_integration.rs
··· 1 1 // Workspace-level integration tests for code generation. 2 2 // Each subdirectory of `codegen/lexicon/` is a test fixture containing 3 - // `input.mlf`, `expected.json`, and optionally `test.toml`. 3 + // `input.mlf`, optionally `test.toml`, and insta snapshot files 4 + // (`lexicon.snap`, `warnings.snap`) checked into the fixture directory. 5 + // 6 + // To update snapshots after an intentional change: 7 + // just test-codegen # produces .snap.new files 8 + // just review # interactive accept/reject 4 9 5 10 use mlf_codegen::generate_lexicon; 6 11 use mlf_integration_tests::test_utils; 7 12 use mlf_lang::{parser::parse_lexicon, Workspace}; 8 - use serde_json::Value; 9 13 use std::fs; 10 14 use std::path::Path; 11 15 12 - fn derive_namespace_fallback(test_name: &str) -> String { 13 - match test_name { 14 - "basic_record" => "app.bsky.feed.post".to_string(), 15 - _ => format!("com.example.{}", test_name), 16 - } 17 - } 18 - 19 16 fn run_lexicon_test(input_path: &Path) -> datatest_stable::Result<()> { 20 17 let test_dir = input_path.parent().ok_or("input.mlf has no parent directory")?; 21 - 22 - let config = test_utils::load_test_config(test_dir, derive_namespace_fallback) 23 - .map_err(|e| format!("{}", e))?; 18 + let test_name = test_dir 19 + .file_name() 20 + .and_then(|s| s.to_str()) 21 + .ok_or("input.mlf parent has no name")? 22 + .to_string(); 24 23 25 - let namespace = config 26 - .test 27 - .namespace 28 - .ok_or("No namespace specified in test.toml")?; 24 + let namespace = test_utils::load_test_config(test_dir, |name| format!("com.example.{}", name)) 25 + .ok() 26 + .and_then(|c| c.test.namespace) 27 + .unwrap_or_else(|| format!("com.example.{}", test_name)); 29 28 30 29 let input = fs::read_to_string(input_path)?; 31 30 let lexicon = parse_lexicon(&input).map_err(|e| format!("Failed to parse: {:?}", e))?; ··· 37 36 .map_err(|e| format!("Failed to resolve: {:?}", e))?; 38 37 39 38 let lexicon = ws.get_lexicon(&namespace).ok_or("Module not found")?; 40 - 41 39 let output = generate_lexicon(&namespace, lexicon, &ws); 42 40 43 - let expected_str = fs::read_to_string(test_dir.join("expected.json"))?; 44 - let expected_json: Value = serde_json::from_str(&expected_str)?; 45 - 46 - if output.json != expected_json { 47 - return Err(format!( 48 - "Output mismatch:\nExpected:\n{}\n\nGot:\n{}", 49 - serde_json::to_string_pretty(&expected_json).unwrap(), 50 - serde_json::to_string_pretty(&output.json).unwrap() 51 - ) 52 - .into()); 53 - } 54 - 55 - let warnings_path = test_dir.join("expected_warnings.txt"); 56 - let expected_warnings = if warnings_path.exists() { 57 - fs::read_to_string(&warnings_path)? 58 - } else { 59 - String::new() 60 - }; 61 - let actual_warnings = output 41 + let formatted_warnings: Vec<String> = output 62 42 .warnings 63 43 .iter() 64 44 .map(|w| format!("{}: {}", w.namespace, w.message)) 65 - .collect::<Vec<_>>() 66 - .join("\n"); 67 - let actual_warnings = if actual_warnings.is_empty() { 68 - String::new() 69 - } else { 70 - format!("{}\n", actual_warnings) 71 - }; 72 - if actual_warnings != expected_warnings { 73 - return Err(format!( 74 - "Warnings mismatch:\n--- expected ---\n{}\n--- got ---\n{}", 75 - expected_warnings, actual_warnings 76 - ) 77 - .into()); 78 - } 45 + .collect(); 46 + 47 + insta::with_settings!({ 48 + snapshot_path => test_dir.to_path_buf(), 49 + prepend_module_to_snapshot => false, 50 + snapshot_suffix => test_name, 51 + }, { 52 + insta::assert_json_snapshot!("lexicon", output.json); 53 + insta::assert_debug_snapshot!("warnings", formatted_warnings); 54 + }); 79 55 80 56 Ok(()) 81 57 }
+7
tests/diagnostics/undefined_reference/errors@undefined_reference.snap
··· 1 + --- 2 + source: tests/diagnostics_integration.rs 3 + expression: errors_in_module 4 + --- 5 + [ 6 + "UndefinedReference { name: \"User\", span: Span { start: 27, end: 31 }, module_namespace: \"test.post\" }", 7 + ]
-9
tests/diagnostics/undefined_reference/expected.json
··· 1 - { 2 - "error_count": 1, 3 - "errors": [ 4 - { 5 - "code": "mlf::undefined_reference", 6 - "message": "UndefinedReference" 7 - } 8 - ] 9 - }
+23 -58
tests/diagnostics_integration.rs
··· 1 1 // Workspace-level integration tests for diagnostics. 2 - // Each subdirectory of `diagnostics/` is a fixture with `input.mlf`, 3 - // `expected.json`, and optionally `test.toml`. 2 + // 3 + // Each subdirectory of `diagnostics/` is a fixture with `input.mlf` 4 + // and `test.toml`. Expected diagnostics are captured as an insta 5 + // `.snap` file checked into the fixture directory. 6 + // 7 + // To update snapshots after an intentional change: 8 + // just test-diagnostics # produces .snap.new files 9 + // just review # interactive accept/reject 4 10 5 11 use mlf_diagnostics::{get_error_module_namespace_str, ValidationDiagnostic}; 6 12 use mlf_integration_tests::test_utils; 7 13 use mlf_lang::{parser::parse_lexicon, Workspace}; 8 - use serde::Deserialize; 9 14 use std::fs; 10 15 use std::path::Path; 11 16 12 - #[derive(Debug, Deserialize)] 13 - struct ExpectedDiagnostic { 14 - error_count: usize, 15 - errors: Vec<ExpectedError>, 16 - } 17 - 18 - #[derive(Debug, Deserialize)] 19 - struct ExpectedError { 20 - #[allow(dead_code)] 21 - code: String, 22 - message: String, 23 - #[serde(default)] 24 - #[allow(dead_code)] 25 - span: Option<ExpectedSpan>, 26 - } 27 - 28 - #[derive(Debug, Deserialize)] 29 - struct ExpectedSpan { 30 - #[allow(dead_code)] 31 - start: usize, 32 - #[allow(dead_code)] 33 - end: usize, 34 - } 35 - 36 17 fn run_diagnostics_test(input_path: &Path) -> datatest_stable::Result<()> { 37 18 let test_dir = input_path.parent().ok_or("input.mlf has no parent directory")?; 19 + let test_name = test_dir 20 + .file_name() 21 + .and_then(|s| s.to_str()) 22 + .ok_or("input.mlf parent has no name")? 23 + .to_string(); 38 24 39 25 let config = test_utils::load_test_config(test_dir, |name| format!("test.{}", name)) 40 26 .map_err(|e| format!("{}", e))?; ··· 56 42 Err(errors) => errors, 57 43 }; 58 44 59 - // Construct the diagnostic to ensure the wiring still works, even if we 60 - // don't assert on its rendered form here. 45 + // Construct the diagnostic to verify the wiring compiles and runs. 61 46 let _diagnostic = ValidationDiagnostic::new( 62 47 "input.mlf".to_string(), 63 48 input.clone(), ··· 65 50 validation_errors.clone(), 66 51 ); 67 52 68 - let expected_str = fs::read_to_string(test_dir.join("expected.json"))?; 69 - let expected: ExpectedDiagnostic = serde_json::from_str(&expected_str)?; 70 - 71 - let errors_in_module: Vec<_> = validation_errors 53 + let errors_in_module: Vec<String> = validation_errors 72 54 .errors 73 55 .iter() 74 56 .filter(|e| get_error_module_namespace_str(e) == namespace) 57 + .map(|e| format!("{:?}", e)) 75 58 .collect(); 76 59 77 - if errors_in_module.len() != expected.error_count { 78 - return Err(format!( 79 - "Expected {} errors but got {}", 80 - expected.error_count, 81 - errors_in_module.len() 82 - ) 83 - .into()); 84 - } 85 - 86 - for (i, expected_error) in expected.errors.iter().enumerate() { 87 - let actual_error = errors_in_module 88 - .get(i) 89 - .ok_or_else(|| format!("Expected error #{} but only got {} errors", i + 1, errors_in_module.len()))?; 90 - 91 - let actual_message = format!("{:?}", actual_error); 92 - if !actual_message.contains(&expected_error.message) { 93 - return Err(format!( 94 - "Error #{}: Expected message to contain '{}' but got: {}", 95 - i + 1, 96 - expected_error.message, 97 - actual_message 98 - ) 99 - .into()); 100 - } 101 - } 60 + insta::with_settings!({ 61 + snapshot_path => test_dir.to_path_buf(), 62 + prepend_module_to_snapshot => false, 63 + snapshot_suffix => test_name, 64 + }, { 65 + insta::assert_debug_snapshot!("errors", errors_in_module); 66 + }); 102 67 103 68 Ok(()) 104 69 }
+4 -1
tests/lexicon_to_mlf/all_constraint_keys/expected.mlf tests/lexicon_to_mlf/all_constraint_keys/mlf@all_constraint_keys.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: output.mlf 4 + --- 1 5 def type version = integer constrained { 2 6 minimum: 1, 3 7 maximum: 10, ··· 35 39 minLength: 32, 36 40 maxLength: 64, 37 41 }; 38 -
+5
tests/lexicon_to_mlf/all_constraint_keys/warnings@all_constraint_keys.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + []
+4 -1
tests/lexicon_to_mlf/array_item_constraints/expected.mlf tests/lexicon_to_mlf/array_item_constraints/mlf@array_item_constraints.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: output.mlf 4 + --- 1 5 def type tags = (string constrained { 2 6 minLength: 1, 3 7 maxLength: 200, 4 8 })[] constrained { 5 9 maxLength: 20, 6 10 }; 7 -
+5
tests/lexicon_to_mlf/array_item_constraints/warnings@array_item_constraints.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + []
-2
tests/lexicon_to_mlf/array_union_items/expected.mlf
··· 1 - def type feed = (post | repost)[]; 2 -
+5
tests/lexicon_to_mlf/array_union_items/mlf@array_union_items.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: output.mlf 4 + --- 5 + def type feed = (post | repost)[];
+5
tests/lexicon_to_mlf/array_union_items/warnings@array_union_items.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + []
-7
tests/lexicon_to_mlf/item_extensions/expected.mlf
··· 1 - @main 2 - @const("x-deprecated", true) 3 - @const("x-since", "2024-01-01") 4 - record itemext { 5 - name!: string, 6 - } 7 -
+10
tests/lexicon_to_mlf/item_extensions/mlf@item_extensions.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: output.mlf 4 + --- 5 + @main 6 + @const("x-deprecated", true) 7 + @const("x-since", "2024-01-01") 8 + record itemext { 9 + name!: string, 10 + }
+5
tests/lexicon_to_mlf/item_extensions/warnings@item_extensions.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + []
-2
tests/lexicon_to_mlf/lenient_array_without_items/expected.mlf
··· 1 - def type tags = unknown[]; 2 -
-1
tests/lexicon_to_mlf/lenient_array_without_items/expected_warnings.txt
··· 1 - com.example.arraynoitems: array type is missing `items` field; treating item type as `unknown` (ATProto spec lists `items` as required)
+5
tests/lexicon_to_mlf/lenient_array_without_items/mlf@lenient_array_without_items.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: output.mlf 4 + --- 5 + def type tags = unknown[];
+7
tests/lexicon_to_mlf/lenient_array_without_items/warnings@lenient_array_without_items.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + [ 6 + "com.example.arraynoitems: array type is missing `items` field; treating item type as `unknown` (ATProto spec lists `items` as required)", 7 + ]
-2
tests/lexicon_to_mlf/lenient_empty_union_refs/expected.mlf
··· 1 - def type items = unknown[]; 2 -
-1
tests/lexicon_to_mlf/lenient_empty_union_refs/expected_warnings.txt
··· 1 - com.example.emptyunion: open union has no `refs`; emitting `unknown` as a placeholder (ATProto spec lists `refs` as required on union types)
+5
tests/lexicon_to_mlf/lenient_empty_union_refs/mlf@lenient_empty_union_refs.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: output.mlf 4 + --- 5 + def type items = unknown[];
+7
tests/lexicon_to_mlf/lenient_empty_union_refs/warnings@lenient_empty_union_refs.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + [ 6 + "com.example.emptyunion: open union has no `refs`; emitting `unknown` as a placeholder (ATProto spec lists `refs` as required on union types)", 7 + ]
-4
tests/lexicon_to_mlf/lenient_object_without_properties/expected.mlf
··· 1 - /// Marker for bold text 2 - def type bold = { 3 - }; 4 -
-1
tests/lexicon_to_mlf/lenient_object_without_properties/expected_warnings.txt
··· 1 - com.example.marker: object type is missing `properties` field; treating as empty (ATProto spec lists `properties` as required)
+7
tests/lexicon_to_mlf/lenient_object_without_properties/mlf@lenient_object_without_properties.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: output.mlf 4 + --- 5 + /// Marker for bold text 6 + def type bold = { 7 + };
+7
tests/lexicon_to_mlf/lenient_object_without_properties/warnings@lenient_object_without_properties.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + [ 6 + "com.example.marker: object type is missing `properties` field; treating as empty (ATProto spec lists `properties` as required)", 7 + ]
-9
tests/lexicon_to_mlf/main_renamed_local_ref/expected.mlf
··· 1 - @main 2 - def type thing = { 3 - value!: string, 4 - }; 5 - 6 - def type container = { 7 - items!: thing[], 8 - }; 9 -
+12
tests/lexicon_to_mlf/main_renamed_local_ref/mlf@main_renamed_local_ref.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: output.mlf 4 + --- 5 + @main 6 + def type thing = { 7 + value!: string, 8 + }; 9 + 10 + def type container = { 11 + items!: thing[], 12 + };
+5
tests/lexicon_to_mlf/main_renamed_local_ref/warnings@main_renamed_local_ref.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + []
+4 -1
tests/lexicon_to_mlf/nullable_fields/expected.mlf tests/lexicon_to_mlf/nullable_fields/mlf@nullable_fields.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: output.mlf 4 + --- 1 5 @main 2 6 record profile { 3 7 handle!: string, ··· 11 15 theme!: string, 12 16 accent: string | null, 13 17 }; 14 -
+5
tests/lexicon_to_mlf/nullable_fields/warnings@nullable_fields.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + []
-6
tests/lexicon_to_mlf/ref_hint_warning/expected.mlf
··· 1 - @main 2 - @const("xFallback", "com.example.other#someType") 3 - def type refhint = { 4 - value: string, 5 - }; 6 -
-1
tests/lexicon_to_mlf/ref_hint_warning/expected_warnings.txt
··· 1 - com.example.refhint: extension field "xFallback" has value "com.example.other#someType" which looks NSID-shaped; emitted as `@const` — consider `@reference` if you intend workspace name resolution when hand-editing the MLF
+9
tests/lexicon_to_mlf/ref_hint_warning/mlf@ref_hint_warning.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: output.mlf 4 + --- 5 + @main 6 + @const("xFallback", "com.example.other#someType") 7 + def type refhint = { 8 + value: string, 9 + };
+7
tests/lexicon_to_mlf/ref_hint_warning/warnings@ref_hint_warning.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + [ 6 + "com.example.refhint: extension field \"xFallback\" has value \"com.example.other#someType\" which looks NSID-shaped; emitted as `@const` — consider `@reference` if you intend workspace name resolution when hand-editing the MLF", 7 + ]
-8
tests/lexicon_to_mlf/self_top_level_description/expected.mlf
··· 1 - /// A short description of the whole lexicon. 2 - self {} 3 - 4 - @main 5 - def type described = { 6 - name!: string, 7 - }; 8 -
+11
tests/lexicon_to_mlf/self_top_level_description/mlf@self_top_level_description.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: output.mlf 4 + --- 5 + /// A short description of the whole lexicon. 6 + self {} 7 + 8 + @main 9 + def type described = { 10 + name!: string, 11 + };
+5
tests/lexicon_to_mlf/self_top_level_description/warnings@self_top_level_description.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + []
+4 -1
tests/lexicon_to_mlf/self_top_level_extensions/expected.mlf tests/lexicon_to_mlf/self_top_level_extensions/mlf@self_top_level_extensions.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: output.mlf 4 + --- 1 5 /// With extensions. 2 6 @const("revision", 3) 3 7 @const("x-vendor-flag", true) ··· 8 12 def type extended = { 9 13 value: string, 10 14 }; 11 -
+5
tests/lexicon_to_mlf/self_top_level_extensions/warnings@self_top_level_extensions.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + []
+4 -1
tests/lexicon_to_mlf/unknown_def_type_passthrough/expected.mlf tests/lexicon_to_mlf/unknown_def_type_passthrough/mlf@unknown_def_type_passthrough.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: output.mlf 4 + --- 1 5 @main 2 6 @const("type", "permission-set") 3 7 @const("title", "Example Access") 4 8 @const("detail", "Access to example resources.") 5 9 @const("permissions", [{ "type": "permission", "resource": "repo", "collection": ["com.example.one", "com.example.two"] }]) 6 10 def type permission = unknown; 7 -
+5
tests/lexicon_to_mlf/unknown_def_type_passthrough/warnings@unknown_def_type_passthrough.snap
··· 1 + --- 2 + source: tests/lexicon_to_mlf_integration.rs 3 + expression: formatted_warnings 4 + --- 5 + []
+23 -36
tests/lexicon_to_mlf_integration.rs
··· 1 1 // Workspace-level integration tests for Lexicon JSON → MLF conversion. 2 2 // 3 - // Each subdirectory of `lexicon_to_mlf/` is a single test case containing: 4 - // - `input.json`: the Lexicon JSON to convert 5 - // - `expected.mlf`: the expected MLF source produced by the converter 6 - // - `expected_warnings.txt` (optional): the expected warnings, one 7 - // per line in `<namespace>: <message>` form. When absent, the test 8 - // asserts no warnings were produced. 3 + // Each subdirectory of `lexicon_to_mlf/` is a test case with an 4 + // `input.json`. Expected output (MLF source + converter warnings) is 5 + // captured as insta `.snap` files checked into the fixture directory. 6 + // 7 + // To update snapshots after an intentional change: 8 + // just test-lexicon-to-mlf # produces .snap.new files 9 + // just review # interactive accept/reject 9 10 10 11 use mlf_cli::generate::mlf::generate_mlf_from_json; 11 12 use serde_json::Value; ··· 14 15 15 16 fn run_case(input_path: &Path) -> datatest_stable::Result<()> { 16 17 let test_dir = input_path.parent().ok_or("input.json has no parent directory")?; 18 + let test_name = test_dir 19 + .file_name() 20 + .and_then(|s| s.to_str()) 21 + .ok_or("input.json parent has no name")? 22 + .to_string(); 17 23 18 24 let input = fs::read_to_string(input_path)?; 19 25 let json: Value = serde_json::from_str(&input)?; 20 26 21 27 let output = generate_mlf_from_json(&json).map_err(|e| format!("{:?}", e))?; 22 28 23 - let expected = fs::read_to_string(test_dir.join("expected.mlf"))?; 24 - if output.mlf != expected { 25 - return Err(format!( 26 - "MLF mismatch:\n--- expected ---\n{}\n--- got ---\n{}", 27 - expected, output.mlf 28 - ) 29 - .into()); 30 - } 31 - 32 - let warnings_path = test_dir.join("expected_warnings.txt"); 33 - let expected_warnings = if warnings_path.exists() { 34 - fs::read_to_string(&warnings_path)? 35 - } else { 36 - String::new() 37 - }; 38 - let actual_warnings = output 29 + let formatted_warnings: Vec<String> = output 39 30 .warnings 40 31 .iter() 41 32 .map(|w| format!("{}: {}", w.namespace, w.message)) 42 - .collect::<Vec<_>>() 43 - .join("\n"); 44 - let actual_warnings = if actual_warnings.is_empty() { 45 - String::new() 46 - } else { 47 - format!("{}\n", actual_warnings) 48 - }; 49 - if actual_warnings != expected_warnings { 50 - return Err(format!( 51 - "Warnings mismatch:\n--- expected ---\n{}\n--- got ---\n{}", 52 - expected_warnings, actual_warnings 53 - ) 54 - .into()); 55 - } 33 + .collect(); 34 + 35 + insta::with_settings!({ 36 + snapshot_path => test_dir.to_path_buf(), 37 + prepend_module_to_snapshot => false, 38 + snapshot_suffix => test_name, 39 + }, { 40 + insta::assert_snapshot!("mlf", output.mlf); 41 + insta::assert_debug_snapshot!("warnings", formatted_warnings); 42 + }); 56 43 57 44 Ok(()) 58 45 }