Skip to content

Commit cf2f7f2

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 cf2f7f2

2 files changed

Lines changed: 257 additions & 0 deletions

File tree

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
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.24
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+
Glob coverage on `main.cmi`'s compile rule:
70+
71+
$ dune rules --root . --format=json --deps _build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmi |
72+
> jq -r 'include "dune"; .[] | depsGlobs | "\(.dir_kind) \(.dir) \(.predicate)"'
73+
In_build_dir _build/default/impl/.impl.objs/byte *.cmi
74+
75+
Case 1 (soundness): edit `helper`'s interface to expose `z`. `main`
76+
reaches `helper` through the vlib wrapper; the compile-rule deps
77+
must cover `vlib__Helper.cmi`, so `main` rebuilds:
78+
79+
$ cat > vlib/helper.mli <<EOF
80+
> val h : string
81+
> val z : int
82+
> EOF
83+
$ dune build @check
84+
$ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("consumer/\\.main\\.eobjs/byte/"))]'
85+
[
86+
{
87+
"target_files": [
88+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmi",
89+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmti"
90+
]
91+
},
92+
{
93+
"target_files": [
94+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmo",
95+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmt"
96+
]
97+
}
98+
]
99+
100+
Case 2 (soundness): edit `virt_iface`'s interface to expose `z`.
101+
`main` reaches `virt_iface` through the vlib wrapper; the compile-
102+
rule deps must cover `vlib__Virt_iface.cmi`, so `main` rebuilds:
103+
104+
$ cat > vlib/virt_iface.mli <<EOF
105+
> val x : string
106+
> val z : int
107+
> EOF
108+
$ dune build @check
109+
$ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("consumer/\\.main\\.eobjs/byte/"))]'
110+
[
111+
{
112+
"target_files": [
113+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmi",
114+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmti"
115+
]
116+
},
117+
{
118+
"target_files": [
119+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmo",
120+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmt"
121+
]
122+
}
123+
]
124+
125+
Case 3 (forward-looking pin on current behaviour): edit `unused`'s
126+
interface to expose `w`. `main` does not reference `unused`, so
127+
under a future per-module narrowing this edit would not rebuild
128+
`main`. Today, the per-library filter rebuilds `main` anyway
129+
because `impl`'s `.cmi` glob covers every module of the
130+
implementation's closure. Promote when per-module narrowing within
131+
a library lands.
132+
133+
$ cat > vlib/unused.mli <<EOF
134+
> val u : string
135+
> val w : string
136+
> EOF
137+
$ dune build @check
138+
$ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("consumer/\\.main\\.eobjs/byte/"))]'
139+
[
140+
{
141+
"target_files": [
142+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmi",
143+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmti"
144+
]
145+
},
146+
{
147+
"target_files": [
148+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmo",
149+
"_build/default/consumer/.main.eobjs/byte/dune__exe__Main.cmt"
150+
]
151+
}
152+
]
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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.24
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+
Glob coverage on `consumer.cmi`'s compile rule:
58+
59+
$ dune rules --root . --format=json --deps _build/default/.consumer.objs/byte/consumer.cmi |
60+
> jq -r 'include "dune"; .[] | depsGlobs | "\(.dir_kind) \(.dir) \(.predicate)"'
61+
In_build_dir _build/default/.wrapped_lib.objs/byte *.cmi
62+
63+
Case 1 (soundness): edit `inner_a`'s interface to expose `z`.
64+
`consumer` reaches `inner_a` through the wrapper `Wrapped_lib`;
65+
the compile-rule deps must cover `wrapped_lib__Inner_a.cmi`, so
66+
`consumer` rebuilds:
67+
68+
$ cat > inner_a.mli <<EOF
69+
> val x : string
70+
> val z : int
71+
> EOF
72+
$ dune build @check
73+
$ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("\\.consumer\\.objs/byte/consumer\\.cm"))]'
74+
[
75+
{
76+
"target_files": [
77+
"_build/default/.consumer.objs/byte/consumer.cmi",
78+
"_build/default/.consumer.objs/byte/consumer.cmo",
79+
"_build/default/.consumer.objs/byte/consumer.cmt"
80+
]
81+
}
82+
]
83+
84+
Case 2 (forward-looking pin on current behaviour): edit `inner_b`'s
85+
interface to expose `w`. `consumer` does not reference `inner_b`,
86+
so under a future per-module narrowing this edit would not rebuild
87+
`consumer`. Today, the per-library filter rebuilds `consumer`
88+
anyway because `wrapped_lib`'s `.cmi` glob covers every module.
89+
Promote when per-module narrowing within a library lands.
90+
91+
$ cat > inner_b.mli <<EOF
92+
> val y : string
93+
> val w : string
94+
> EOF
95+
$ dune build @check
96+
$ dune trace cat | jq -s 'include "dune"; [.[] | targetsMatchingFilter(test("\\.consumer\\.objs/byte/consumer\\.cm"))]'
97+
[
98+
{
99+
"target_files": [
100+
"_build/default/.consumer.objs/byte/consumer.cmi",
101+
"_build/default/.consumer.objs/byte/consumer.cmo",
102+
"_build/default/.consumer.objs/byte/consumer.cmt"
103+
]
104+
}
105+
]

0 commit comments

Comments
 (0)