Skip to content

Commit 815d118

Browse files
authored
Merge branch 'develop' into fix/3302-matrix-burndown
2 parents 5d085c3 + 3b7199c commit 815d118

26 files changed

Lines changed: 1096 additions & 209 deletions

.github/workflows/compat-matrix.yml

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -439,12 +439,40 @@ jobs:
439439
" || { echo "JUnit conversion failed for ${db} (non-fatal)"; rm -f "$JUNIT_FILE"; }
440440
fi
441441
442+
# Zero-test guard (#3302): a compile-wiped leg returns HTTP 200 with
443+
# totalSpecs=0 (one bad CFC zeroes the whole directory compile), which
444+
# previously rendered as a pass. Every engine runs the same core suite
445+
# (~4,700 specs), so anything below the floor means the suite never
446+
# actually ran. Revisit the floor if per-DB spec subsets ever ship.
447+
MIN_SPECS=4000
448+
TOTAL_SPECS="-1"
449+
if [ -f "$RESULT_FILE" ] && { [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "417" ]; }; then
450+
TOTAL_SPECS=$(python3 -c "
451+
import json, sys
452+
try:
453+
d = json.load(open('$RESULT_FILE'))
454+
print(int(d.get('totalSpecs', 0)))
455+
except:
456+
print(-1)
457+
" 2>/dev/null || echo "-1")
458+
fi
459+
460+
SPECS_OK=true
461+
if { [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "417" ]; } && [ "$TOTAL_SPECS" -lt "$MIN_SPECS" ]; then
462+
SPECS_OK=false
463+
echo "::error::${{ matrix.cfengine }} + ${db}: HTTP ${HTTP_CODE} but only ${TOTAL_SPECS} testcases reported (floor: ${MIN_SPECS}) — suite likely compile-wiped, treating leg as failed"
464+
fi
465+
442466
# Track per-database result
443-
if [ "$HTTP_CODE" = "200" ]; then
444-
echo "PASSED: ${{ matrix.cfengine }} + ${db}"
467+
if [ "$HTTP_CODE" = "200" ] && [ "$SPECS_OK" = true ]; then
468+
echo "PASSED: ${{ matrix.cfengine }} + ${db} (${TOTAL_SPECS} testcases)"
445469
DB_STATUS="pass"
446470
else
447-
echo "FAILED: ${{ matrix.cfengine }} + ${db} (HTTP ${HTTP_CODE})"
471+
if [ "$HTTP_CODE" = "200" ]; then
472+
echo "FAILED: ${{ matrix.cfengine }} + ${db} (HTTP 200 but zero-test guard tripped)"
473+
else
474+
echo "FAILED: ${{ matrix.cfengine }} + ${db} (HTTP ${HTTP_CODE})"
475+
fi
448476
DB_STATUS="fail"
449477
if echo "$SOFT_FAIL_DBS" | grep -qw "$db"; then
450478
echo "::warning::${db} tests failed but marked as soft-fail (non-blocking)"
@@ -486,6 +514,8 @@ jobs:
486514
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
487515
488516
SOFT_FAIL_DBS="oracle"
517+
# Keep in sync with MIN_SPECS in the run-tests step (#3302).
518+
MIN_SPECS=4000
489519
IFS=',' read -ra DBS <<< "${{ steps.db-list.outputs.databases }}"
490520
for db in "${DBS[@]}"; do
491521
RESULT_FILE="/tmp/test-results/${{ matrix.cfengine }}-${db}-result.txt"
@@ -494,18 +524,22 @@ jobs:
494524
IS_SOFT_FAIL=true
495525
fi
496526
if [ -f "$RESULT_FILE" ]; then
497-
# Check JSON for failures
498-
FAIL_COUNT=$(python3 -c "
527+
# Check JSON for failures and testcase count (zero-test guard, #3302)
528+
STATS=$(python3 -c "
499529
import json, sys
500530
try:
501531
d = json.load(open('$RESULT_FILE'))
502-
print(d.get('totalFail', 0) + d.get('totalError', 0))
532+
print(int(d.get('totalFail', 0) + d.get('totalError', 0)), int(d.get('totalSpecs', 0)))
503533
except:
504-
print(-1)
505-
" 2>/dev/null || echo "-1")
534+
print(-1, -1)
535+
" 2>/dev/null || echo "-1 -1")
536+
FAIL_COUNT="${STATS% *}"
537+
SPEC_COUNT="${STATS#* }"
506538
507-
if [ "$FAIL_COUNT" = "0" ]; then
539+
if [ "$FAIL_COUNT" = "0" ] && [ "$SPEC_COUNT" -ge "$MIN_SPECS" ]; then
508540
echo "| ${db} | :white_check_mark: Pass |" >> $GITHUB_STEP_SUMMARY
541+
elif [ "$FAIL_COUNT" = "0" ]; then
542+
echo "| ${db} | :warning: ${SPEC_COUNT} tests (zero-test guard) |" >> "$GITHUB_STEP_SUMMARY"
509543
elif [ "$FAIL_COUNT" = "-1" ] && [ "$IS_SOFT_FAIL" = true ]; then
510544
echo "| ${db} | :warning: Error (soft-fail) |" >> $GITHUB_STEP_SUMMARY
511545
elif [ "$FAIL_COUNT" = "-1" ]; then
@@ -613,6 +647,12 @@ jobs:
613647
files: junit-results/**/*.xml
614648
check_name: "Wheels Test Results"
615649
comment_title: "Wheels Test Results"
650+
# Keep the aggregate check neutral (#3302): oracle soft-fail debt
651+
# otherwise pins a red "Wheels Test Results" check to whatever SHA
652+
# the matrix was dispatched on, marking innocent PRs UNSTABLE.
653+
# Leg pass/fail gating lives in the tests job (OVERALL_STATUS);
654+
# annotations, PR comments, and artifacts are unaffected by this.
655+
fail_on: nothing
616656
report_individual_runs: true
617657
report_suite_logs: any
618658
json_file: junit-results/test-results.json
@@ -647,27 +687,41 @@ jobs:
647687
MATRIX_MD="${MATRIX_MD}
648688
"
649689
MATRIX_MD="${MATRIX_MD}
650-
| Engine | MySQL | PostgreSQL | SQL Server | H2 | CockroachDB | Oracle | SQLite |"
690+
| Engine | MySQL | PostgreSQL | SQL Server | H2 | CockroachDB | Oracle (soft-fail) | SQLite |"
651691
MATRIX_MD="${MATRIX_MD}
652-
|--------|:-----:|:----------:|:----------:|:--:|:-----------:|:------:|:------:|"
692+
|--------|:-----:|:----------:|:----------:|:--:|:-----------:|:------------------:|:------:|"
693+
694+
# Keep in sync with SOFT_FAIL_DBS and MIN_SPECS in the tests job (#3302).
695+
SOFT_FAIL_DBS="oracle"
696+
MIN_SPECS=4000
653697
654698
for engine in lucee6 lucee7 adobe2023 adobe2025 boxlang; do
655699
ROW="| **${engine}** |"
656700
for db in mysql postgres sqlserver h2 cockroachdb oracle sqlite; do
657701
FILE="results/test-results-${engine}/${engine}-${db}-result.txt"
702+
IS_SOFT_FAIL=false
703+
if echo "$SOFT_FAIL_DBS" | grep -qw "$db"; then
704+
IS_SOFT_FAIL=true
705+
fi
658706
if [ -f "$FILE" ]; then
659-
FAIL=$(python3 -c "
707+
STATS=$(python3 -c "
660708
import json, sys
661709
try:
662710
d = json.load(open('$FILE'))
663-
print(int(d.get('totalFail', 0) + d.get('totalError', 0)))
711+
print(int(d.get('totalFail', 0) + d.get('totalError', 0)), int(d.get('totalSpecs', 0)))
664712
except:
665-
print(-1)
666-
" 2>/dev/null || echo "-1")
667-
if [ "$FAIL" = "0" ]; then
713+
print(-1, -1)
714+
" 2>/dev/null || echo "-1 -1")
715+
FAIL="${STATS% *}"
716+
SPECS="${STATS#* }"
717+
if [ "$FAIL" = "0" ] && [ "$SPECS" -ge "$MIN_SPECS" ]; then
668718
ROW="${ROW} :white_check_mark: |"
669719
elif [ "$FAIL" = "-1" ]; then
670720
ROW="${ROW} :warning: |"
721+
elif [ "$FAIL" = "0" ]; then
722+
ROW="${ROW} :warning: ${SPECS} tests |"
723+
elif [ "$IS_SOFT_FAIL" = true ]; then
724+
ROW="${ROW} :warning: ${FAIL} |"
671725
else
672726
ROW="${ROW} :x: ${FAIL} |"
673727
fi
@@ -681,6 +735,9 @@ jobs:
681735
682736
MATRIX_MD="${MATRIX_MD}
683737
738+
*Oracle is soft-fail (non-blocking, tracked in #2663) — :warning: cells in that column never gate the run.*
739+
*A ':warning: N tests' cell means the leg reported fewer than ${MIN_SPECS} testcases (suite likely compile-wiped, counted as failed).*
740+
684741
*Results for commit ${GITHUB_SHA:0:7}.*"
685742
686743
# Write to step summary

CLAUDE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,9 @@ model("User")
390390
.orderBy("name", "ASC")
391391
.limit(25)
392392
.get();
393-
// Methods: where, orWhere, whereNull, whereNotNull, whereBetween, whereIn, whereNotIn, orderBy, limit, get
393+
// Methods: where, orWhere, whereNull, whereNotNull, whereBetween, whereIn, whereNotIn, orderBy,
394+
// limit, offset, select, include, group, distinct, forUpdate, get
395+
// Any of these (not just where) can START the chain on the model, e.g. model("User").select("id,name").get()
394396
395397
// Batch processing — memory-efficient
396398
model("User").findEach(batchSize=1000, callback=function(user) {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
- Debug bar reload link (and the CFML error page's displayed URL) now honors the `subpath`
2+
setting: the base URL is composed from the resolved `webPath` plus the front-controller
3+
filename — the same idiom as `urlFor()` — instead of raw `cgi.script_name`, so subfolder
4+
deployments emit `/myapp/posts?reload=` instead of the unroutable
5+
`/myapp/public/index.cfm/posts?reload=`. Root installs render byte-identical to before.
6+
Extracted into the unit-tested `$buildDebugReloadUrl()` helper in `Global.cfc` ([#3344](https://github.com/wheels-dev/wheels/issues/3344))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Debug bar: the minimized "Debug" restore button now renders after clicking the X. The `#wdb-minimized` button was nested inside the `#wheels-debugbar` container that `wdbMinimize()` hides with `display:none`, so it could never appear and the bar stayed gone for the whole browser session (manual `sessionStorage` cleanup was the only recovery). It is now a sibling of the container, so minimizing shows the restore button bottom-right and clicking it brings the bar back ([#3345](https://github.com/wheels-dev/wheels/issues/3345)).
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- `select()`, `include()`, `group()`, `distinct()`, and `forUpdate()` can now start a query-builder chain directly on the model class (e.g. `model("Person").select("id,firstName").where("department", "engineering").get()`), matching `where()` and the other entry-position builder methods. `forUpdate()` is also available when transitioning from a scope chain. ([#3346](https://github.com/wheels-dev/wheels/issues/3346))
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
- Web test runner (`/wheels/core/tests` and `/wheels/app/tests`): the swap→run→restore window that
2+
temporarily replaces the live `application.wheels` config with test configuration is now serialized
3+
under an exclusive named lock, and the restore runs in a `finally` block. Overlapping test requests
4+
can no longer clobber each other's `application.$$$wheels` backup and leave test config live until
5+
the next `reload=true`, and an erroring suite now restores the original config too. ParallelRunner
6+
partition sub-requests detect the already-applied swap and skip both the swap and the shared lock,
7+
so parallel test mode does not deadlock. Note: this serializes test-vs-test only — a normal request
8+
concurrent with a test run still sees swapped config; true isolation is deferred to a
9+
separate-application-context design (refs [#3025](https://github.com/wheels-dev/wheels/issues/3025)).
10+
Also removes the orphaned legacy RocketUnit runner twin `vendor/wheels/rocketunit_tests/Test.cfc`
11+
(nothing loads it; the active legacy chain via `wheels.Test` is unchanged).

vendor/wheels/Global.cfc

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2699,6 +2699,80 @@ return local.$wheels;
26992699
return local.base & local.relative;
27002700
}
27012701

2702+
/**
2703+
* Internal function. Builds the debug bar's base reload URL (issue #3344).
2704+
* The base is composed from the resolved `webPath` plus the front-controller
2705+
* filename — the same idiom `urlFor()` uses — instead of raw
2706+
* `cgi.script_name`, so subfolder (subpath) installs emit links like
2707+
* `/myapp/posts?reload=` rather than `/myapp/public/index.cfm/posts?reload=`
2708+
* (which the user's rewrite rules don't route). The caller selects which
2709+
* path_info to pass (`request.cgi.path_info` when available, `cgi.path_info`
2710+
* otherwise — engines report it differently). `webPath` and `rewriteFile`
2711+
* default from application scope; tests pass them explicitly, and early
2712+
* boot/error paths where they're missing fall back to the raw script name
2713+
* (the pre-#3344 behavior). Pure string logic so it can be unit-tested in
2714+
* isolation.
2715+
*/
2716+
public string function $buildDebugReloadUrl(
2717+
required string scriptName,
2718+
string pathInfo = "",
2719+
string queryString = "",
2720+
string webPath,
2721+
string rewriteFile
2722+
) {
2723+
// Resolve webPath/rewriteFile from application scope unless overridden.
2724+
// No runtime default-arg expressions (some engines evaluate those
2725+
// eagerly) — same pattern as $resolveSubpathInclude.
2726+
if (StructKeyExists(arguments, "webPath")) {
2727+
local.resolvedWebPath = arguments.webPath;
2728+
} else if (IsDefined("application.wheels.webPath")) {
2729+
local.resolvedWebPath = application.wheels.webPath;
2730+
} else {
2731+
local.resolvedWebPath = "";
2732+
}
2733+
if (StructKeyExists(arguments, "rewriteFile")) {
2734+
local.resolvedRewriteFile = arguments.rewriteFile;
2735+
} else if (IsDefined("application.wheels.rewriteFile")) {
2736+
local.resolvedRewriteFile = application.wheels.rewriteFile;
2737+
} else {
2738+
local.resolvedRewriteFile = "";
2739+
}
2740+
2741+
// Base: webPath + front-controller filename (matches urlFor()); fall
2742+
// back to the raw script name when webPath isn't resolved yet.
2743+
if (Len(local.resolvedWebPath)) {
2744+
local.rv = local.resolvedWebPath & ListLast(arguments.scriptName, "/");
2745+
} else {
2746+
local.rv = arguments.scriptName;
2747+
}
2748+
if (arguments.pathInfo != arguments.scriptName) {
2749+
local.rv &= arguments.pathInfo;
2750+
}
2751+
if (Len(arguments.queryString)) {
2752+
local.rv &= "?" & arguments.queryString;
2753+
}
2754+
if (Len(local.resolvedRewriteFile)) {
2755+
local.rv = ReplaceNoCase(local.rv, "/" & local.resolvedRewriteFile, "");
2756+
}
2757+
local.reloadTokens = "development,testing,maintenance,production,true";
2758+
local.iEnd = ListLen(local.reloadTokens);
2759+
for (local.i = 1; local.i <= local.iEnd; local.i++) {
2760+
local.token = ListGetAt(local.reloadTokens, local.i);
2761+
local.rv = ReplaceNoCase(
2762+
ReplaceNoCase(local.rv, "?reload=" & local.token, ""),
2763+
"&reload=" & local.token,
2764+
""
2765+
);
2766+
}
2767+
if (Find("?", local.rv)) {
2768+
local.rv &= "&";
2769+
} else {
2770+
local.rv &= "?";
2771+
}
2772+
local.rv &= "reload=";
2773+
return local.rv;
2774+
}
2775+
27022776
/**
27032777
* Internal function.
27042778
*/

vendor/wheels/events/onerror/cfmlerror.cfm

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,19 @@
6565
<!--- Request Info Grid --->
6666
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;margin:1.5em 0;">
6767
<cfif IsDefined("application.wheels.rewriteFile")>
68+
<!---
69+
Base composed from webPath (subpath-aware, issue #3344) so subfolder
70+
installs display /myapp/... instead of /myapp/public/... — same idiom
71+
as urlFor() and $buildDebugReloadUrl().
72+
--->
73+
<cfif IsDefined("application.wheels.webPath") AND Len(application.wheels.webPath)>
74+
<cfset local.errorUrlBase = Replace(application.wheels.webPath & ListLast(cgi.script_name, "/"), "/#application.wheels.rewriteFile#", "")>
75+
<cfelse>
76+
<cfset local.errorUrlBase = Replace(cgi.script_name, "/#application.wheels.rewriteFile#", "")>
77+
</cfif>
6878
<div style="background:##181825;border:1px solid ##45475a;border-radius:6px;padding:12px 16px;">
6979
<div style="font-size:10px;font-weight:700;color:##6c7086;text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px;">URL</div>
70-
<div style="font-family:monospace;font-size:12px;color:##cdd6f4;word-break:break-all;">http<cfif cgi.http_x_forwarded_proto EQ "https" OR cgi.server_port_secure EQ "true">s</cfif>://#EncodeForHTML(cgi.server_name)##Replace(cgi.script_name, "/#application.wheels.rewriteFile#", "")#<cfif IsDefined("request.cgi.path_info")>#EncodeForHTML(request.cgi.path_info)#<cfelse>#EncodeForHTML(cgi.path_info)#</cfif><cfif cgi.query_string IS NOT "">?#EncodeForHTML(cgi.query_string)#</cfif></div>
80+
<div style="font-family:monospace;font-size:12px;color:##cdd6f4;word-break:break-all;">http<cfif cgi.http_x_forwarded_proto EQ "https" OR cgi.server_port_secure EQ "true">s</cfif>://#EncodeForHTML(cgi.server_name)##EncodeForHTML(local.errorUrlBase)#<cfif IsDefined("request.cgi.path_info")>#EncodeForHTML(request.cgi.path_info)#<cfelse>#EncodeForHTML(cgi.path_info)#</cfif><cfif cgi.query_string IS NOT "">?#EncodeForHTML(cgi.query_string)#</cfif></div>
7181
</div>
7282
</cfif>
7383
<cfif Len(cgi.http_referer)>

vendor/wheels/events/onrequestend/debug.cfm

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,21 @@ OR (StructKeyExists(local.reqHeaders, "X-Fetch") AND local.reqHeaders["X-Fetch"]
77
OR (StructKeyExists(url, "format") AND ListFindNoCase("json,xml,csv,pdf", url.format))>
88
<cfexit>
99
</cfif>
10-
<cfset local.baseReloadURL = cgi.script_name>
10+
<!---
11+
Base reload URL composed from webPath (subpath-aware, issue #3344) via
12+
$buildDebugReloadUrl() in Global.cfc. Engines report path_info differently,
13+
so prefer the normalized request.cgi copy when available.
14+
--->
1115
<cfif IsDefined("request.cgi.path_info")>
12-
<cfif request.cgi.path_info IS NOT cgi.script_name>
13-
<cfset local.baseReloadURL &= request.cgi.path_info>
14-
</cfif>
16+
<cfset local.debugPathInfo = request.cgi.path_info>
1517
<cfelse>
16-
<cfif cgi.path_info IS NOT cgi.script_name>
17-
<cfset local.baseReloadURL &= cgi.path_info>
18-
</cfif>
19-
</cfif>
20-
<cfif Len(cgi.query_string)>
21-
<cfset local.baseReloadURL &= "?" & cgi.query_string>
18+
<cfset local.debugPathInfo = cgi.path_info>
2219
</cfif>
23-
<cfset local.baseReloadURL = ReplaceNoCase(local.baseReloadURL, "/" & application.wheels.rewriteFile, "")>
24-
<cfloop list="development,testing,maintenance,production,true" index="local.i">
25-
<cfset local.baseReloadURL = ReplaceNoCase(
26-
ReplaceNoCase(local.baseReloadURL, "?reload=" & local.i, ""),
27-
"&reload=" & local.i,
28-
""
29-
)>
30-
</cfloop>
31-
<cfif local.baseReloadURL Contains "?">
32-
<cfset local.baseReloadURL &= "&">
33-
<cfelse>
34-
<cfset local.baseReloadURL &= "?">
35-
</cfif>
36-
<cfset local.baseReloadURL &= "reload=">
20+
<cfset local.baseReloadURL = $buildDebugReloadUrl(
21+
scriptName = cgi.script_name,
22+
pathInfo = local.debugPathInfo,
23+
queryString = cgi.query_string
24+
)>
3725
<cfset local.gitbranch = DirectoryExists(GetDirectoryFromPath(GetBaseTemplatePath()) & ".git") ? FileRead(
3826
GetDirectoryFromPath(GetBaseTemplatePath()) & ".git/HEAD"
3927
) : "">
@@ -489,15 +477,17 @@ OR (StructKeyExists(url, "format") AND ListFindNoCase("json,xml,csv,pdf", url.fo
489477
</div>
490478
</cfif>
491479

480+
</div>
481+
492482
<!--- ============ MINIMIZED BUTTON ============ --->
493-
<div id="wdb-minimized" style="display:none;position:fixed;bottom:8px;right:8px;z-index:99999;">
483+
<!--- Sibling of ##wheels-debugbar on purpose: wdbMinimize() sets the container to display:none, and a descendant of a display:none element can never render, so nesting this inside the container makes the restore button unreachable (issue ##3345). It is independently position:fixed. The script include stays below so both elements exist when debugbar.js's load-time wdbMinimize() re-invocation runs. --->
484+
<div id="wdb-minimized" style="all:initial;display:none;position:fixed;bottom:8px;right:8px;z-index:99999;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,sans-serif;">
494485
<button onclick="wdbRestore()" style="background:##1e1e2e;border:1px solid ##45475a;border-radius:8px;padding:6px 10px;cursor:pointer;color:##89b4fa;font-size:12px;font-family:inherit;display:flex;align-items:center;gap:4px;box-shadow:0 2px 8px rgba(0,0,0,.3);">
495486
<svg viewBox="0 0 153 18" xmlns="http://www.w3.org/2000/svg" style="width:20px;height:5px;"><path d="M15.71 12c1.65 0 2.99 1.34 2.99 3s-1.34 3-2.99 3-2.99-1.34-2.99-3v-1.27c0-.42-.15-.79-.45-1.09L6.1 6.45c-.3-.3-.66-.45-1.09-.45H3.75c-1.65 0-2.99-1.34-2.99-3S2.09 0 3.74 0s2.99 1.34 2.99 3v1.27c0 .42.15.79.45 1.09l6.17 6.19c.3.3.66.45 1.09.45z" fill="##f38ba8"/></svg>
496487
Debug
497488
</button>
498489
</div>
499490

500491
<script><cfinclude template="/wheels/public/assets/js/debugbar.js"></script>
501-
</div>
502492
</cfoutput></cfsavecontent><cfoutput>#ReReplace(local.wdbHtml, "(?m)>\s+<", "><", "all")#</cfoutput>
503493
<!--- cfformat-ignore-end --->

0 commit comments

Comments
 (0)