Skip to content

Commit 09a6ed1

Browse files
committed
Add property-based codec law tests
1 parent 4b13040 commit 09a6ed1

3 files changed

Lines changed: 149 additions & 22 deletions

File tree

TASKS.md

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ Completed rename, parser, bridge, compatibility, JSON Schema, docs, and projecti
1414
- Support both C# and F# records/classes as generator inputs.
1515
- Prefer readable checked-in output over opaque build-only generation.
1616
- Keep the generator in a separate `.NET`-only project so reflection-heavy analysis and templates do not bleed into the AOT/Fable-safe core assembly.
17+
- Generate ordinary checked-in F# schema code rather than introducing a second runtime schema system.
18+
- Treat CLR-model analysis, JSON-example scaffolding, and imported-contract scaffolding as `.NET`-only tooling layered on top of the stable runtime DSL.
19+
- Keep generated output reviewable and copy-editable by users.
20+
21+
- [x] **Task 30:** Fixed the docs-site asset root by aligning `PackageProjectUrl` with the GitHub Pages URL instead of the repo URL, and hardened `scripts/generate-api-docs.sh` to clear stale `fsdocs` cache, build the doc assemblies first, and fail if generated output points theme/search assets at `github.com/adz/CodecMapper/...`.
1722

1823
- [x] **Task 29:** Split `src/CodecMapper/Library.fs` into explicit dependency-ordered files (`Core.fs`, `Schema.fs`, `Json.fs`, `JsonSchema.fs`, `Xml.fs`, `KeyValue.fs`, and `Yaml.fs`) and updated `CodecMapper.fsproj` to preserve the existing no-behavior-change compilation order.
1924

@@ -23,25 +28,6 @@ Completed rename, parser, bridge, compatibility, JSON Schema, docs, and projecti
2328

2429
- [x] **Task 33:** Added a canonical contract-pattern guide covering basic records, nested records, validated wrappers, versioned contracts, config contracts, JSON Schema import, and the C# bridge, and linked it from the README and docs landing pages so the copy-paste patterns are easy to find.
2530

26-
- [ ] **Task 34: Keep Task 18 focused on build-time code generation**
27-
- Generate ordinary checked-in F# schema code rather than introducing a second runtime schema system.
28-
- Treat CLR-model analysis, JSON-example scaffolding, and imported-contract scaffolding as `.NET`-only tooling layered on top of the stable runtime DSL.
29-
- Keep generated output reviewable and copy-editable by users.
30-
31-
- [ ] **Task 30: Fix the published docs site asset loading**
32-
- Reproduce the generated `fsdocs` output locally and identify why theme/search assets are being loaded from blocked cross-origin URLs.
33-
- Make the published site self-contained or otherwise serve its JS/CSS assets from paths that work on GitHub Pages without CORS failures.
34-
- Add a verification step so docs generation catches broken asset references before publishing.
35-
36-
- [x] **Task 30:** Fixed the docs-site asset root by aligning `PackageProjectUrl` with the GitHub Pages URL instead of the repo URL, and hardened `scripts/generate-api-docs.sh` to clear stale `fsdocs` cache, build the doc assemblies first, and fail if generated output points theme/search assets at `github.com/adz/CodecMapper/...`.
37-
38-
- [ ] **Task 35: Add property-based test coverage for codec laws**
39-
- Add property-based tests for the real F# implementation rather than a sidecar model, since the main risks here are semantic drift, parser edge cases, and encode/decode symmetry across many inputs.
40-
- Start with fixed representative schemas that already exist in the repo, then generate values for them: primitives, nested records, options, validated wrappers, collections, and numeric boundary cases.
41-
- Make round-trip laws the first goal: `deserialize (serialize x) = x` for JSON and XML wherever the format supports the same shape.
42-
- Add parser robustness properties for malformed inputs so failures stay deterministic and do not hang, over-consume input, or silently accept trailing content.
43-
- Add format-symmetry properties where appropriate so one authored schema preserves the same semantic value across JSON and XML.
44-
- Prefer `FsCheck.Xunit` in `tests/CodecMapper.Tests` so the property layer stays close to the existing xUnit and `Swensen.Unquote` test style.
45-
- Keep the current example-based parser tests for exact regressions and expected error text; property tests should expand coverage, not replace those focused cases.
46-
- Avoid starting with arbitrary recursive schema generation. The first iteration should optimize for debuggable failures and useful shrinking, not maximal generator cleverness.
47-
- Treat generator design as part of the contract: keep generated values inside the supported deterministic surface instead of exploring JSON/XML features that the library intentionally leaves out.
31+
- [x] **Task 35: Add property-based test coverage for codec laws**
32+
- Added `FsCheck.Xunit`-backed round-trip properties in `tests/CodecMapper.Tests` for representative nested-record, option, and collection schemas across both JSON and XML.
33+
- Kept the generators inside the supported deterministic surface so failures stay debuggable and align with the library's intentional JSON/XML subset.

