mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-27 06:53:11 +08:00
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:
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user