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
5 changes: 3 additions & 2 deletions tools/code-quality/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ python3 tools/code-quality/cfml-coverage.py combine vendor/wheels \
python3 tools/code-quality/cfml-coverage.py revert vendor/wheels
```

Baseline (Lucee 7 + SQLite, measured 2026-08): **~71% function coverage**
(1,937/2,723 functions). Known caveats:
Baseline (Lucee 7 + SQLite, measured 2026-08): **83.2% function coverage**
(2,175/2,614 functions — template pseudo-entries and migrator generator
templates excluded from the denominator). Known caveats:

- Coverage is per-leg: `databaseAdapters/*` paths for MySQL/Oracle/SQLServer/
CockroachDB only execute on those matrix legs — a SQLite-only measurement
Expand Down
35 changes: 32 additions & 3 deletions tools/code-quality/cfml-coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ def _script_insertion_points(masked):
def _files(root):
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS]
# migrator/templates/* are generator payloads — the scaffolding copied
# into a user's app/db/migrate on `wheels migrate`. The originals never
# execute, so instrumenting them only adds permanently-uncovered noise.
if os.path.relpath(dirpath, root).replace(os.sep, '/').endswith('migrator/templates'):
dirnames[:] = []
continue
for fn in filenames:
if fn.lower().endswith(('.cfc', '.cfm')):
yield os.path.join(dirpath, fn)
Expand All @@ -207,8 +213,14 @@ def instrument(root):
# first, then apply them right-to-left so earlier insertions never
# shift later positions.
matches = [(pos, 'script', name) for pos, name in _script_insertion_points(masked)]
for m in TAG_FN.finditer(masked):
brace = masked.find('>', m.end())
# Tag-based cffunctions: match against the ORIGINAL text — masked
# attribute values (blanked quotes) make `name\s*=\s*["']?...` skip
# the value and capture the NEXT attribute's name. Filter out matches
# that start inside a masked (comment/string) region.
for m in TAG_FN.finditer(text):
if m.start() < len(masked) and masked[m.start()] == ' ':
continue
brace = text.find('>', m.end())
if brace >= 0:
matches.append((brace + 1, 'tag', m.group(1)))
matches.sort(key=lambda t: -t[0])
Expand Down Expand Up @@ -259,7 +271,23 @@ def combine(root, coverage_path, complexity_path):
complexity = {r['id']: int(r['complexity']) for r in raw_complexity}
# cov keys are "<rel>:<fn>"; complexity ids are "<rel>:<line>:<fn>"
rows = []
skipped_templates = 0
for cid, comp in complexity.items():
rel = cid.split(':')[0]
# Whole-file pseudo-entries (`<template>`) come from tag-based .cfm
# views, interface declarations, and abstract stubs — they are not
# executable functions, so function-level coverage cannot measure
# them. They'd otherwise count as permanently uncovered noise.
if cid.endswith(':<template>'):
skipped_templates += 1
continue
# migrator/templates/* are generator payloads (scaffolding copied into
# user apps on `wheels migrate`) — the originals never execute. The
# instrumenter skips them, so they'd otherwise read as uncovered.
# (The complexity GATE still checks them; only coverage excludes them.)
if rel.startswith('migrator/templates/'):
skipped_templates += 1
continue
covered = _cov_key(cid) in cov
cv = 100.0 if covered else 0.0
rows.append({'id': cid, 'complexity': comp, 'covered': covered, 'crap': round(crap(comp, cv), 1)})
Expand All @@ -269,7 +297,8 @@ def combine(root, coverage_path, complexity_path):
for r in rows[:50]:
print(f'{r["crap"]:7.1f} {r["complexity"]:4d} {"yes" if r["covered"] else "no ":>3} {r["id"]}')
n_cov = sum(1 for r in rows if r['covered'])
print(f'\nfunction coverage: {n_cov}/{len(rows)} ({100*n_cov/max(1,len(rows)):.1f}%)')
print(f'\nfunction coverage: {n_cov}/{len(rows)} ({100*n_cov/max(1,len(rows)):.1f}%)'
+ (f' (excluded {skipped_templates} template pseudo-entries)' if skipped_templates else ''))
json.dump(rows, open('crap-report.json', 'w'), indent=2)
print('wrote crap-report.json')

Expand Down
413 changes: 413 additions & 0 deletions vendor/wheels/tests/specs/database/AdapterSqlGenerationSpec.cfc

Large diffs are not rendered by default.

133 changes: 133 additions & 0 deletions vendor/wheels/tests/specs/global/MigratorGlobalCoverageSpec.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
component extends="wheels.WheelsTest" {

/**
* Covers remaining migrator definition builders (TableDefinition column
* type helpers, ViewDefinition, Migration addReference/createView/down)
* and global helper functions ($convertToStringBoolean,
* $convertToStringBoxLangSlashDatetime, $normalizePath,
* $boxLangVersionMessage, pluginNames, injector,
* $clearControllerInitializationCache). All are pure or side-effect-free
* in the test application context.
*/
function run() {

g = application.wo;

describe("migrator definition builders", () => {

beforeEach(() => {
adapter = CreateObject("component", "wheels.databaseAdapters.SQLite.SQLiteMigrator");
migration = CreateObject("component", "wheels.migrator.Migration").init();
});

describe("TableDefinition column helpers", () => {

it("bigInteger()/char()/uniqueidentifier() append typed columns", () => {
var t = new wheels.migrator.TableDefinition(
adapter = adapter,
name = "spec_types",
id = false
);
t.bigInteger(columnNames = "views");
t.char(columnNames = "code", limit = 9);
t.uniqueidentifier(columnNames = "uid");
expect(arrayLen(t.columns)).toBe(3);
expect(t.columns[1].type).toBe("biginteger");
expect(t.columns[2].type).toBe("char");
expect(t.columns[3].type).toBe("uniqueidentifier");
});

});

describe("ViewDefinition", () => {

it("init()/selectStatement() store the view shape", () => {
var view = new wheels.migrator.ViewDefinition(
adapter = adapter,
name = "spec_view"
);
expect(view.name).toBe("spec_view");
view.selectStatement(sql = "SELECT id FROM c_o_r_e_users");
expect(view.selectSql).toBe("SELECT id FROM c_o_r_e_users");
});

});

describe("Migration helpers", () => {

it("down() announces the not-implemented stub", () => {
migration.down();
expect(true).toBeTrue();
});

it("createView() returns a ViewDefinition", () => {
var view = migration.createView(name = "spec_view2");
expect(isObject(view)).toBeTrue();
expect(view.name).toBe("spec_view2");
});

});

});

describe("global helpers", () => {

it("$convertToStringBoolean() renders true/false and ''", () => {
expect(g.$convertToStringBoolean(val = true)).toBe("true");
expect(g.$convertToStringBoolean(val = false)).toBe("false");
expect(g.$convertToStringBoolean(val = "")).toBe("");
});

it("$convertToStringBoxLangSlashDatetime() parses AM/PM slash dates", () => {
var dt = g.$convertToStringBoxLangSlashDatetime(value = "06/25/2024 10:30 PM");
expect(DateFormat(dt, "yyyy-mm-dd")).toBe("2024-06-25");
expect(Hour(dt)).toBe(22);
});

it("$normalizePath() converts [x] segments to dots and strips leading dots", () => {
expect(g.$normalizePath(path = "users[name]")).toBe("users.name");
expect(g.$normalizePath(path = ".posts")).toBe("posts");
});

it("$boxLangVersionMessage() renders the compatibility message", () => {
var below = g.$boxLangVersionMessage(
major = 0, minor = 9, patch = 0,
minimumMajor = 1, minimumMinor = 0, minimumPatch = 0,
maximumMajor = 2, maximumMinor = 0, maximumPatch = 0,
version = "0.9.0"
);
expect(below).toInclude("requires BoxLang version 1.0.0 or higher");
var above = g.$boxLangVersionMessage(
major = 3, minor = 0, patch = 0,
minimumMajor = 1, minimumMinor = 0, minimumPatch = 0,
maximumMajor = 2, maximumMinor = 0, maximumPatch = 0,
version = "3.0.0"
);
expect(above).toInclude("tested up to BoxLang version 2.0.0");
});

it("pluginNames() returns the loaded plugin list", () => {
expect(isSimpleValue(g.pluginNames())).toBeTrue();
});

it("injector() returns the DI container when initialized", () => {
try {
var di = g.injector();
expect(isObject(di)).toBeTrue();
} catch (Wheels.DI.NotInitialized e) {
// Some app contexts never boot the DI container; the
// typed error is the other valid outcome.
expect(e.type).toBe("Wheels.DI.NotInitialized");
}
});

it("$clearControllerInitializationCache() clears the controller cache", () => {
g.$clearControllerInitializationCache();
expect(isStruct(application.wheels.controllers)).toBeTrue();
});

});

}

}
146 changes: 146 additions & 0 deletions vendor/wheels/tests/specs/model/ModelSurfaceCoverageSpec.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
component extends="wheels.WheelsTest" {

/**
* Covers model-layer surfaces the main specs never touch directly: the
* ScopeChain terminal finders/aggregates (the named-scope chain API), the
* QueryBuilder offset builder, the remaining callback registration
* helpers, and assorted class-level registration helpers (ignoredColumns,
* sharedModel, isClass/isInstance, withAdvisoryLock, errorsOnBase,
* $coerceOracleTimestamp). State-mutating registrations are reverted or
* scoped to zero-row where clauses so other specs are unaffected.
*/
function run() {

describe("model layer surfaces", () => {

describe("ScopeChain terminal methods", () => {

it("findByKey()/findFirst()/findLastOne() delegate with merged specs", () => {
var chain = new wheels.model.query.ScopeChain(
modelReference = model("post"),
specs = [{where = "views > 0"}]
);
expect(chain.findByKey(key = 1)).toBeWheelsModel();
expect(chain.findFirst(order = "id")).toBeWheelsModel();
expect(chain.findLastOne(order = "id")).toBeWheelsModel();
});

it("average()/sum()/maximum()/minimum() aggregate through the chain", () => {
var chain = new wheels.model.query.ScopeChain(modelReference = model("post"));
expect(chain.average(property = "views")).toBeNumeric();
expect(chain.sum(property = "views")).toBeNumeric();
expect(chain.maximum(property = "views")).toBeNumeric();
expect(chain.minimum(property = "views")).toBeNumeric();
});

it("updateAll()/deleteAll() respect the merged zero-row where", () => {
var chain = new wheels.model.query.ScopeChain(
modelReference = model("post"),
specs = [{where = "id = 0"}]
);
// The id = 0 guard means these mutate nothing.
expect(chain.updateAll(properties = {views = 999})).toBeNumeric();
expect(chain.deleteAll()).toBeNumeric();
});

it("findEach()/findInBatches() iterate through the chain", () => {
var chain = new wheels.model.query.ScopeChain(
modelReference = model("post"),
specs = [{where = "views > 0", maxRows = 3}]
);
var seen = {count = 0};
// Hoisted closures: an inline closure as a named
// constructor arg crashes Adobe CF (cross-engine
// invariant #5).
var eachCallback = function(post) {
seen.count++;
};
chain.findEach(batchSize = 2, callback = eachCallback);
expect(seen.count).toBeGT(0);
seen.count = 0;
var batchCallback = function(posts) {
seen.count += posts.recordCount;
};
chain.findInBatches(batchSize = 2, callback = batchCallback);
expect(seen.count).toBeGT(0);
});

});

describe("QueryBuilder", () => {

it("offset() records the offset value", () => {
var builder = new wheels.model.query.QueryBuilder(modelReference = model("post"));
expect(builder.offset(value = 5)).toBe(builder);
});

});

describe("callback registration helpers", () => {

it("registers the remaining lifecycle callbacks and clears them", () => {
var m = model("post");
m.afterInitialization();
m.afterNew();
m.afterUpdate();
m.afterValidation();
m.afterValidationOnCreate();
m.afterValidationOnUpdate();
m.beforeUpdate();
m.beforeValidationOnCreate();
m.beforeValidationOnUpdate();
// Revert so no phantom callback fires on later specs.
m.$clearCallbacks();
});

it("$coerceOracleTimestamp delegates to the engine adapter", () => {
var m = model("post");
var d = CreateDate(2024, 6, 25);
var coerced = m.$coerceOracleTimestamp(value = d);
expect(isDate(coerced)).toBeTrue();
});

});

describe("class-level registration helpers", () => {

it("ignoredColumns() registers and clears the ignored set", () => {
var m = model("post");
m.ignoredColumns(columns = ["spec_phantom"]);
m.ignoredColumns();
});

it("isClass()/isInstance() distinguish class from record", () => {
var m = model("post");
expect(m.isClass()).toBeTrue();
var record = m.findByKey(key = 1);
expect(record.isInstance()).toBeTrue();
expect(record.isClass()).toBeFalse();
});

it("sharedModel() marks the class as tenant-shared", () => {
var m = model("post");
m.sharedModel();
expect(m.$classData().sharedModel).toBeTrue();
});

it("errorsOnBase() returns an array of base errors", () => {
var m = model("post").new();
expect(m.errorsOnBase()).toBeArray();
});

it("withAdvisoryLock() runs the callback (no-op on SQLite)", () => {
var m = model("post");
var result = m.withAdvisoryLock(name = "spec_advisory", callback = function() {
return 42;
});
expect(result).toBe(42);
});

});

});

}

}
Loading
Loading