tests/CodecMapper.Tests/CodecMapper.Tests.fsproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@
2424
<Compile Include="JsonSchemaImportTests.fs" />
2525
<Compile Include="CSharpBridgeTests.fs" />
2626
<Compile Include="CSharpFacadeTests.fs" />
27+
<Compile Include="PropertyTests.fs" />
2728
</ItemGroup>
2829

2930
<ItemGroup>
3031
<PackageReference Include="coverlet.collector" Version="6.0.4" />
32+
<PackageReference Include="FsCheck.Xunit" Version="3.3.2" />
3133
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
3234
<PackageReference Include="Unquote" Version="7.0.1" />
3335
<PackageReference Include="xunit" Version="2.9.3" />
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
module PropertyTests
2+
3+
open System
4+
open FsCheck
5+
open FsCheck.Xunit
6+
open CodecMapper
7+
open TestCommon
8+
9+
let private addressSchema =
10+
Schema.define<Address>
11+
|> Schema.construct makeAddress
12+
|> Schema.field "street" _.Street
13+
|> Schema.field "city" _.City
14+
|> Schema.build
15+
16+
let private personSchema =
17+
Schema.define<Person>
18+
|> Schema.construct makePerson
19+
|> Schema.field "id" _.Id
20+
|> Schema.field "name" _.Name
21+
|> Schema.fieldWith "home" _.Home addressSchema
22+
|> Schema.build
23+
24+
let private optionalRecordSchema =
25+
Schema.define<OptionalRecord>
26+
|> Schema.construct makeOptionalRecord
27+
|> Schema.field "nickname" _.Nickname
28+
|> Schema.field "age" _.Age
29+
|> Schema.build
30+
31+
let private collectionSchema =
32+
Schema.define<CollectionRecord>
33+
|> Schema.construct makeCollectionRecord
34+
|> Schema.field "list" _.List
35+
|> Schema.field "array" _.Array
36+
|> Schema.build
37+
38+
type private PropertyArbitraries =
39+
static member private SafeText() : Arbitrary<string> =
40+
let safeChar: Gen<char> =
41+
FsCheck.FSharp.Gen.elements ([ 'a' .. 'z' ] @ [ 'A' .. 'Z' ] @ [ '0' .. '9' ] @ [ ' '; '-'; '_'; '.' ])
42+
43+
let generator: Gen<string> =
44+
FsCheck.FSharp.GenBuilder.gen {
45+
let! length = FsCheck.FSharp.Gen.choose (0, 24)
46+
let! chars = FsCheck.FSharp.Gen.arrayOfLength length safeChar
47+
return String(chars: char array)
48+
}
49+
50+
FsCheck.FSharp.Arb.fromGen generator
51+
52+
static member Address() : Arbitrary<Address> =
53+
let safeText: Gen<string> = PropertyArbitraries.SafeText().Generator
54+
55+
let generator: Gen<Address> =
56+
FsCheck.FSharp.GenBuilder.gen {
57+
let! street = safeText
58+
let! city = safeText
59+
return { Street = street; City = city }
60+
}
61+
62+
FsCheck.FSharp.Arb.fromGen generator
63+
64+
static member Person() : Arbitrary<Person> =
65+
let safeText: Gen<string> = PropertyArbitraries.SafeText().Generator
66+
let addressGen: Gen<Address> = PropertyArbitraries.Address().Generator
67+
68+
let generator: Gen<Person> =
69+
FsCheck.FSharp.GenBuilder.gen {
70+
let! id = FsCheck.FSharp.Gen.choose (-5000, 5000)
71+
let! name = safeText
72+
let! home = addressGen
73+
return { Id = id; Name = name; Home = home }
74+
}
75+
76+
FsCheck.FSharp.Arb.fromGen generator
77+
78+
static member OptionalRecord() : Arbitrary<OptionalRecord> =
79+
let safeText: Gen<string> = PropertyArbitraries.SafeText().Generator
80+
81+
let safeOption (generator: Gen<'T>) : Gen<'T option> =
82+
FsCheck.FSharp.Gen.frequency (
83+
[
84+
1, FsCheck.FSharp.Gen.constant None
85+
3, FsCheck.FSharp.Gen.map Some generator
86+
]
87+
)
88+
89+
let generator: Gen<OptionalRecord> =
90+
FsCheck.FSharp.GenBuilder.gen {
91+
let! nickname = safeOption safeText
92+
let! age = safeOption (FsCheck.FSharp.Gen.choose (-5000, 5000))
93+
return { Nickname = nickname; Age = age }
94+
}
95+
96+
FsCheck.FSharp.Arb.fromGen generator
97+
98+
static member CollectionRecord() : Arbitrary<CollectionRecord> =
99+
let safeText: Gen<string> = PropertyArbitraries.SafeText().Generator
100+
101+
let generator: Gen<CollectionRecord> =
102+
FsCheck.FSharp.GenBuilder.gen {
103+
let! values = FsCheck.FSharp.Gen.listOf (FsCheck.FSharp.Gen.choose (-100, 100))
104+
let! aliases = FsCheck.FSharp.Gen.map List.toArray (FsCheck.FSharp.Gen.listOf safeText)
105+
return { List = values; Array = aliases }
106+
}
107+
108+
FsCheck.FSharp.Arb.fromGen generator
109+
110+
let private jsonPersonCodec = Json.compile personSchema
111+
let private xmlPersonCodec = Xml.compile personSchema
112+
let private jsonOptionalCodec = Json.compile optionalRecordSchema
113+
let private xmlOptionalCodec = Xml.compile optionalRecordSchema
114+
let private jsonCollectionCodec = Json.compile collectionSchema
115+
let private xmlCollectionCodec = Xml.compile collectionSchema
116+
117+
[<Property(Arbitrary = [| typeof<PropertyArbitraries> |], MaxTest = 100)>]
118+
let ``Person round-trips through JSON`` (value: Person) =
119+
Json.deserialize jsonPersonCodec (Json.serialize jsonPersonCodec value) = value
120+
121+
[<Property(Arbitrary = [| typeof<PropertyArbitraries> |], MaxTest = 100)>]
122+
let ``Person round-trips through XML`` (value: Person) =
123+
Xml.deserialize xmlPersonCodec (Xml.serialize xmlPersonCodec value) = value
124+
125+
[<Property(Arbitrary = [| typeof<PropertyArbitraries> |], MaxTest = 100)>]
126+
let ``Optional records round-trip through JSON`` (value: OptionalRecord) =
127+
Json.deserialize jsonOptionalCodec (Json.serialize jsonOptionalCodec value) = value
128+
129+
[<Property(Arbitrary = [| typeof<PropertyArbitraries> |], MaxTest = 100)>]
130+
let ``Optional records round-trip through XML`` (value: OptionalRecord) =
131+
Xml.deserialize xmlOptionalCodec (Xml.serialize xmlOptionalCodec value) = value
132+
133+
[<Property(Arbitrary = [| typeof<PropertyArbitraries> |], MaxTest = 100)>]
134+
let ``Collection records round-trip through JSON`` (value: CollectionRecord) =
135+
Json.deserialize jsonCollectionCodec (Json.serialize jsonCollectionCodec value) = value
136+
137+
[<Property(Arbitrary = [| typeof<PropertyArbitraries> |], MaxTest = 100)>]
138+
let ``Collection records round-trip through XML`` (value: CollectionRecord) =
139+
Xml.deserialize xmlCollectionCodec (Xml.serialize xmlCollectionCodec value) = value

0 commit comments

Comments
 (0)