Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/3083-hardener-shoulds-docs.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Guides for `wheels test` now state the CLI throws `Wheels.TestsFailed` on `directoryRejected`, `bundlesDiscovered=0`, and compile-skipped specs — not only Fail/Error — so a vacuous or rejected scope is not documented as exit 0 (#3083)
1 change: 1 addition & 0 deletions changelog.d/3083-hardener-shoulds.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Legacy CommandBox `wheels test` / `browser:test` refuse with a deprecation `error()` then `return` and no longer invoke TestBox (`testbox run`). Unrouted `vendor/wheels/controllers/Tests.cfc` is removed; `/wheels/core/tests` and `/wheels/app/tests` stay on `Public.cfc` (#3083)
182 changes: 182 additions & 0 deletions cli/lucli/tests/specs/commands/HardenerShouldsSpec.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/**
* Hardener SHOULDs 4–6 (WheelsTest review slice).
*
* Source-scan / existence locks — no live CommandBox, no live runTests HTTP.
* Same altitude as MainCommandSpec / TestExitFailClosedSpec.
*
* SHOULD 4 — guides must teach CLI fail-closed (Wheels.TestsFailed), not
* "no Fail/Error means exit 0" / ignore directoryRejected.
* SHOULD 5 — legacy CommandBox cli/src test runners must not be a weaker
* exit path than LuCLI `wheels test` / `wheels browser test`.
* SHOULD 6 — vendor/wheels/controllers/Tests.cfc is unrouted and must not
* ship; allowlisted runners stay on Public.cfc.
*/
component extends="wheels.wheelstest.system.BaseSpec" {

function beforeAll() {
variables.repoRoot = expandPath("/cli/../");
variables.guidesRoot = variables.repoRoot & "web/sites/guides/src/content/docs/v4-0-0/";
}

function run() {

describe("SHOULD 4 — docs teach fail-closed, not silent full-suite / vacuous exit 0", () => {

it("testing.mdx names Wheels.TestsFailed and the ##3083 honesty signals", () => {
var src = fileRead(guidesRoot & "command-line-tools/wheels-commands/testing.mdx");
expect(src).toInclude("Wheels.TestsFailed");
expect(src).toInclude("directoryRejected");
expect(src).toInclude("bundlesDiscovered");
});

it("running-framework-tests.mdx says wheels test throws Wheels.TestsFailed", () => {
var src = fileRead(guidesRoot & "contributing/running-framework-tests.mdx");
expect(src).toInclude("Wheels.TestsFailed");
});

it("quick-start.mdx does not teach that a run with no Fail/Error always exits 0", () => {
var src = fileRead(guidesRoot & "command-line-tools/quick-start.mdx");
expect(src).notToInclude("a run with no failures exits `0`");
});

it("running-tests-locally.mdx names Wheels.TestsFailed for the CLI path", () => {
var src = fileRead(guidesRoot & "testing/running-tests-locally.mdx");
expect(src).toInclude("Wheels.TestsFailed");
});

it("ci-integration.mdx gates the CommandBox curl example on directoryRejected / bundlesDiscovered", () => {
var src = fileRead(guidesRoot & "testing/ci-integration.mdx");
expect(src).toInclude("directoryRejected");
expect(src).toInclude("bundlesDiscovered");
expect(src).toInclude("Wheels.TestsFailed");
});

});

describe("SHOULD 5 — CommandBox cli/src test runners are not a weaker exit path", () => {

it("test/run.cfc does not swallow TestBox failing exit codes", () => {
var src = fileRead(expandPath("/cli/src/commands/wheels/test/run.cfc"));
expect(findNoCase("failing exit code", src)).toBe(
0,
"CommandBox wheels test run must not catch-and-ignore TestBox failing exit codes."
);
});

it("test/all.cfc does not swallow TestBox failing exit codes", () => {
var src = fileRead(expandPath("/cli/src/commands/wheels/test/all.cfc"));
expect(findNoCase("failing exit code", src)).toBe(
0,
"CommandBox wheels test:all must not catch-and-ignore TestBox failing exit codes."
);
});

it("test/unit.cfc does not swallow TestBox failing exit codes", () => {
var src = fileRead(expandPath("/cli/src/commands/wheels/test/unit.cfc"));
expect(findNoCase("failing exit code", src)).toBe(
0,
"CommandBox wheels test:unit must not catch-and-ignore TestBox failing exit codes."
);
});

it("test/integration.cfc does not swallow TestBox failing exit codes", () => {
var src = fileRead(expandPath("/cli/src/commands/wheels/test/integration.cfc"));
expect(findNoCase("failing exit code", src)).toBe(
0,
"CommandBox wheels test:integration must not catch-and-ignore TestBox failing exit codes."
);
});

it("browser/test.cfc refuses with a deprecation error instead of returning after Fail/Error", () => {
var src = fileRead(expandPath("/cli/src/commands/wheels/browser/test.cfc"));
expect(src).toInclude("DEPRECATED");
expect(reFindNoCase("error\s*\(", src)).toBeGT(
0,
"CommandBox wheels browser:test must error() so the process cannot exit 0 after Fail/Error."
);
});

it("legacy CommandBox test runners point operators at LuCLI wheels test", () => {
var files = [
"test/run.cfc",
"test/all.cfc",
"test/unit.cfc",
"test/integration.cfc",
"test/coverage.cfc",
"test/watch.cfc",
"browser/test.cfc"
];
for (var rel in files) {
var src = fileRead(expandPath("/cli/src/commands/wheels/" & rel));
expect(src).toInclude(
"DEPRECATED",
rel & " must refuse with a deprecation instead of offering a weaker exit path."
);
expect(src).toInclude("LuCLI");
}
});

it("CommandBox test runners do not invoke testbox run after deprecation error()", () => {
// CommandBox error() prints red and does NOT abort. A later
// command("testbox run") still executes. C5.
var files = [
"test/run.cfc",
"test/all.cfc",
"test/unit.cfc",
"test/integration.cfc",
"test/coverage.cfc",
"test/watch.cfc",
"browser/test.cfc"
];
for (var rel in files) {
var src = fileRead(expandPath("/cli/src/commands/wheels/" & rel));
expect(findNoCase("testbox run", src)).toBe(
0,
rel & " must not contain testbox run after error(); CommandBox error() does not abort."
);
}
});

it("deprecation error() is followed by return so CommandBox cannot fall through", () => {
var files = [
"test/run.cfc",
"test/all.cfc",
"test/unit.cfc",
"test/integration.cfc",
"test/coverage.cfc",
"test/watch.cfc",
"browser/test.cfc"
];
for (var rel in files) {
var src = fileRead(expandPath("/cli/src/commands/wheels/" & rel));
expect(reFindNoCase("error\s*\(\s*""DEPRECATED[^;]*;\s*return\s*;", src)).toBeGT(
0,
rel & " must return immediately after error(""DEPRECATED..."") — error() does not abort."
);
}
});

});

describe("SHOULD 6 — orphan vendor/wheels/controllers/Tests.cfc is gone", () => {

it("does not ship vendor/wheels/controllers/Tests.cfc", () => {
expect(fileExists(expandPath("/vendor/wheels/controllers/Tests.cfc"))).toBeFalse(
"Unrouted Tests.cfc is not on the ##3083 allowlisted runners; delete it rather than leave an orphan."
);
});

it("allowlisted runners route to Public.cfc, not a Tests controller", () => {
var publicRoutes = fileRead(expandPath("/vendor/wheels/public/routes.cfm"));
var testRoutes = fileRead(expandPath("/vendor/wheels/tests/routes.cfm"));
expect(publicRoutes).toInclude("public####tests_testbox");
expect(publicRoutes).toInclude("wheels####public####testbox");
expect(reFindNoCase("to\s*=\s*""Tests####", publicRoutes)).toBe(0);
expect(reFindNoCase("to\s*=\s*""Tests####", testRoutes)).toBe(0);
});

});

}

}
85 changes: 2 additions & 83 deletions cli/src/commands/wheels/browser/test.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -23,89 +23,8 @@ component aliases="wheels browser:test, wheels browser test" extends="../base" {
boolean verbose = false,
string directory = "wheels.tests.specs.wheelstest"
) {
var projectRoot = getCWD();

try {
var manifest = browserService.getManifest(projectRoot);
var installDir = browserService.resolveInstallDir();
var status = browserService.verifyInstall(
manifest=manifest,
installDir=installDir
);
if (!status.installed) {
print.redLine("Playwright not installed.");
if (arrayLen(status.missing)) {
print.yellowLine("Missing: " & arrayToList(status.missing, ", "));
}
if (arrayLen(status.mismatched)) {
print.yellowLine("SHA mismatch: " & arrayToList(status.mismatched, ", "));
}
print.line("");
print.line("Run: wheels browser:install");
return;
}
} catch (any e) {
print.redLine("Error: " & e.message);
return;
}

print.line("Running browser tests...");
print.line("Directory: " & arguments.directory);
print.line("");

var serverInfo = command("server info").params(property="host").run(returnOutput=true);
var port = command("server info").params(property="port").run(returnOutput=true);
var host = trim(serverInfo) ?: "localhost";
var portNum = trim(port) ?: "8080";
var baseUrl = "http://" & host & ":" & portNum;

var testUrl = baseUrl
& "/wheels/core/tests?db=sqlite&format=json&directory="
& arguments.directory;

try {
cfhttp(url=testUrl, method="GET", timeout=300, result="local.response");
} catch (any e) {
print.redLine("Failed to reach test runner at: " & testUrl);
print.redLine("Is the server running? Try: server start");
return;
}

if (arguments.format == "json") {
print.line(local.response.fileContent);
return;
}

try {
var data = deserializeJSON(local.response.fileContent);
print.line("Pass: " & data.totalPass & " Fail: " & data.totalFail & " Error: " & data.totalError);
print.line("");

for (var bundle in (data.bundleStats ?: [])) {
for (var suite in (bundle.suiteStats ?: [])) {
for (var spec in (suite.specStats ?: [])) {
if (listFindNoCase("Failed,Error", spec.status ?: "")) {
print.redLine(
" " & (spec.status ?: "") & ": "
& (spec.name ?: "unknown")
);
if (arguments.verbose && len(spec.failMessage ?: "")) {
print.line(" " & left(spec.failMessage, 200));
}
}
}
}
}

if (data.totalFail == 0 && data.totalError == 0) {
print.greenLine("All browser tests passed.");
}
} catch (any e) {
print.redLine("Failed to parse test results: " & e.message);
if (arguments.verbose) {
print.line(left(local.response.fileContent ?: "", 500));
}
}
error("DEPRECATED: CommandBox `wheels browser:test` is frozen and does not fail-closed. Use the LuCLI `wheels` binary (`wheels browser test`). Removal scheduled for Wheels 5.0. See cli/src/README.md.");
return;
}

}
74 changes: 2 additions & 72 deletions cli/src/commands/wheels/test/all.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -45,77 +45,7 @@ component aliases='wheels test:all' extends="../base" {
string servername = ""
) {
requireWheelsApp(getCWD());
arguments = reconstructArgs(
argStruct=arguments,
allowedValues={
type=["app", "core", "plugin"],
format=["txt", "json", "junit", "html"],
coverageReporter=["html", "json", "xml"]
}
);
arguments.directory = resolveTestDirectory(arguments.type, arguments.directory);

// Build the test URL
var testUrl = buildTestUrl(
type = arguments.type,
servername = arguments.servername,
format = arguments.format
);

// Add coverage parameters if enabled
if (arguments.coverage) {
testUrl &= "&coverage=true";
testUrl &= "&coverageBrowserOutputDir=#encodeForURL(arguments.coverageOutputDir)#";
// Add coverage reporter format to URL
testUrl &= "&coverageReporter=#encodeForURL(arguments.coverageReporter)#";
}

// Add fail-fast parameter if specified
if (arguments.failFast) {
testUrl &= "&bail=true";
}

// Build TestBox command parameters
var params = {
runner = testUrl,
recurse = arguments.recurse,
verbose = arguments.verbose
};

// Add directory parameter if specified
if (len(arguments.directory)) {
params.directory = arguments.directory;
}
// Add optional filtering parameters
if (len(arguments.bundles)) {
params.testbundles = arguments.bundles;
}

if (len(arguments.labels)) {
params.labels = arguments.labels;
}

if (len(arguments.excludes)) {
params.excludes = arguments.excludes;
}

if (len(arguments.filter)) {
// Handle filter parameter
if (reFindNoCase("Test$", arguments.filter)) {
params.testBundles = arguments.filter;
} else {
params.testSpecs = arguments.filter;
}
}

try {
// Execute TestBox command
command('testbox run').params(argumentCollection=params).run();
} catch (any e) {
// Let TestBox handle its own output and errors
if (!findNoCase("failing exit code", e.message)) {
rethrow;
}
}
error("DEPRECATED: CommandBox `wheels test:all` is frozen and does not fail-closed. Use the LuCLI `wheels` binary (`wheels test`). Removal scheduled for Wheels 5.0. See cli/src/README.md.");
return;
}
}
Loading
Loading