Contributing
Running the Framework Test Suite
This page is for people hacking on Wheels itself — the code in vendor/wheels/ of the framework repo. If you're testing your application, you want Running Tests Locally; nothing on this page exists in a scaffolded app.
The fast inner loop is Lucee 7 + SQLite via tools/test-local.sh. The Docker matrix exists for the other job: proving a change works across every engine and database before you push.
The one-shot script — tools/test-local.sh
Section titled “The one-shot script — tools/test-local.sh”tools/test-local.sh is what every framework contributor runs before pushing. It creates the two SQLite test databases (wheelstestdb.db and wheelstestdb_tenant_b.db), starts a disposable server on port 8080 if one isn't already up, downloads the SQLite JDBC driver if missing, runs the suite, and prints coloured pass/fail output. On exit it stops any server it started.
bash tools/test-local.sh # run all core testsbash tools/test-local.sh model # run model specs onlybash tools/test-local.sh security # run security specs onlybash tools/test-local.sh browser # run browser specs (Playwright JARs required)The script accepts these short aliases as the first argument: model / models, controller / controllers, view / views, security, middleware, dispatch, migrator. Anything else is passed through as a literal directory path (dot-notation, rooted at wheels.tests.specs.).
Environment knobs:
PORT=9090 bash tools/test-local.sh— use a non-default port (handy when 8080 is in use).DB=mysql bash tools/test-local.sh— run against a different database; bring up the container yourself first (see the Docker matrix below).WHEELS_BROWSER_TEST_BASE_URL— auto-set tohttp://localhost:${PORT}so browser specs hit the same server the script starts.
You can also run the framework suite through the CLI: wheels test --core, with --db=<sqlite|h2|mysql|postgres|sqlserver|oracle|cockroachdb> selecting between the wheelstestdb_* datasources the matrix wires up (--db is only honoured with --core).
The core test-runner URL
Section titled “The core test-runner URL”The fastest iteration loop is hitting the runner directly over HTTP — this is what the script calls under the hood:
# Run everythingcurl "http://localhost:60007/wheels/core/tests?db=sqlite&format=json"
# Filter by directorycurl "http://localhost:60007/wheels/core/tests?db=sqlite&format=json&directory=wheels.tests.specs.model"
# Filter to a single spec file — use testBundles, not directorycurl "http://localhost:60007/wheels/core/tests?db=sqlite&format=json&testBundles=wheels.tests.specs.model.callbacksSpec"
# Skip populate when iterating on one spec and the schema is already in placecurl "http://localhost:60007/wheels/core/tests?db=sqlite&format=json&populate=false&testBundles=wheels.tests.specs.model.callbacksSpec"
# Scope to a single installed package's tests (e.g. wheels-sentry, wheels-i18n)curl "http://localhost:60007/wheels/core/tests?db=sqlite&format=json&directory=vendor.wheels-sentry.tests"directory= only filters down to a directory — pointing it at a single spec file matches zero bundles and runs nothing; use testBundles= for one file. The core endpoint only accepts directory values starting with wheels.tests (or vendor.<package>.tests); anything else is rejected and the full suite runs instead — the JSON flags this with directoryRejected: true and a warnings[] entry rather than silently reporting green (#3083). populate=false is the other inner-loop trick: populate drops and recreates every test table, so skipping it once the schema exists shaves the run down to just your specs.
Docker cross-engine matrix
Section titled “Docker cross-engine matrix”compose.yml at the repo root ships a service per supported engine, bind-mounted to your checkout — edit code, hit the port, no rebuilds. tools/test-matrix.sh wraps it end-to-end (tools/test-matrix.sh lucee6,adobe2025 sqlite mirrors CI's compat matrix); or drive it by hand:
# Start Lucee 7 + Adobe 2025 (minimum for a cross-engine check)docker compose up -d lucee7 adobe2025
# Wait ~60s for startup, then run bothcurl -s -o /tmp/lucee7.json "http://localhost:60007/wheels/core/tests?db=sqlite&format=json"curl -s -o /tmp/adobe2025.json "http://localhost:62025/wheels/core/tests?db=sqlite&format=json"
# Summarizefor f in /tmp/lucee7.json /tmp/adobe2025.json; do python3 -c "import jsond = json.load(open('$f'))print('$f', d['totalPass'], 'pass', d['totalFail'], 'fail', d['totalError'], 'error')"done| Engine | Service | Port |
|--------|---------|------|
| Lucee 5 | lucee5 | 60005 |
| Lucee 6 | lucee6 | 60006 |
| Lucee 7 | lucee7 | 60007 |
| Adobe 2018 | adobe2018 | 62018 |
| Adobe 2021 | adobe2021 | 62021 |
| Adobe 2023 | adobe2023 | 62023 |
| Adobe 2025 | adobe2025 | 62025 |
| BoxLang | boxlang | 60001 |
Testing against specific databases
Section titled “Testing against specific databases”Each engine ships with H2 and SQLite baked in. Everything else is a compose service you bring up alongside the engine: mysql, postgres, sqlserver, cockroachdb, oracle.
docker compose up -d lucee7 mysqlcurl -sf "http://localhost:60007/wheels/core/tests?db=mysql&format=json"Supported db values: sqlite (script default), h2, mysql, postgres, sqlserver, oracle, cockroachdb. Oracle is soft-failed in CI — failures log warnings but don't block the build. (The docker-compose.db-*.yml files at the repo root are dev-stack overlays for the demo app, not test-matrix services.)
Cross-engine gotchas
Section titled “Cross-engine gotchas”The runtime differences that catch first-time framework contributors — each one has stung a real PR. The full list lives in Coding Standards; the ones that bite in tests specifically:
struct.map()resolves to the built-in struct member function on Lucee/Adobe, not your CFC method.applicationscope members — Adobe CF rejects function members on theapplicationscope.- Closure
thiscapture — share references across closures withvar ctx = { ref: obj }. obj["key"]()inside closures crashes Adobe 2021/2023's parser — split intovar fn = obj["key"]; fn();.- Arrays copy by value in struct literals on Adobe — reference through a parent struct instead.
privatemixin functions are not integrated —$integrateComponents()copies onlypublicmethods; helpers invendor/wheels/model/*.cfcand friends must bepublicwith a$prefix.
Contributor failure modes
Section titled “Contributor failure modes”- Docker daemon not running — start Docker Desktop (macOS/Windows) or
systemctl start docker(Linux). - Port already in use —
lsof -i :60007and stop the holder; orPORT=a different one for the script. - Populate SQL errors — "Table already exists" means a stale table from a previous engine-specific run (add
DROP TABLE IF EXISTS); "No such function: NOW" means cross-engine SQL drift — useCURRENT_TIMESTAMP. - Stale SQLite files —
rm -f wheelstestdb.db wheelstestdb_tenant_b.dbbetween weird runs. - Server not responding after
compose up— engines cold-start in 30–90 seconds; checkdocker compose logs <service>if it stays down past two minutes.
docker compose downrm -f wheelstestdb.db wheelstestdb_tenant_b.dbwheels stop