Skip to content

Commit 56fcf23

Browse files
authored
feat(cli): gate wheels doctor mixin-collision detail behind --verbose (#2287)
* feat(cli): add --verbose gating to wheels doctor mixin collisions Default output now shows a single-line count summary; full per-collision detail is enumerated only under `wheels doctor --verbose`. Structured collision entries live on `results.mixinCollisions` so the service stays presentation-neutral. Closes #2261 * fix(cli): gate packages extraction on tar exit code, not stderr `$extract()` threw on any non-empty stderr, which tripped on GNU tar's informational warnings ("Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.com.apple.provenance'") when extracting macOS-authored tarballs on Linux. Extraction still succeeded — exit code 0 — so the stderr check was spurious. Switch to cfexecute's `result=` arg and throw only when `exitCode != 0`, preserving stderr in extendedInfo for diagnostics. Fixes 4 CI errors in InstallerSpec / PackagesMainCliSpec that were previously masked by TestBox's negative totalError accounting.
1 parent 3e4f9d9 commit 56fcf23

4 files changed

Lines changed: 65 additions & 18 deletions

File tree

cli/lucli/Module.cfc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1237,6 +1237,18 @@ component extends="modules.BaseModule" {
12371237
out("");
12381238
}
12391239

1240+
// Mixin collision detail (verbose only)
1241+
if (verbose && structKeyExists(results, "mixinCollisions") && arrayLen(results.mixinCollisions)) {
1242+
out("Mixin collisions (#arrayLen(results.mixinCollisions)#):", "yellow");
1243+
for (var c in results.mixinCollisions) {
1244+
out(
1245+
" ! method '#c.method#' on '#c.target#' provided by #c.firstSource# '#c.firstName#' is overwritten by #c.secondSource# '#c.secondName#'. Acknowledge via provides.overrides to silence.",
1246+
"yellow"
1247+
);
1248+
}
1249+
out("");
1250+
}
1251+
12401252
// Passed (verbose only, or when no issues)
12411253
if (verbose || (results.status == "HEALTHY")) {
12421254
out("Passed (#arrayLen(results.passed)#):", "green");

cli/lucli/services/Doctor.cfc

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ component {
1919
* Run all health checks and return categorized results.
2020
*/
2121
public struct function runChecks() {
22-
var results = {issues: [], warnings: [], passed: []};
22+
var results = {issues: [], warnings: [], passed: [], mixinCollisions: []};
2323

2424
checkRequiredDirs(results);
2525
checkRecommendedDirs(results);
@@ -303,15 +303,24 @@ component {
303303
for (var i = 2; i <= arrayLen(entries); i++) {
304304
var second = entries[i];
305305
collisionCount++;
306-
arrayAppend(
307-
arguments.results.warnings,
308-
"Mixin collision: method '#second.method#' on '#second.target#' provided by #first.source# '#first.name#' is overwritten by #second.source# '#second.name#'. Acknowledge via provides.overrides to silence."
309-
);
306+
arrayAppend(arguments.results.mixinCollisions, {
307+
target: second.target,
308+
method: second.method,
309+
firstName: first.name,
310+
firstSource: first.source,
311+
secondName: second.name,
312+
secondSource: second.source
313+
});
310314
first = second;
311315
}
312316
}
313317

314-
if (collisionCount == 0 && (directoryExists(vendorDir) || directoryExists(pluginsDir))) {
318+
if (collisionCount > 0) {
319+
arrayAppend(
320+
arguments.results.warnings,
321+
"#collisionCount# mixin collision(s) detected — run 'wheels doctor --verbose' for details"
322+
);
323+
} else if (directoryExists(vendorDir) || directoryExists(pluginsDir)) {
315324
arrayAppend(arguments.results.passed, "No static mixin collisions detected in vendor/ or plugins/");
316325
}
317326
}

cli/lucli/services/packages/Installer.cfc

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,15 @@ component {
162162

163163
private void function $extract(required string tarballPath, required string destDir) {
164164
// Wrap in a try so a missing `tar` produces a clear error message.
165+
local.result = {};
165166
try {
166167
cfexecute(
167168
name = "tar",
168169
arguments = "-xzf #arguments.tarballPath# -C #arguments.destDir#",
169170
timeout = 120,
170171
variable = "local.stdout",
171-
errorVariable = "local.stderr"
172+
errorVariable = "local.stderr",
173+
result = "local.result"
172174
);
173175
} catch (any e) {
174176
Throw(
@@ -177,11 +179,17 @@ component {
177179
extendedInfo = e.message
178180
);
179181
}
180-
if (StructKeyExists(local, "stderr") && Len(Trim(local.stderr))) {
182+
// Gate on exit code, not stderr presence. GNU tar on Linux prints
183+
// informational warnings ("Ignoring unknown extended header keyword
184+
// 'LIBARCHIVE.xattr.com.apple.provenance'") for macOS-authored
185+
// tarballs while still exiting 0 and extracting cleanly.
186+
var exitCode = StructKeyExists(local.result, "exitCode") ? local.result.exitCode : 0;
187+
if (exitCode != 0) {
188+
var stderr = StructKeyExists(local, "stderr") ? local.stderr : "";
181189
Throw(
182190
type = "Wheels.Packages.ExtractionFailed",
183-
message = "tar reported an error during extraction.",
184-
extendedInfo = local.stderr
191+
message = "tar exited with code #exitCode# during extraction.",
192+
extendedInfo = stderr
185193
);
186194
}
187195
}

cli/lucli/tests/specs/services/DoctorSpec.cfc

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,9 @@ component extends="wheels.wheelstest.system.BaseSpec" {
244244
var root = makeProjectRoot();
245245
var doctor = new cli.lucli.services.Doctor(projectRoot = root);
246246
var results = doctor.runChecks();
247+
expect(arrayLen(results.mixinCollisions)).toBe(0);
247248
var combined = arrayToList(results.warnings, " ");
248-
expect(combined).notToInclude("Mixin collision");
249+
expect(combined).notToInclude("mixin collision");
249250
directoryDelete(root, true);
250251
});
251252

@@ -257,38 +258,55 @@ component extends="wheels.wheelstest.system.BaseSpec" {
257258
var doctor = new cli.lucli.services.Doctor(projectRoot = root);
258259
var results = doctor.runChecks();
259260

261+
expect(arrayLen(results.mixinCollisions)).toBe(0);
260262
var warningText = arrayToList(results.warnings, " ");
261-
expect(warningText).notToInclude("Mixin collision");
263+
expect(warningText).notToInclude("mixin collision");
262264
var passedText = arrayToList(results.passed, " ");
263265
expect(passedText).toInclude("No static mixin collisions");
264266
directoryDelete(root, true);
265267
});
266268

267-
it("warns when two packages provide the same method on the same target", () => {
269+
it("emits summary in warnings and detail in mixinCollisions when packages collide", () => {
268270
var root = makeProjectRoot();
269271
makePackage(root, "pkgA", "controller", "$shared", []);
270272
makePackage(root, "pkgB", "controller", "$shared", []);
271273

272274
var doctor = new cli.lucli.services.Doctor(projectRoot = root);
273275
var results = doctor.runChecks();
274276

277+
// Default output: count summary only, no inline detail
275278
var warningText = arrayToList(results.warnings, " ");
276-
expect(warningText).toInclude("Mixin collision");
277-
expect(warningText).toInclude("$shared");
278-
expect(warningText).toInclude("controller");
279+
expect(warningText).toInclude("mixin collision(s) detected");
280+
expect(warningText).toInclude("--verbose");
281+
expect(warningText).notToInclude("$shared");
282+
283+
// Verbose-dump data on the results struct
284+
expect(arrayLen(results.mixinCollisions)).toBe(1);
285+
var c = results.mixinCollisions[1];
286+
expect(c.method).toBe("$shared");
287+
expect(c.target).toBe("controller");
288+
// directoryList() iteration order isn't guaranteed across filesystems,
289+
// so assert both packages are present without fixing which came first.
290+
var participants = [c.firstName, c.secondName];
291+
arraySort(participants, "textnocase");
292+
expect(participants[1]).toBe("pkgA");
293+
expect(participants[2]).toBe("pkgB");
294+
expect(c.firstSource).toBe("package");
295+
expect(c.secondSource).toBe("package");
279296
directoryDelete(root, true);
280297
});
281298

282-
it("suppresses warning when overriding package declares provides.overrides", () => {
299+
it("suppresses warning and detail when overriding package declares provides.overrides", () => {
283300
var root = makeProjectRoot();
284301
makePackage(root, "pkgA", "controller", "$shared", []);
285302
makePackage(root, "pkgB", "controller", "$shared", ["$shared"]);
286303

287304
var doctor = new cli.lucli.services.Doctor(projectRoot = root);
288305
var results = doctor.runChecks();
289306

307+
expect(arrayLen(results.mixinCollisions)).toBe(0);
290308
var warningText = arrayToList(results.warnings, " ");
291-
expect(warningText).notToInclude("Mixin collision");
309+
expect(warningText).notToInclude("mixin collision");
292310
directoryDelete(root, true);
293311
});
294312

0 commit comments

Comments
 (0)