A human-friendly DSL for ATProto Lexicons
27
fork

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 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 }