-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathtest_object_address.cpp
More file actions
373 lines (326 loc) · 15.2 KB
/
Copy pathtest_object_address.cpp
File metadata and controls
373 lines (326 loc) · 15.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
// mcpp#344 — a package's object addresses must be a function of that package
// alone.
//
// This is the machine-checkable form of the invariant the global build cache
// depends on. The cache key deliberately excludes the consuming project (that
// is what makes an entry shareable across projects), so if a dependency's
// object layout can shift when the consumer pulls in some UNRELATED package,
// two consumers write and read incompatible layouts under one key. #344 was
// exactly that: `compat.zlib`'s compress.o was `obj/compress.o` alone and
// `obj/compat_zlib/zlib-1.3.2/compress.o` alongside `compat.bzip2` (which ships
// its own compress.c), because basename disambiguation (#233) was driven by a
// census over every unit in the build.
//
// The test therefore builds the SAME dependency twice — once alone, once beside
// a package engineered to collide with it — and demands byte-identical
// addresses. It fails on the pre-#344 tree.
#include <gtest/gtest.h>
import std;
import mcpp.build.plan;
import mcpp.manifest;
import mcpp.modgraph.graph;
import mcpp.modgraph.scanner;
import mcpp.toolchain.model;
using namespace mcpp::build;
namespace {
struct Tmp {
std::filesystem::path path;
Tmp() {
path = std::filesystem::temp_directory_path()
/ std::format("mcpp_obj_addr_{}", std::random_device{}());
std::filesystem::create_directories(path);
}
~Tmp() {
std::error_code ec;
std::filesystem::remove_all(path, ec);
}
};
void touchFile(const std::filesystem::path& p) {
std::filesystem::create_directories(p.parent_path());
std::ofstream(p) << "/* test */\n";
}
mcpp::toolchain::Toolchain gccLike() {
mcpp::toolchain::Toolchain tc;
tc.compiler = mcpp::toolchain::CompilerId::GCC;
tc.version = "16.1.0";
tc.binaryPath = "/usr/bin/g++";
tc.targetTriple = "x86_64-linux-gnu";
return tc;
}
mcpp::modgraph::PackageRoot makePackage(const std::filesystem::path& root,
std::string_view name) {
mcpp::modgraph::PackageRoot p;
p.root = root;
p.manifest.package.name = std::string(name);
p.manifest.package.version = "1.0.0";
p.manifest.package.standard = "c++23";
return p;
}
// One C source per package, all sharing the basename `compress.c` — the shape
// that used to trigger the global census.
mcpp::modgraph::SourceUnit unitFor(const std::filesystem::path& pkgRoot,
const std::filesystem::path& rel,
std::string_view pkgName) {
mcpp::modgraph::SourceUnit u;
u.path = pkgRoot / rel;
u.relPath = rel;
u.packageName = std::string(pkgName);
touchFile(u.path);
return u;
}
struct Built {
std::filesystem::path object;
std::filesystem::path packageObjectRel;
};
// Plan a graph containing the root project plus `deps`, and return the
// addresses computed for the FIRST dependency's single unit.
std::optional<Built> planZlib(const Tmp& t, bool withBzip2, std::string* err) {
auto projectRoot = t.path / "proj";
auto storeRoot = t.path / "store";
mcpp::manifest::Manifest rootManifest;
rootManifest.package.name = "app";
rootManifest.package.version = "0.1.0";
rootManifest.package.standard = "c++23";
mcpp::manifest::Target lib;
lib.name = "app";
lib.kind = mcpp::manifest::Target::Library;
rootManifest.targets.push_back(lib);
std::vector<mcpp::modgraph::PackageRoot> packages;
auto rootPkg = makePackage(projectRoot, "app");
rootPkg.manifest = rootManifest;
packages.push_back(rootPkg);
packages.push_back(makePackage(storeRoot / "compat.zlib@1.3.2", "compat.zlib"));
if (withBzip2)
packages.push_back(
makePackage(storeRoot / "compat.bzip2@1.0.8", "compat.bzip2"));
mcpp::modgraph::Graph graph;
graph.units.push_back(unitFor(projectRoot, "src/app.cpp", "app"));
graph.units.push_back(
unitFor(packages[1].root, "zlib-1.3.2/compress.c", "compat.zlib"));
if (withBzip2)
graph.units.push_back(
unitFor(packages[2].root, "bzip2-1.0.8/compress.c", "compat.bzip2"));
std::vector<std::size_t> topo;
for (std::size_t i = 0; i < graph.units.size(); ++i) topo.push_back(i);
auto plan = make_plan(rootManifest, gccLike(), {}, graph, topo, packages,
projectRoot, projectRoot / "target" / "t",
{}, {}, {storeRoot});
if (!plan) { if (err) *err = plan.error(); return std::nullopt; }
auto want = packages[1].root / "zlib-1.3.2" / "compress.c";
for (auto& cu : plan->compileUnits) {
if (cu.source != want) continue;
return Built{cu.object, cu.packageObjectRel};
}
if (err) *err = "zlib compile unit not found in the plan";
return std::nullopt;
}
} // namespace
// The load-bearing assertion. Adding an unrelated, deliberately colliding
// package to the graph must not move zlib's object by one byte — not its build
// path, and above all not its cache address.
TEST(ObjectAddress, DependencyAddressesAreImmuneToTheRestOfTheGraph) {
Tmp t;
std::string errAlone, errBeside;
auto alone = planZlib(t, /*withBzip2=*/false, &errAlone);
auto beside = planZlib(t, /*withBzip2=*/true, &errBeside);
ASSERT_TRUE(alone) << errAlone;
ASSERT_TRUE(beside) << errBeside;
EXPECT_EQ(alone->packageObjectRel, beside->packageObjectRel)
<< "the cache-entry address moved when an unrelated package joined the "
"graph — that is mcpp#344";
EXPECT_EQ(alone->object, beside->object);
}
// The address must also be package-internal: nothing about the consumer, and
// nothing that only exists on this machine. It is read back by another machine
// that computed the same key.
TEST(ObjectAddress, DependencyCacheAddressIsPackageInternal) {
Tmp t;
std::string err;
auto built = planZlib(t, /*withBzip2=*/true, &err);
ASSERT_TRUE(built) << err;
auto rel = built->packageObjectRel.generic_string();
EXPECT_FALSE(rel.empty());
EXPECT_FALSE(built->packageObjectRel.is_absolute()) << rel;
EXPECT_FALSE(rel.starts_with("..")) << rel;
// Mirrors the source's path relative to its OWN package root, and carries
// no package-partition component (that lives in the build path only).
EXPECT_EQ(rel, "zlib-1.3.2/compress.o") << rel;
// The build path does partition by package — that is what makes the
// cross-package census unnecessary.
auto obj = built->object.generic_string();
EXPECT_EQ(obj, "obj/compat_zlib/zlib-1.3.2/compress.o") << obj;
}
// `path_is_under_any` decides both whether a package may be cached and where
// its objects are anchored, and it must answer LEXICALLY.
//
// A payload store whose entries are symlinks into another store is ordinary:
// tests/e2e/_inherit_toolchain.sh builds one so an isolated MCPP_HOME can reuse
// the developer's toolchains, and CI caches do the same to avoid re-downloading.
// std::filesystem::relative() runs weakly_canonical and resolves those links, at
// which point `<home>/registry/data/xpkgs/<pkg>` no longer looks like it is in
// the store — every such package silently drops out of the build cache. That
// regression was caught by e2e 40 and is pinned here where it is cheap.
TEST(ObjectAddress, PathContainmentIsLexicalSoSymlinkedStoresStillCount) {
Tmp t;
auto real = t.path / "real-store";
auto store = t.path / "home" / "registry" / "data" / "xpkgs";
std::filesystem::create_directories(real / "compat.zlib@1.3.2" / "src");
std::filesystem::create_directories(store);
std::error_code ec;
std::filesystem::create_directory_symlink(
real / "compat.zlib@1.3.2", store / "compat.zlib@1.3.2", ec);
if (ec) GTEST_SKIP() << "symlinks unavailable: " << ec.message();
auto pkgRoot = store / "compat.zlib@1.3.2";
EXPECT_TRUE(path_is_under_any(pkgRoot, {store}));
EXPECT_TRUE(path_is_under_any(pkgRoot / "src" / "compress.c", {store}));
// And it still says no to something genuinely outside — the gate has to
// keep rejecting `target/.mangled/**`, which is the case it exists for.
EXPECT_FALSE(path_is_under_any(t.path / "proj" / "target" / ".mangled" / "x",
{store}));
EXPECT_FALSE(path_is_under_any(pkgRoot, {}));
}
// The root project is never cached, so it keeps the flat layout every project
// has had since 0.0.1 — and a dependency shipping a same-named file can no
// longer force it to disambiguate.
TEST(ObjectAddress, RootObjectsStayFlatAndUncacheable) {
Tmp t;
auto projectRoot = t.path / "proj";
auto storeRoot = t.path / "store";
mcpp::manifest::Manifest rootManifest;
rootManifest.package.name = "app";
rootManifest.package.version = "0.1.0";
rootManifest.package.standard = "c++23";
mcpp::manifest::Target lib;
lib.name = "app";
lib.kind = mcpp::manifest::Target::Library;
rootManifest.targets.push_back(lib);
std::vector<mcpp::modgraph::PackageRoot> packages;
auto rootPkg = makePackage(projectRoot, "app");
rootPkg.manifest = rootManifest;
packages.push_back(rootPkg);
packages.push_back(makePackage(storeRoot / "compat.zlib@1.3.2", "compat.zlib"));
mcpp::modgraph::Graph graph;
// Root ships its own compress.c, colliding with the dependency's.
graph.units.push_back(unitFor(projectRoot, "src/compress.c", "app"));
graph.units.push_back(
unitFor(packages[1].root, "zlib-1.3.2/compress.c", "compat.zlib"));
std::vector<std::size_t> topo{0, 1};
auto plan = make_plan(rootManifest, gccLike(), {}, graph, topo, packages,
projectRoot, projectRoot / "target" / "t",
{}, {}, {storeRoot});
ASSERT_TRUE(plan) << plan.error();
for (auto& cu : plan->compileUnits) {
if (cu.source == projectRoot / "src" / "compress.c") {
EXPECT_EQ(cu.object.generic_string(), "obj/compress.o");
EXPECT_TRUE(cu.packageObjectRel.empty())
<< "the root project must never get a cache address";
}
}
}
// ── A source outside the package that declares it (mcpp#641, item 3) ───────
namespace {
mcpp::manifest::Manifest rootManifestNamed(std::string_view name) {
mcpp::manifest::Manifest m;
m.package.name = std::string(name);
m.package.version = "0.1.0";
m.package.standard = "c++23";
mcpp::manifest::Target bin;
bin.name = std::string(name);
bin.kind = mcpp::manifest::Target::Library;
m.targets.push_back(bin);
return m;
}
// The address of `source` in a plan whose root package `installer` lives at
// `installerRoot` and declares a source under the root of its dependency
// `dep`, as a build program's `mcpp::source(...)` does.
std::optional<std::string> addressOfDeclaredDependencySource(
const std::filesystem::path& installerRoot,
const std::filesystem::path& depRoot,
std::string* err) {
auto rootManifest = rootManifestNamed("installer");
std::vector<mcpp::modgraph::PackageRoot> packages;
auto rootPkg = makePackage(installerRoot, "installer");
rootPkg.manifest = rootManifest;
packages.push_back(rootPkg);
packages.push_back(makePackage(depRoot, "dep"));
const auto helper = depRoot / "platform" / "impl" / "helper.cpp";
mcpp::modgraph::Graph graph;
graph.units.push_back(unitFor(installerRoot, "src/main.cpp", "installer"));
graph.units.push_back(unitFor(installerRoot,
std::filesystem::relative(helper, installerRoot), "installer"));
graph.units.back().path = helper;
touchFile(helper);
graph.units.push_back(unitFor(depRoot, "src/own.cpp", "dep"));
std::vector<std::size_t> topo{0, 1, 2};
auto plan = make_plan(rootManifest, gccLike(), {}, graph, topo, packages,
installerRoot, installerRoot / "target" / "t", {}, {}, {});
if (!plan) { if (err) *err = plan.error(); return std::nullopt; }
std::optional<std::string> found;
for (auto& cu : plan->compileUnits) {
if (cu.source == depRoot / "src" / "own.cpp") {
// The dependency's own source keeps the mirrored address.
if (cu.object.generic_string() != "obj/dep/src/own.o") {
if (err) *err = "dependency's own source moved: " + cu.object.generic_string();
return std::nullopt;
}
}
if (cu.source == helper) found = cu.object.generic_string();
}
if (!found && err) *err = "helper compile unit not found";
return found;
}
} // namespace
// The address names the package that contains the file and the path inside it,
// so it does not grow with the distance between the two package roots.
TEST(ObjectAddress, ASourceUnderAnotherPackageIsAddressedByThatPackage) {
Tmp t;
const auto dep = t.path / "fw" / "dep";
std::string err;
auto near = addressOfDeclaredDependencySource(t.path / "fw" / "tool", dep, &err);
ASSERT_TRUE(near) << err;
EXPECT_EQ(*near, "obj/installer/__pkg/dep/platform/impl/helper.o");
EXPECT_EQ(near->find("__up"), std::string::npos) << *near;
auto deeper = addressOfDeclaredDependencySource(
t.path / "fw" / "tools" / "windows" / "installer" / "tool", dep, &err);
ASSERT_TRUE(deeper) << err;
EXPECT_EQ(*deeper, *near)
<< "moving the declaring package deeper changed the address";
}
// A source no package contains, colliding by basename with a root source, is
// filed under a hash of its directory rather than a climbing mirror of it.
TEST(ObjectAddress, AnUnownedEscapingSourceIsAddressedByAHashOfItsDirectory) {
Tmp t;
auto projectRoot = t.path / "proj";
auto depRoot = t.path / "store" / "dep@1.0.0";
auto outside = t.path / "elsewhere" / "out" / "gen.cpp";
auto rootManifest = rootManifestNamed("app");
std::vector<mcpp::modgraph::PackageRoot> packages;
auto rootPkg = makePackage(projectRoot, "app");
rootPkg.manifest = rootManifest;
packages.push_back(rootPkg);
packages.push_back(makePackage(depRoot, "dep"));
mcpp::modgraph::Graph graph;
graph.units.push_back(unitFor(projectRoot, "src/gen.cpp", "app"));
graph.units.push_back(unitFor(depRoot,
std::filesystem::relative(outside, depRoot), "dep"));
graph.units.back().path = outside;
touchFile(outside);
std::vector<std::size_t> topo{0, 1};
auto plan = make_plan(rootManifest, gccLike(), {}, graph, topo, packages,
projectRoot, projectRoot / "target" / "t", {}, {}, {});
ASSERT_TRUE(plan) << plan.error();
std::string rootObj, outsideObj;
for (auto& cu : plan->compileUnits) {
if (cu.source == projectRoot / "src" / "gen.cpp") rootObj = cu.object.generic_string();
if (cu.source == outside) outsideObj = cu.object.generic_string();
}
EXPECT_EQ(rootObj, "obj/app/src/gen.o");
ASSERT_TRUE(outsideObj.starts_with("obj/dep/__ext/")) << outsideObj;
EXPECT_TRUE(outsideObj.ends_with("/gen.o")) << outsideObj;
EXPECT_EQ(outsideObj.find("__up"), std::string::npos) << outsideObj;
const auto hash = outsideObj.substr(std::string("obj/dep/__ext/").size(), 8);
EXPECT_EQ(outsideObj.size(), std::string("obj/dep/__ext/").size() + 8 + std::string("/gen.o").size())
<< outsideObj;
EXPECT_TRUE(std::ranges::all_of(hash, [](char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'); })) << outsideObj;
}