Skip to content

Commit 324abf6

Browse files
committed
test: guards for wrapped-lib soundness under non-trivial wrapping
Two forward-looking guard files in `test/blackbox-tests/test-cases/ per-module-lib-deps/`. Each file mixes soundness cases (must keep rebuilding) with a forward-looking pin on current per-library filter behaviour (currently rebuilds the consumer when an unreferenced sibling module changes; will need promotion once per-module narrowing within a library lands): - `wrapped-transition-soundness.t`: a consumer reaches an inner module of a `(wrapped (transition ...))` library through the wrapper alias `Wrapped_lib.Inner_a.x`. Case 1 (soundness) pins that the consumer rebuilds when the referenced inner module's interface changes — its compile-rule deps must cover `wrapped_lib__Inner_a.cmi` (the mangled inner-module artifact, not the un-prefixed transition compat shim), not only the wrapper's `.cmi`. Case 2 (narrowing pin) covers an unreferenced sibling (`inner_b`). - `wrapped-from-vlib-soundness.t`: a consumer depends on a virtual-library implementation that inherits its `(wrapped ...)` setting from the vlib (the impl does not redeclare `wrapped`). Cases 1–2 (soundness) pin that the consumer rebuilds when a concrete vlib module (`helper`) or a virtual module (`virt_iface`) has its interface change — its compile-rule deps must cover the impl's `.cmi` directory. Case 3 (narrowing pin) covers an unreferenced sibling (`unused`). The soundness cases hold trivially today via the cctx-wide compile-rule glob over each dep library's `.cmi` directory. Future changes that narrow compile-rule deps per-module must keep that coverage for the referenced-module / inherited-wrapped-library edge cases (the soundness cases); the narrowing pins will flip when the narrowing lands and should be promoted then. Test structure: jq regexes are anchored to the objdir (`\.consumer\.objs/byte/consumer\.cm` and `consumer/\.main\.eobjs/byte/`) rather than relying on dune's internal mangling. Signed-off-by: Robin Bate Boerop <me@robinbb.com>
1 parent 824816b commit 324abf6

2 files changed

Lines changed: 245 additions & 0 deletions

