fix(store): keep a top-level test/ directory as a test signal for Python

The Python exemption for a singular test/ directory was meant for shipped
packages like django/test/, but it also applied to a repository's own
test/ suite. Helpers such as test/helpers.py or SQLAlchemy's
test/orm/_fixtures.py became production components, their indexed is_test
flag was ignored, and a route handler there would count as an entry point.

Only a nested test/ is now exempt; a top-level test/ stays a test
directory for every language, together with its indexed flag. The segment
and suffix checks use small helpers instead of hand-counted lengths.
Django and the cbm control repository are unchanged.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
This commit is contained in:
Bernhard Jackiewicz
2026-10-02 20:11:35 +02:00
parent 7c7dce8300
commit 028b341fc9
2 changed files with 35 additions and 23 deletions
+29 -20
View File
@@ -194,39 +194,48 @@ static bool ap_cluster_edge(int type) {
return type == 1 || type == 5 || type == 11;
}
static bool ap_named(const char *text, size_t length, const char *name) {
return length == strlen(name) && !strncmp(text, name, length);
}
static bool ap_suffix(const char *text, const char *suffix) {
size_t length = strlen(text), tail = strlen(suffix);
return length > tail && !strcmp(text + length - tail, suffix);
}
/* Conservative source conventions supplement the positive indexed is_test
* flag. Non-test classification means no test evidence, not production use.
* Python discovers tests by file name (test_*.py, *_test.py, tests.py,
* conftest.py). A singular test/ directory there is often a shipped package
* (django/test/ is Django's public testing API), so for a .py file without a
* test name it is no signal on its own. *package then tells the caller that
* the indexed flag only repeats that directory rule (cbm_is_test_file) and
* must not decide alone either; it keeps one role per file. tests/ and
* __tests__/ stay directory signals, and test/ stays one for other files. */
* tests/ and __tests__/ are test directories anywhere, and so is a top-level
* test/. Python discovers tests by file name (test_*.py, *_test.py, tests.py,
* conftest.py); a nested test/ there is often a shipped package (django/test/
* is Django's public testing API), so for a .py file without a test name it
* is no signal on its own. *package then tells the caller that the indexed
* flag only repeats that directory rule (cbm_is_test_file) and must not
* decide alone either; it keeps one role per file. */
static bool ap_test_path(const char *path, bool *package) {
const char *base = path;
bool directory = false, singular = false;
bool nested = false;
for (const char *p = path;; p++) {
if (*p == '/' || *p == '\\' || !*p) {
size_t len = (size_t)(p - base);
if ((len == 5 && !strncmp(base, "tests", len)) ||
(len == 9 && !strncmp(base, "__tests__", len)))
directory = true;
else if (len == 4 && !strncmp(base, "test", len))
singular = true;
bool singular = ap_named(base, len, "test");
if ((singular && base == path) || ap_named(base, len, "tests") ||
ap_named(base, len, "__tests__"))
return true;
nested = nested || singular;
if (!*p)
break;
base = p + 1;
}
}
size_t len = strlen(base);
bool python = len > 3 && !strcmp(base + len - 3, ".py");
if (directory || !strncmp(base, "test_", 5) || strstr(base, "_test.") ||
strstr(base, ".test.") || strstr(base, ".spec.") ||
(python && (!strcmp(base, "tests.py") || !strcmp(base, "conftest.py"))))
if (ap_suffix(base, ".py")) {
if (!strcmp(base, "tests.py") || !strcmp(base, "conftest.py"))
return true;
*package = nested;
} else if (nested)
return true;
*package = singular && python;
return singular && !python;
return !strncmp(base, "test_", 5) || strstr(base, "_test.") || strstr(base, ".test.") ||
strstr(base, ".spec.");
}
static int ap_counts(ap_context *c, const char *project) {
+6 -3
View File
@@ -1248,8 +1248,10 @@ TEST(projection_python_test_files_follow_name_conventions) {
projection_node(store, "Class", "projection.app.tests.ViewTests", "app/tests.py", false);
projection_node(store, "Function", "projection.app.models_test.check", "app/models_test.py",
false);
projection_node(store, "Function", "projection.conftest.fixture", "test/conftest.py", false);
projection_node(store, "Function", "projection.app.conftest.fixture", "app/conftest.py", false);
projection_node(store, "Function", "projection.check", "test/check.c", false);
/* A top-level test/ is a test suite, also for helpers without a test name. */
projection_node(store, "Function", "projection.test.helpers.build", "test/helpers.py", false);
projection_node(store, "Function", "projection.app.views.index", "app/views.py", false);
/* django/test/ ships Django's testing API. The extractor flags its
* functions from the directory alone; the class carries no flag. */
@@ -1268,7 +1270,8 @@ TEST(projection_python_test_files_follow_name_conventions) {
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
ASSERT_NOT_NULL(doc);
const char *tests[] = {"projection.app.tests.ViewTests", "projection.app.models_test.check",
"projection.conftest.fixture", "projection.check"};
"projection.app.conftest.fixture", "projection.check",
"projection.test.helpers.build"};
for (size_t i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) {
yyjson_val *part = projection_component_of(doc, tests[i]);
ASSERT_NOT_NULL(part);
@@ -1283,7 +1286,7 @@ TEST(projection_python_test_files_follow_name_conventions) {
ASSERT_NOT_NULL(api);
ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(api, "role")), "non_test");
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(api, "member_count")), 2);
ASSERT_EQ(yyjson_arr_size(projection_field(doc, "components")), 6);
ASSERT_EQ(yyjson_arr_size(projection_field(doc, "components")), 7);
yyjson_doc_free(doc);
free(json);
cbm_store_close(store);