File tree

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
Regression guards for soundness, with a forward-looking pin on
2+
current behaviour, all when a consumer depends on an implementation
3+
that inherits its `(wrapped ...)` setting from the virtual library
4+
(the implementation does not redeclare `wrapped`).
5+
6+
`vlib` declares `(wrapped true)` with a virtual module `virt_iface`
7+
and concrete siblings `helper` and `unused`. `impl` implements
8+
`vlib` without redeclaring `wrapped`. The executable `main` depends
9+
on `impl` and reaches `virt_iface` and `helper` via the vlib
10+
wrapper: `Vlib.Virt_iface.x` and `Vlib.Helper.h`. `main` does not
11+
reference `unused`.
12+
13+
The implementation's closure includes `virt_iface`'s impl and
14+
`vlib`'s concrete modules. `main`'s compile rule must therefore
15+
cover `vlib__Virt_iface.cmi` and `vlib__Helper.cmi`. Any future
16+
per-module narrowing that treats inherited-wrapped libraries as
17+
ordinary local libraries must still keep that coverage; otherwise
18+
a change to either module's interface fails to invalidate `main`.
19+
20+
$ make_dune_project 3.23
21+
22+
$ mkdir vlib impl consumer
23+
24+
$ cat > vlib/dune <<EOF
25+
> (library
26+
> (name vlib)
27+
> (wrapped true)
28+
> (virtual_modules virt_iface))
29+
> EOF
30+
$ cat > vlib/virt_iface.mli <<EOF
31+
> val x : string
32+
> EOF
33+
$ cat > vlib/helper.ml <<EOF
34+
> let h = "h"
35+
> let z = 42
36+
> EOF
37+
$ cat > vlib/helper.mli <<EOF
38+
> val h : string
39+
> EOF
40+
$ cat > vlib/unused.ml <<EOF
41+
> let u = "u"
42+
> let w = "w"
43+
> EOF
44+
$ cat > vlib/unused.mli <<EOF
45+
> val u : string
46+
> EOF
47+
48+
$ cat > impl/dune <<EOF
49+
> (library
50+
> (name impl)
51+
> (implements vlib))
52+
> EOF
53+
$ cat > impl/virt_iface.ml <<EOF
54+
> let x = "impl"
55+
> let z = 42
56+
> EOF
57+
58+
$ cat > consumer/dune <<EOF
59+
> (executable
60+
> (name main)
61+
> (libraries impl))
62+
> EOF
63+
$ cat > consumer/main.ml <<EOF
64+
> let () = print_string Vlib.Virt_iface.x; print_string Vlib.Helper.h
65+
> EOF
66+
67+
$ dune build @check
68+
69+
Case 1 (soundness): edit `helper`'s interface to expose `z`. `main`
70+
reaches `helper` through the vlib wrapper; the compile-rule deps
71+
must cover `vlib__Helper.cmi`, so `main` rebuilds:
72+
73+
$ cat > vlib/helper.mli <<EOF
74+
> val h : string
75+
> val z : int
76+
> EOF
77+
$ dune build @check
78+
$ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("consumer/\\.main\\.eobjs/byte/"))]'
79+
[
80+
{
81+
"target_files": [
82+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmi",
83+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmti"
84+
]
85+
},
86+
{
87+
"target_files": [
88+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmo",
89+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmt"
90+
]
91+
}
92+
]
93+
94+
Case 2 (soundness): edit `virt_iface`'s interface to expose `z`.
95+
`main` reaches `virt_iface` through the vlib wrapper; the compile-
96+
rule deps must cover `vlib__Virt_iface.cmi`, so `main` rebuilds:
97+
98+
$ cat > vlib/virt_iface.mli <<EOF
99+
> val x : string
100+
> val z : int
101+
> EOF
102+
$ dune build @check
103+
$ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("consumer/\\.main\\.eobjs/byte/"))]'
104+
[
105+
{
106+
"target_files": [
107+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmi",
108+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmti"
109+
]
110+
},
111+
{
112+
"target_files": [
113+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmo",
114+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmt"
115+
]
116+
}
117+
]
118+
119+
Case 3 (forward-looking pin on current behaviour): edit `unused`'s
120+
interface to expose `w`. `main` does not reference `unused`, so
121+
under a future per-module narrowing this edit would not rebuild
122+
`main`. Today, the per-library filter rebuilds `main` anyway
123+
because `impl`'s `.cmi` glob covers every module of the
124+
implementation's closure. Promote when per-module narrowing within
125+
a library lands.
126+
127+
$ cat > vlib/unused.mli <<EOF
128+
> val u : string
129+
> val w : string
130+
> EOF
131+
$ dune build @check
132+
$ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("consumer/\\.main\\.eobjs/byte/"))]'
133+
[
134+
{
135+
"target_files": [
136+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmi",
137+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmti"
138+
]
139+
},
140+
{
141+
"target_files": [
142+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmo",
143+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmt"
144+
]
145+
}
146+
]
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
Regression guard for wrapped-library soundness, with a forward-
2+
looking pin on current behaviour, both under
3+
`(wrapped (transition ...))`.
4+
5+
`wrapped_lib` uses `(wrapped (transition ...))` with inner modules
6+
`inner_a` and `inner_b` plus a hand-written wrapper module that
7+
aliases both. The library `consumer` depends on `wrapped_lib` and
8+
writes `Wrapped_lib.Inner_a.x` — naming the wrapper and one inner
9+
module but not the other.
10+
11+
The wrapper's `.cmi` only carries an alias name; the type lives in
12+
the inner module's mangled artifact `wrapped_lib__Inner_a.cmi` (not
13+
the `inner_a.cmi` transition shim). So `consumer`'s compile rule
14+
must cover `wrapped_lib__Inner_a.cmi` alongside `wrapped_lib.cmi`.
15+
Any future per-module narrowing of compile-rule deps must keep that
16+
coverage; otherwise a change to `inner_a`'s interface fails to
17+
invalidate `consumer`.
18+
19+
$ make_dune_project 3.23
20+
21+
$ cat > dune <<EOF
22+
> (library
23+
> (name wrapped_lib)
24+
> (wrapped (transition "use Wrapped_lib.X instead of X"))
25+
> (modules wrapped_lib inner_a inner_b))
26+
> (library
27+
> (name consumer)
28+
> (modules consumer)
29+
> (libraries wrapped_lib))
30+
> EOF
31+
32+
$ cat > wrapped_lib.ml <<EOF
33+
> module Inner_a = Inner_a
34+
> module Inner_b = Inner_b
35+
> EOF
36+
$ cat > inner_a.ml <<EOF
37+
> let x = "a"
38+
> let z = 42
39+
> EOF
40+
$ cat > inner_a.mli <<EOF
41+
> val x : string
42+
> EOF
43+
$ cat > inner_b.ml <<EOF
44+
> let y = "b"
45+
> let w = "w"
46+
> EOF
47+
$ cat > inner_b.mli <<EOF
48+
> val y : string
49+
> EOF
50+
51+
$ cat > consumer.ml <<EOF
52+
> let _ = Wrapped_lib.Inner_a.x
53+
> EOF
54+
55+
$ dune build @check
56+
57+
Case 1 (soundness): edit `inner_a`'s interface to expose `z`.
58+
`consumer` reaches `inner_a` through the wrapper `Wrapped_lib`;
59+
the compile-rule deps must cover `wrapped_lib__Inner_a.cmi`, so
60+
`consumer` rebuilds:
61+
62+
$ cat > inner_a.mli <<EOF
63+
> val x : string
64+
> val z : int
65+
> EOF
66+
$ dune build @check
67+
$ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("\\.consumer\\.objs/byte/consumer\\.cm"))]'
68+
[
69+
{
70+
"target_files": [
71+
"_build/default/.consumer.objs/byte/consumer.cmi",
72+
"_build/default/.consumer.objs/byte/consumer.cmo",
73+
"_build/default/.consumer.objs/byte/consumer.cmt"
74+
]
75+
}
76+
]
77+
78+
Case 2 (forward-looking pin on current behaviour): edit `inner_b`'s
79+
interface to expose `w`. `consumer` does not reference `inner_b`,
80+
so under a future per-module narrowing this edit would not rebuild
81+
`consumer`. Today, the per-library filter rebuilds `consumer`
82+
anyway because `wrapped_lib`'s `.cmi` glob covers every module.
83+
Promote when per-module narrowing within a library lands.
84+
85+
$ cat > inner_b.mli <<EOF
86+
> val y : string
87+
> val w : string
88+
> EOF
89+
$ dune build @check
90+
$ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("\\.consumer\\.objs/byte/consumer\\.cm"))]'
91+
[
92+
{
93+
"target_files": [
94+
"_build/default/.consumer.objs/byte/consumer.cmi",
95+
"_build/default/.consumer.objs/byte/consumer.cmo",
96+
"_build/default/.consumer.objs/byte/consumer.cmt"
97+
]
98+
}
99+
]

0 commit comments

Comments
 (0)