1360 lines
43 KiB
C
1360 lines
43 KiB
C
/*
|
|
* test_discover.c — Tests for directory skip logic, suffix filters, and file walk.
|
|
*
|
|
* RED phase: Tests define expected filtering behavior for the discover module.
|
|
*/
|
|
#include "test_framework.h"
|
|
#include "test_helpers.h"
|
|
#include "discover/discover.h"
|
|
|
|
typedef struct {
|
|
char *home;
|
|
char *xdg_config_home;
|
|
} git_env_snapshot_t;
|
|
|
|
static git_env_snapshot_t save_git_env(void) {
|
|
git_env_snapshot_t snapshot = {0};
|
|
const char *home = getenv("HOME");
|
|
const char *xdg = getenv("XDG_CONFIG_HOME");
|
|
snapshot.home = home ? cbm_strdup(home) : NULL;
|
|
snapshot.xdg_config_home = xdg ? cbm_strdup(xdg) : NULL;
|
|
return snapshot;
|
|
}
|
|
|
|
static void restore_git_env(git_env_snapshot_t *snapshot) {
|
|
if (snapshot->home) {
|
|
cbm_setenv("HOME", snapshot->home, 1);
|
|
free(snapshot->home);
|
|
} else {
|
|
cbm_unsetenv("HOME");
|
|
}
|
|
|
|
if (snapshot->xdg_config_home) {
|
|
cbm_setenv("XDG_CONFIG_HOME", snapshot->xdg_config_home, 1);
|
|
free(snapshot->xdg_config_home);
|
|
} else {
|
|
cbm_unsetenv("XDG_CONFIG_HOME");
|
|
}
|
|
}
|
|
|
|
static bool discover_has_rel_path(const cbm_file_info_t *files, int count, const char *rel_path) {
|
|
for (int i = 0; i < count; i++) {
|
|
if (strcmp(files[i].rel_path, rel_path) == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* ── Directory skip (always skipped) ───────────────────────────── */
|
|
|
|
TEST(skip_git) {
|
|
ASSERT_TRUE(cbm_should_skip_dir(".git", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(skip_node_modules) {
|
|
ASSERT_TRUE(cbm_should_skip_dir("node_modules", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(skip_pycache) {
|
|
ASSERT_TRUE(cbm_should_skip_dir("__pycache__", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(skip_venv) {
|
|
ASSERT_TRUE(cbm_should_skip_dir("venv", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(skip_dist) {
|
|
ASSERT_TRUE(cbm_should_skip_dir("dist", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(skip_target) {
|
|
ASSERT_TRUE(cbm_should_skip_dir("target", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(skip_vendor) {
|
|
ASSERT_TRUE(cbm_should_skip_dir("vendor", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(skip_vendored) {
|
|
ASSERT_TRUE(cbm_should_skip_dir("vendored", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(skip_terraform) {
|
|
ASSERT_TRUE(cbm_should_skip_dir(".terraform", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(skip_coverage) {
|
|
ASSERT_TRUE(cbm_should_skip_dir("coverage", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(skip_idea) {
|
|
ASSERT_TRUE(cbm_should_skip_dir(".idea", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(skip_claude) {
|
|
ASSERT_TRUE(cbm_should_skip_dir(".claude", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
|
|
/* Not skipped in full mode */
|
|
TEST(no_skip_src) {
|
|
ASSERT_FALSE(cbm_should_skip_dir("src", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(no_skip_lib) {
|
|
ASSERT_FALSE(cbm_should_skip_dir("lib", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(no_skip_docs_full) {
|
|
ASSERT_FALSE(cbm_should_skip_dir("docs", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(no_skip_test_full) {
|
|
ASSERT_FALSE(cbm_should_skip_dir("__tests__", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
|
|
/* Fast mode additional skips */
|
|
TEST(skip_fast_docs) {
|
|
ASSERT_TRUE(cbm_should_skip_dir("docs", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
TEST(skip_fast_examples) {
|
|
ASSERT_TRUE(cbm_should_skip_dir("examples", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
TEST(skip_fast_tests) {
|
|
ASSERT_TRUE(cbm_should_skip_dir("__tests__", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
TEST(skip_fast_fixtures) {
|
|
ASSERT_TRUE(cbm_should_skip_dir("fixtures", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
TEST(skip_fast_testdata) {
|
|
ASSERT_TRUE(cbm_should_skip_dir("testdata", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
TEST(skip_fast_generated) {
|
|
ASSERT_TRUE(cbm_should_skip_dir("generated", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
TEST(skip_fast_assets) {
|
|
ASSERT_TRUE(cbm_should_skip_dir("assets", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
TEST(skip_fast_3rdparty) {
|
|
ASSERT_TRUE(cbm_should_skip_dir("third_party", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
TEST(skip_fast_e2e) {
|
|
ASSERT_TRUE(cbm_should_skip_dir("e2e", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
|
|
/* ── Suffix filters ────────────────────────────────────────────── */
|
|
|
|
TEST(suffix_pyc) {
|
|
ASSERT_TRUE(cbm_has_ignored_suffix("module.pyc", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(suffix_o) {
|
|
ASSERT_TRUE(cbm_has_ignored_suffix("main.o", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(suffix_so) {
|
|
ASSERT_TRUE(cbm_has_ignored_suffix("lib.so", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(suffix_png) {
|
|
ASSERT_TRUE(cbm_has_ignored_suffix("icon.png", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(suffix_jpg) {
|
|
ASSERT_TRUE(cbm_has_ignored_suffix("photo.jpg", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(suffix_wasm) {
|
|
ASSERT_TRUE(cbm_has_ignored_suffix("app.wasm", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(suffix_db) {
|
|
ASSERT_TRUE(cbm_has_ignored_suffix("data.db", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(suffix_sqlite) {
|
|
ASSERT_TRUE(cbm_has_ignored_suffix("store.sqlite3", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(suffix_tmp) {
|
|
ASSERT_TRUE(cbm_has_ignored_suffix("file.tmp", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(suffix_tilde) {
|
|
ASSERT_TRUE(cbm_has_ignored_suffix("file~", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
|
|
/* Not ignored */
|
|
TEST(suffix_go) {
|
|
ASSERT_FALSE(cbm_has_ignored_suffix("main.go", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(suffix_py) {
|
|
ASSERT_FALSE(cbm_has_ignored_suffix("app.py", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
TEST(suffix_c) {
|
|
ASSERT_FALSE(cbm_has_ignored_suffix("lib.c", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
|
|
/* Fast mode additional suffixes */
|
|
TEST(suffix_fast_zip) {
|
|
ASSERT_TRUE(cbm_has_ignored_suffix("archive.zip", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
TEST(suffix_fast_pdf) {
|
|
ASSERT_TRUE(cbm_has_ignored_suffix("manual.pdf", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
TEST(suffix_fast_mp3) {
|
|
ASSERT_TRUE(cbm_has_ignored_suffix("sound.mp3", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
TEST(suffix_fast_pem) {
|
|
ASSERT_TRUE(cbm_has_ignored_suffix("cert.pem", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
|
|
/* ── Filename skip (fast mode) ─────────────────────────────────── */
|
|
|
|
TEST(fn_skip_license) {
|
|
ASSERT_TRUE(cbm_should_skip_filename("LICENSE", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
TEST(fn_skip_changelog) {
|
|
ASSERT_TRUE(cbm_should_skip_filename("CHANGELOG.md", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
TEST(fn_skip_gosum) {
|
|
ASSERT_TRUE(cbm_should_skip_filename("go.sum", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
TEST(fn_skip_yarnlock) {
|
|
ASSERT_TRUE(cbm_should_skip_filename("yarn.lock", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
TEST(fn_skip_pkglock) {
|
|
ASSERT_TRUE(cbm_should_skip_filename("package-lock.json", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
|
|
/* Not skipped in full mode */
|
|
TEST(fn_no_skip_license_full) {
|
|
ASSERT_FALSE(cbm_should_skip_filename("LICENSE", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
|
|
/* ── Fast mode patterns ────────────────────────────────────────── */
|
|
|
|
TEST(pattern_dts) {
|
|
ASSERT_TRUE(cbm_matches_fast_pattern("types.d.ts", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
TEST(pattern_pbgo) {
|
|
ASSERT_TRUE(cbm_matches_fast_pattern("service.pb.go", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
TEST(pattern_pb2py) {
|
|
ASSERT_TRUE(cbm_matches_fast_pattern("api_pb2.py", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
TEST(pattern_mock) {
|
|
ASSERT_TRUE(cbm_matches_fast_pattern("mock_service.go", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
TEST(pattern_test_dot) {
|
|
ASSERT_TRUE(cbm_matches_fast_pattern("App.test.js", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
TEST(pattern_spec) {
|
|
ASSERT_TRUE(cbm_matches_fast_pattern("App.spec.ts", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
TEST(pattern_stories) {
|
|
ASSERT_TRUE(cbm_matches_fast_pattern("Button.stories.tsx", CBM_MODE_FAST));
|
|
PASS();
|
|
}
|
|
|
|
/* Not matched in full mode */
|
|
TEST(pattern_dts_full) {
|
|
ASSERT_FALSE(cbm_matches_fast_pattern("types.d.ts", CBM_MODE_FULL));
|
|
PASS();
|
|
}
|
|
|
|
/* ── File discovery (integration) — cross-platform via test_helpers.h ── */
|
|
|
|
TEST(discover_simple) {
|
|
char *base = th_mktempdir("cbm_disc_simple");
|
|
ASSERT(base != NULL);
|
|
|
|
th_write_file(TH_PATH(base, "src/main.go"), "package main\n");
|
|
th_write_file(TH_PATH(base, "src/app.py"), "print(1)\n");
|
|
th_write_file(TH_PATH(base, "src/icon.png"), "binary\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 2); /* main.go + app.py, not icon.png */
|
|
|
|
bool found_go = false, found_py = false;
|
|
for (int i = 0; i < count; i++) {
|
|
if (files[i].language == CBM_LANG_GO)
|
|
found_go = true;
|
|
if (files[i].language == CBM_LANG_PYTHON)
|
|
found_py = true;
|
|
}
|
|
ASSERT_TRUE(found_go);
|
|
ASSERT_TRUE(found_py);
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
TEST(discover_skips_git_dir) {
|
|
char *base = th_mktempdir("cbm_disc_git");
|
|
ASSERT(base != NULL);
|
|
|
|
th_mkdir_p(TH_PATH(base, ".git"));
|
|
th_write_file(TH_PATH(base, ".git/config"), "x\n");
|
|
th_write_file(TH_PATH(base, "src/main.go"), "package main\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 1);
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
TEST(discover_with_gitignore) {
|
|
char *base = th_mktempdir("cbm_disc_gi");
|
|
ASSERT(base != NULL);
|
|
|
|
th_mkdir_p(TH_PATH(base, ".git"));
|
|
th_write_file(TH_PATH(base, ".gitignore"), "*.log\n");
|
|
th_write_file(TH_PATH(base, "src/main.go"), "package main\n");
|
|
th_write_file(TH_PATH(base, "src/debug.log"), "error\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 1);
|
|
ASSERT_EQ(files[0].language, CBM_LANG_GO);
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
TEST(discover_with_global_xdg_ignore) {
|
|
git_env_snapshot_t env = save_git_env();
|
|
char *tmp = th_mktempdir("cbm_disc_global_xdg");
|
|
ASSERT(tmp != NULL);
|
|
|
|
char base[512], repo[512], home[512], xdg[512], xdg_env[512];
|
|
snprintf(base, sizeof(base), "%s", tmp);
|
|
snprintf(repo, sizeof(repo), "%s/repo", base);
|
|
snprintf(home, sizeof(home), "%s/home", base);
|
|
snprintf(xdg, sizeof(xdg), "%s/xdg", base);
|
|
snprintf(xdg_env, sizeof(xdg_env), "%s/", xdg);
|
|
|
|
cbm_setenv("HOME", home, 1);
|
|
cbm_setenv("XDG_CONFIG_HOME", xdg_env, 1);
|
|
|
|
th_mkdir_p(TH_PATH(repo, ".git"));
|
|
th_write_file(TH_PATH(repo, ".git/config"), "[core]\n");
|
|
th_write_file(TH_PATH(xdg, "git/ignore"), "secret.go\nignored_dir/\n");
|
|
th_write_file(TH_PATH(repo, "main.go"), "package main\n");
|
|
th_write_file(TH_PATH(repo, "secret.go"), "package secret\n");
|
|
th_write_file(TH_PATH(repo, "ignored_dir/thing.go"), "package ignored\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(repo, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 1);
|
|
ASSERT_TRUE(discover_has_rel_path(files, count, "main.go"));
|
|
ASSERT_FALSE(discover_has_rel_path(files, count, "secret.go"));
|
|
ASSERT_FALSE(discover_has_rel_path(files, count, "ignored_dir/thing.go"));
|
|
|
|
cbm_discover_free(files, count);
|
|
restore_git_env(&env);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
TEST(discover_global_excludesfile_from_gitconfig_tilde) {
|
|
git_env_snapshot_t env = save_git_env();
|
|
char *tmp = th_mktempdir("cbm_disc_global_cfg");
|
|
ASSERT(tmp != NULL);
|
|
|
|
char base[512], repo[512], home[512];
|
|
snprintf(base, sizeof(base), "%s", tmp);
|
|
snprintf(repo, sizeof(repo), "%s/repo", base);
|
|
snprintf(home, sizeof(home), "%s/home", base);
|
|
|
|
cbm_setenv("HOME", home, 1);
|
|
cbm_unsetenv("XDG_CONFIG_HOME");
|
|
|
|
th_mkdir_p(TH_PATH(repo, ".git"));
|
|
th_write_file(TH_PATH(repo, ".git/config"), "[core]\n");
|
|
th_write_file(TH_PATH(home, ".gitconfig"), "[core]\n excludesFile = ~/custom-ignore\n");
|
|
th_write_file(TH_PATH(home, "custom-ignore"), "skip-me.go\n");
|
|
th_write_file(TH_PATH(repo, "keep.go"), "package keep\n");
|
|
th_write_file(TH_PATH(repo, "skip-me.go"), "package skip\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(repo, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 1);
|
|
ASSERT_TRUE(discover_has_rel_path(files, count, "keep.go"));
|
|
ASSERT_FALSE(discover_has_rel_path(files, count, "skip-me.go"));
|
|
|
|
cbm_discover_free(files, count);
|
|
restore_git_env(&env);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
TEST(discover_repo_local_excludesfile_is_ignored) {
|
|
git_env_snapshot_t env = save_git_env();
|
|
char *tmp = th_mktempdir("cbm_disc_repo_cfg_ignored");
|
|
ASSERT(tmp != NULL);
|
|
|
|
char base[512], repo[512], home[512], secret[512], config[1024];
|
|
snprintf(base, sizeof(base), "%s", tmp);
|
|
snprintf(repo, sizeof(repo), "%s/repo", base);
|
|
snprintf(home, sizeof(home), "%s/home", base);
|
|
snprintf(secret, sizeof(secret), "%s/secret-ignore", home);
|
|
snprintf(config, sizeof(config), "[core]\n excludesFile = %s\n", secret);
|
|
|
|
cbm_setenv("HOME", home, 1);
|
|
cbm_unsetenv("XDG_CONFIG_HOME");
|
|
|
|
th_mkdir_p(TH_PATH(repo, ".git"));
|
|
th_write_file(TH_PATH(repo, ".git/config"), config);
|
|
th_write_file(secret, "skip-me.go\n");
|
|
th_write_file(TH_PATH(repo, "keep.go"), "package keep\n");
|
|
th_write_file(TH_PATH(repo, "skip-me.go"), "package skip\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(repo, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 2);
|
|
ASSERT_TRUE(discover_has_rel_path(files, count, "keep.go"));
|
|
ASSERT_TRUE(discover_has_rel_path(files, count, "skip-me.go"));
|
|
|
|
cbm_discover_free(files, count);
|
|
restore_git_env(&env);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
TEST(discover_missing_global_excludes_is_noop) {
|
|
git_env_snapshot_t env = save_git_env();
|
|
char *tmp = th_mktempdir("cbm_disc_global_missing");
|
|
ASSERT(tmp != NULL);
|
|
|
|
char base[512], repo[512], home[512];
|
|
snprintf(base, sizeof(base), "%s", tmp);
|
|
snprintf(repo, sizeof(repo), "%s/repo", base);
|
|
snprintf(home, sizeof(home), "%s/home", base);
|
|
|
|
cbm_setenv("HOME", home, 1);
|
|
cbm_unsetenv("XDG_CONFIG_HOME");
|
|
|
|
th_mkdir_p(TH_PATH(repo, ".git"));
|
|
th_write_file(TH_PATH(repo, ".git/config"), "[core]\n");
|
|
th_write_file(TH_PATH(repo, "main.go"), "package main\n");
|
|
th_write_file(TH_PATH(repo, "would-be-global.go"), "package global\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(repo, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 2);
|
|
ASSERT_TRUE(discover_has_rel_path(files, count, "main.go"));
|
|
ASSERT_TRUE(discover_has_rel_path(files, count, "would-be-global.go"));
|
|
|
|
cbm_discover_free(files, count);
|
|
restore_git_env(&env);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
TEST(discover_cbmignore_negates_global_ignore) {
|
|
git_env_snapshot_t env = save_git_env();
|
|
char *tmp = th_mktempdir("cbm_disc_global_neg");
|
|
ASSERT(tmp != NULL);
|
|
|
|
char base[512], repo[512], home[512], xdg[512];
|
|
snprintf(base, sizeof(base), "%s", tmp);
|
|
snprintf(repo, sizeof(repo), "%s/repo", base);
|
|
snprintf(home, sizeof(home), "%s/home", base);
|
|
snprintf(xdg, sizeof(xdg), "%s/xdg", base);
|
|
|
|
cbm_setenv("HOME", home, 1);
|
|
cbm_setenv("XDG_CONFIG_HOME", xdg, 1);
|
|
|
|
th_mkdir_p(TH_PATH(repo, ".git"));
|
|
th_write_file(TH_PATH(repo, ".git/config"), "[core]\n");
|
|
th_write_file(TH_PATH(xdg, "git/ignore"), "rescued.go\nblocked.go\n");
|
|
th_write_file(TH_PATH(repo, ".cbmignore"), "!rescued.go\n");
|
|
th_write_file(TH_PATH(repo, "main.go"), "package main\n");
|
|
th_write_file(TH_PATH(repo, "rescued.go"), "package rescued\n");
|
|
th_write_file(TH_PATH(repo, "blocked.go"), "package blocked\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(repo, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 2);
|
|
ASSERT_TRUE(discover_has_rel_path(files, count, "main.go"));
|
|
ASSERT_TRUE(discover_has_rel_path(files, count, "rescued.go"));
|
|
ASSERT_FALSE(discover_has_rel_path(files, count, "blocked.go"));
|
|
|
|
cbm_discover_free(files, count);
|
|
restore_git_env(&env);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
/* issue #234: a directory listed in the root .gitignore (e.g. "vendor/") must
|
|
* be excluded from discovery even when untracked — Composer/PHP projects rely
|
|
* on this. */
|
|
TEST(discover_gitignore_dir_excluded_issue234) {
|
|
char *base = th_mktempdir("cbm_disc_gi234");
|
|
ASSERT(base != NULL);
|
|
|
|
th_mkdir_p(TH_PATH(base, ".git"));
|
|
th_write_file(TH_PATH(base, ".gitignore"), "vendor/\n");
|
|
th_write_file(TH_PATH(base, "src/main.php"), "<?php\nfunction appmain() {}\n");
|
|
th_write_file(TH_PATH(base, "vendor/autoload.php"), "<?php\nfunction autoload() {}\n");
|
|
th_write_file(TH_PATH(base, "vendor/pkg/lib.php"), "<?php\nfunction lib() {}\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
/* Nothing under vendor/ should be discovered. */
|
|
for (int i = 0; i < count; i++) {
|
|
ASSERT_TRUE(strstr(files[i].rel_path, "vendor") == NULL);
|
|
}
|
|
ASSERT_EQ(count, 1); /* only src/main.php */
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
TEST(discover_max_file_size) {
|
|
char *base = th_mktempdir("cbm_disc_size");
|
|
ASSERT(base != NULL);
|
|
|
|
th_write_file(TH_PATH(base, "small.go"), "small\n");
|
|
/* Create a large file (> 1KB) */
|
|
char bigpath[512];
|
|
snprintf(bigpath, sizeof(bigpath), "%s/big.go", base);
|
|
FILE *f = fopen(bigpath, "w");
|
|
ASSERT(f != NULL);
|
|
for (int i = 0; i < 200; i++) {
|
|
fprintf(f, "// padding line %d to exceed 1KB\n", i);
|
|
}
|
|
fclose(f);
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
opts.max_file_size = 1024;
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 1);
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
TEST(discover_null_path) {
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
int rc = cbm_discover(NULL, NULL, &files, &count);
|
|
ASSERT_EQ(rc, -1);
|
|
PASS();
|
|
}
|
|
|
|
TEST(discover_nonexistent_path) {
|
|
char *base = th_mktempdir("cbm_disc_noexist");
|
|
char fake[512];
|
|
snprintf(fake, sizeof(fake), "%s/nonexistent_12345", base ? base : "/tmp");
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
int rc = cbm_discover(fake, NULL, &files, &count);
|
|
ASSERT_EQ(rc, -1);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
TEST(discover_free_null) {
|
|
cbm_discover_free(NULL, 0);
|
|
PASS();
|
|
}
|
|
|
|
TEST(discover_skips_worktrees) {
|
|
char *base = th_mktempdir("cbm_disc_wt");
|
|
ASSERT(base != NULL);
|
|
|
|
th_write_file(TH_PATH(base, "src/main.go"), "package main\n");
|
|
th_write_file(TH_PATH(base, ".worktrees/feature/src/app.go"), "package app\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 1);
|
|
|
|
bool found_main = false;
|
|
for (int i = 0; i < count; i++) {
|
|
if (strstr(files[i].rel_path, "main.go"))
|
|
found_main = true;
|
|
ASSERT_NULL(strstr(files[i].rel_path, ".worktrees"));
|
|
}
|
|
ASSERT_TRUE(found_main);
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
TEST(discover_cbmignore) {
|
|
char *base = th_mktempdir("cbm_disc_cbmi");
|
|
ASSERT(base != NULL);
|
|
|
|
th_mkdir_p(TH_PATH(base, ".git"));
|
|
th_write_file(TH_PATH(base, ".cbmignore"), "generated/\n*.pb.go\n");
|
|
th_write_file(TH_PATH(base, "main.go"), "package main\n");
|
|
th_write_file(TH_PATH(base, "generated/types.go"), "package gen\n");
|
|
th_write_file(TH_PATH(base, "api.pb.go"), "package api\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 1);
|
|
ASSERT_TRUE(strstr(files[0].rel_path, "main.go") != NULL);
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
TEST(discover_cbmignore_stacks) {
|
|
char *base = th_mktempdir("cbm_disc_stack");
|
|
ASSERT(base != NULL);
|
|
|
|
th_mkdir_p(TH_PATH(base, ".git"));
|
|
th_write_file(TH_PATH(base, ".gitignore"), "*.log\n");
|
|
th_write_file(TH_PATH(base, ".cbmignore"), "docs/\n");
|
|
th_write_file(TH_PATH(base, "main.go"), "package main\n");
|
|
th_write_file(TH_PATH(base, "docs/api.go"), "package docs\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 1);
|
|
|
|
bool found_docs = false;
|
|
for (int i = 0; i < count; i++) {
|
|
if (strstr(files[i].rel_path, "docs/"))
|
|
found_docs = true;
|
|
}
|
|
ASSERT_FALSE(found_docs);
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
TEST(discover_symlink_skipped) {
|
|
#ifdef _WIN32
|
|
/* Symlinks require elevated privileges on Windows — skip.
|
|
* Guard the entire body: symlink() doesn't exist on Windows. */
|
|
SKIP_PLATFORM("Windows: symlinks need admin / symlink() unavailable");
|
|
#else
|
|
char *base = th_mktempdir("cbm_disc_sym");
|
|
ASSERT(base != NULL);
|
|
|
|
th_write_file(TH_PATH(base, "real.go"), "package main\n");
|
|
char real_path[512], link_path[512];
|
|
snprintf(real_path, sizeof(real_path), "%s/real.go", base);
|
|
snprintf(link_path, sizeof(link_path), "%s/link.go", base);
|
|
symlink(real_path, link_path);
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 1);
|
|
|
|
bool found_real = false, found_link = false;
|
|
for (int i = 0; i < count; i++) {
|
|
if (strstr(files[i].rel_path, "real.go"))
|
|
found_real = true;
|
|
if (strstr(files[i].rel_path, "link.go"))
|
|
found_link = true;
|
|
}
|
|
ASSERT_TRUE(found_real);
|
|
ASSERT_FALSE(found_link);
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
#endif
|
|
}
|
|
|
|
TEST(discover_new_ignore_patterns) {
|
|
char *base = th_mktempdir("cbm_disc_newign");
|
|
ASSERT(base != NULL);
|
|
|
|
const char *dirs[] = {".next", ".terraform", "zig-cache", ".cargo", "elm-stuff", "bazel-out"};
|
|
for (int i = 0; i < 6; i++) {
|
|
char path[512];
|
|
snprintf(path, sizeof(path), "%s/%s/file.go", base, dirs[i]);
|
|
th_write_file(path, "package x\n");
|
|
}
|
|
th_write_file(TH_PATH(base, "main.go"), "package main\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 1);
|
|
ASSERT_TRUE(strstr(files[0].rel_path, "main.go") != NULL);
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
TEST(discover_generic_dirs_full_mode) {
|
|
char *base = th_mktempdir("cbm_disc_genfull");
|
|
ASSERT(base != NULL);
|
|
|
|
th_write_file(TH_PATH(base, "bin/main.go"), "package bin\n");
|
|
th_write_file(TH_PATH(base, "build/main.go"), "package build\n");
|
|
th_write_file(TH_PATH(base, "out/main.go"), "package out\n");
|
|
|
|
cbm_discover_opts_t opts = {.mode = CBM_MODE_FULL};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 3);
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
TEST(discover_generic_dirs_fast_mode) {
|
|
char *base = th_mktempdir("cbm_disc_genfast");
|
|
ASSERT(base != NULL);
|
|
|
|
th_write_file(TH_PATH(base, "bin/main.go"), "package bin\n");
|
|
th_write_file(TH_PATH(base, "build/main.go"), "package build\n");
|
|
th_write_file(TH_PATH(base, "out/main.go"), "package out\n");
|
|
|
|
cbm_discover_opts_t opts = {.mode = CBM_MODE_FAST};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 0);
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
TEST(discover_deploy_excluded_full_mode) {
|
|
char *base = th_mktempdir("cbm_disc_deploy");
|
|
ASSERT(base != NULL);
|
|
|
|
th_write_file(TH_PATH(base, "src/main.go"), "package main\n");
|
|
th_write_file(TH_PATH(base, "deploy/main.go"), "package deploy\n");
|
|
th_write_file(TH_PATH(base, "deployed/main.go"), "package deployed\n");
|
|
|
|
cbm_discover_opts_t opts = {.mode = CBM_MODE_FULL};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 1);
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
TEST(discover_cbmignore_no_git) {
|
|
char *base = th_mktempdir("cbm_disc_nogit");
|
|
ASSERT(base != NULL);
|
|
|
|
th_write_file(TH_PATH(base, ".cbmignore"), "scratch/\n");
|
|
th_write_file(TH_PATH(base, "main.go"), "package main\n");
|
|
th_write_file(TH_PATH(base, "scratch/tmp.go"), "package scratch\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 1);
|
|
ASSERT_TRUE(strstr(files[0].rel_path, "main.go") != NULL);
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
/* ── .cbmignore negation vs built-in skip dirs (issue #500) ────── */
|
|
|
|
static bool discover_excluded_contains(char **excluded, int count, const char *rel_path) {
|
|
for (int i = 0; i < count; i++) {
|
|
if (strcmp(excluded[i], rel_path) == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* A "!obj/" negation in .cbmignore must un-skip the built-in ALWAYS_SKIP
|
|
* "obj" dir so files inside it get discovered — and the un-skipped dir must
|
|
* not be reported as an excluded subtree (#411 list stays coherent). */
|
|
TEST(discover_cbmignore_negates_always_skip_dir) {
|
|
char *base = th_mktempdir("cbm_disc_cbmi_neg_obj");
|
|
ASSERT(base != NULL);
|
|
|
|
th_write_file(TH_PATH(base, ".cbmignore"), "!obj/\n");
|
|
th_write_file(TH_PATH(base, "main.go"), "package main\n");
|
|
th_write_file(TH_PATH(base, "obj/generated.go"), "package obj\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
char **excluded = NULL;
|
|
int excluded_count = 0;
|
|
|
|
int rc = cbm_discover_ex(base, &opts, &files, &count, &excluded, &excluded_count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 2);
|
|
ASSERT_TRUE(discover_has_rel_path(files, count, "main.go"));
|
|
ASSERT_TRUE(discover_has_rel_path(files, count, "obj/generated.go"));
|
|
ASSERT_FALSE(discover_excluded_contains(excluded, excluded_count, "obj"));
|
|
|
|
cbm_discover_free_excluded(excluded, excluded_count);
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
/* An anchored negation ("!src/target/") un-skips only that nested dir; other
|
|
* dirs with the same basename stay built-in-skipped. */
|
|
TEST(discover_cbmignore_negates_only_nested_skip_dir) {
|
|
char *base = th_mktempdir("cbm_disc_cbmi_neg_nested");
|
|
ASSERT(base != NULL);
|
|
|
|
th_write_file(TH_PATH(base, ".cbmignore"), "!src/target/\n");
|
|
th_write_file(TH_PATH(base, "src/main.go"), "package src\n");
|
|
th_write_file(TH_PATH(base, "src/target/lib.go"), "package target\n");
|
|
th_write_file(TH_PATH(base, "other/target/lib.go"), "package other\n");
|
|
th_write_file(TH_PATH(base, "target/root.go"), "package root\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 2);
|
|
ASSERT_TRUE(discover_has_rel_path(files, count, "src/main.go"));
|
|
ASSERT_TRUE(discover_has_rel_path(files, count, "src/target/lib.go"));
|
|
ASSERT_FALSE(discover_has_rel_path(files, count, "other/target/lib.go"));
|
|
ASSERT_FALSE(discover_has_rel_path(files, count, "target/root.go"));
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
/* Negation also un-skips FAST-mode skip dirs ("docs" is in FAST_SKIP_DIRS). */
|
|
TEST(discover_cbmignore_negates_fast_skip_dir) {
|
|
char *base = th_mktempdir("cbm_disc_cbmi_neg_fast");
|
|
ASSERT(base != NULL);
|
|
|
|
th_write_file(TH_PATH(base, ".cbmignore"), "!docs/\n");
|
|
th_write_file(TH_PATH(base, "main.go"), "package main\n");
|
|
th_write_file(TH_PATH(base, "docs/guide.go"), "package docs\n");
|
|
|
|
cbm_discover_opts_t opts = {.mode = CBM_MODE_FAST};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 2);
|
|
ASSERT_TRUE(discover_has_rel_path(files, count, "main.go"));
|
|
ASSERT_TRUE(discover_has_rel_path(files, count, "docs/guide.go"));
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
/* Last-match-wins ordering: "obj/" then "!obj/" un-skips; "!obj/" then
|
|
* "obj/" re-ignores, so the built-in skip stands. */
|
|
TEST(discover_cbmignore_negation_last_match_wins) {
|
|
char *base = th_mktempdir("cbm_disc_cbmi_order1");
|
|
ASSERT(base != NULL);
|
|
|
|
th_write_file(TH_PATH(base, ".cbmignore"), "obj/\n!obj/\n");
|
|
th_write_file(TH_PATH(base, "main.go"), "package main\n");
|
|
th_write_file(TH_PATH(base, "obj/generated.go"), "package obj\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 2);
|
|
ASSERT_TRUE(discover_has_rel_path(files, count, "obj/generated.go"));
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
|
|
base = th_mktempdir("cbm_disc_cbmi_order2");
|
|
ASSERT(base != NULL);
|
|
|
|
th_write_file(TH_PATH(base, ".cbmignore"), "!obj/\nobj/\n");
|
|
th_write_file(TH_PATH(base, "main.go"), "package main\n");
|
|
th_write_file(TH_PATH(base, "obj/generated.go"), "package obj\n");
|
|
|
|
files = NULL;
|
|
count = 0;
|
|
|
|
rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 1);
|
|
ASSERT_TRUE(discover_has_rel_path(files, count, "main.go"));
|
|
ASSERT_FALSE(discover_has_rel_path(files, count, "obj/generated.go"));
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
/* Safety-core policy (#489, #802): .git, node_modules, and the
|
|
* worktree-internal dirs can never be un-skipped, even by an explicit
|
|
* .cbmignore negation. Green by construction; guards the policy. */
|
|
TEST(discover_cbmignore_negation_cannot_unskip_safety_core) {
|
|
char *base = th_mktempdir("cbm_disc_cbmi_safety");
|
|
ASSERT(base != NULL);
|
|
|
|
th_write_file(TH_PATH(base, ".cbmignore"),
|
|
"!.git/\n!node_modules/\n!.worktrees/\n!.claude-worktrees/\n");
|
|
th_write_file(TH_PATH(base, "main.go"), "package main\n");
|
|
th_write_file(TH_PATH(base, ".git/hooks/hook.go"), "package hooks\n");
|
|
th_write_file(TH_PATH(base, "node_modules/pkg/index.js"), "module.exports = 1;\n");
|
|
th_write_file(TH_PATH(base, ".worktrees/wt/dup.go"), "package dup\n");
|
|
th_write_file(TH_PATH(base, ".claude-worktrees/wt/dup.go"), "package dup\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 1);
|
|
ASSERT_TRUE(discover_has_rel_path(files, count, "main.go"));
|
|
ASSERT_FALSE(discover_has_rel_path(files, count, ".git/hooks/hook.go"));
|
|
ASSERT_FALSE(discover_has_rel_path(files, count, "node_modules/pkg/index.js"));
|
|
ASSERT_FALSE(discover_has_rel_path(files, count, ".worktrees/wt/dup.go"));
|
|
ASSERT_FALSE(discover_has_rel_path(files, count, ".claude-worktrees/wt/dup.go"));
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
/* ── .git/info/exclude tests (issue #489) ─────────────────────── */
|
|
|
|
/* Per-clone excludes written to .git/info/exclude (not committed) must be
|
|
* honored the same as .gitignore. Without this, repos that keep worktrees
|
|
* under a path excluded only via info/exclude hit OOM during indexing. */
|
|
TEST(discover_git_info_exclude) {
|
|
char *base = th_mktempdir("cbm_disc_exc");
|
|
ASSERT(base != NULL);
|
|
|
|
th_mkdir_p(TH_PATH(base, ".git/info"));
|
|
th_write_file(TH_PATH(base, ".git/info/exclude"), "worktrees/\n");
|
|
th_write_file(TH_PATH(base, "src/main.go"), "package main\n");
|
|
th_write_file(TH_PATH(base, "worktrees/feature/app.go"), "package app\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 1);
|
|
ASSERT_TRUE(strstr(files[0].rel_path, "main.go") != NULL);
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
TEST(discover_git_info_exclude_stacks_with_gitignore) {
|
|
char *base = th_mktempdir("cbm_disc_exc_stack");
|
|
ASSERT(base != NULL);
|
|
|
|
th_mkdir_p(TH_PATH(base, ".git/info"));
|
|
th_write_file(TH_PATH(base, ".gitignore"), "*.log\n");
|
|
th_write_file(TH_PATH(base, ".git/info/exclude"), "scratch/\n");
|
|
th_write_file(TH_PATH(base, "main.go"), "package main\n");
|
|
th_write_file(TH_PATH(base, "debug.log"), "log\n");
|
|
th_write_file(TH_PATH(base, "scratch/tmp.go"), "package scratch\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 1);
|
|
|
|
bool found_log = false;
|
|
bool found_scratch = false;
|
|
for (int i = 0; i < count; i++) {
|
|
if (strstr(files[i].rel_path, ".log")) found_log = true;
|
|
if (strstr(files[i].rel_path, "scratch")) found_scratch = true;
|
|
}
|
|
ASSERT_FALSE(found_log);
|
|
ASSERT_FALSE(found_scratch);
|
|
ASSERT_TRUE(strstr(files[0].rel_path, "main.go") != NULL);
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Linked-worktree ignore tests ──────────────────────────────── */
|
|
|
|
/* In a linked worktree, <repo>/.git is a regular file ("gitdir: <path>"), not a
|
|
* directory, and info/exclude + config live in the shared common dir named by
|
|
* <gitdir>/commondir. The discover step must follow the gitlink so per-clone
|
|
* excludes are honored exactly as in an ordinary checkout. Regression for the
|
|
* worktree gap left by issue #489 (which only handled .git as a directory). */
|
|
TEST(discover_worktree_info_exclude) {
|
|
char *base = th_mktempdir("cbm_disc_wt_exc");
|
|
ASSERT(base != NULL);
|
|
|
|
/* Worktree gitlink -> per-worktree gitdir (relative to the worktree root). */
|
|
th_write_file(TH_PATH(base, ".git"), "gitdir: maingit/worktrees/wt\n");
|
|
/* Per-worktree gitdir points back to the shared common dir via commondir. */
|
|
th_mkdir_p(TH_PATH(base, "maingit/worktrees/wt"));
|
|
th_write_file(TH_PATH(base, "maingit/worktrees/wt/commondir"), "../..\n");
|
|
/* Shared exclude lives in the common dir, not the per-worktree gitdir. */
|
|
th_mkdir_p(TH_PATH(base, "maingit/info"));
|
|
th_write_file(TH_PATH(base, "maingit/info/exclude"), "build/\n");
|
|
|
|
th_write_file(TH_PATH(base, "src/main.go"), "package main\n");
|
|
th_write_file(TH_PATH(base, "build/gen.go"), "package build\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 1);
|
|
ASSERT_TRUE(strstr(files[0].rel_path, "main.go") != NULL);
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
/* Committed .gitignore must still apply in a worktree even when commondir is
|
|
* absent (older gitdir layouts): the gitlink resolver falls back to the gitdir
|
|
* itself, and the root .gitignore is loaded unconditionally (issue #510). */
|
|
TEST(discover_worktree_committed_gitignore) {
|
|
char *base = th_mktempdir("cbm_disc_wt_gi");
|
|
ASSERT(base != NULL);
|
|
|
|
th_write_file(TH_PATH(base, ".git"), "gitdir: maingit/worktrees/wt\n");
|
|
th_mkdir_p(TH_PATH(base, "maingit/worktrees/wt"));
|
|
th_write_file(TH_PATH(base, ".gitignore"), "build/\n");
|
|
|
|
th_write_file(TH_PATH(base, "src/main.go"), "package main\n");
|
|
th_write_file(TH_PATH(base, "build/gen.go"), "package build\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
ASSERT_EQ(count, 1);
|
|
ASSERT_TRUE(strstr(files[0].rel_path, "main.go") != NULL);
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Nested .gitignore tests (issue #178) ──────────────────────── */
|
|
|
|
TEST(discover_nested_gitignore) {
|
|
char *base = th_mktempdir("cbm_disc_ngi");
|
|
ASSERT(base != NULL);
|
|
|
|
th_mkdir_p(TH_PATH(base, ".git"));
|
|
th_write_file(TH_PATH(base, "main.go"), "package main\n");
|
|
th_write_file(TH_PATH(base, "webapp/.gitignore"), "generated/\n");
|
|
th_write_file(TH_PATH(base, "webapp/src/routes.js"), "export default []\n");
|
|
th_write_file(TH_PATH(base, "webapp/generated/types.js"), "export {}\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
|
|
bool found_generated = false;
|
|
bool found_routes = false;
|
|
for (int i = 0; i < count; i++) {
|
|
if (strstr(files[i].rel_path, "generated"))
|
|
found_generated = true;
|
|
if (strstr(files[i].rel_path, "routes.js"))
|
|
found_routes = true;
|
|
}
|
|
ASSERT_FALSE(found_generated);
|
|
ASSERT_TRUE(found_routes);
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
TEST(discover_nested_gitignore_stacks_with_root) {
|
|
char *base = th_mktempdir("cbm_disc_ngi_stack");
|
|
ASSERT(base != NULL);
|
|
|
|
th_mkdir_p(TH_PATH(base, ".git"));
|
|
th_write_file(TH_PATH(base, ".gitignore"), "*.log\n");
|
|
th_write_file(TH_PATH(base, "webapp/.gitignore"), ".output/\n");
|
|
th_write_file(TH_PATH(base, "main.go"), "package main\n");
|
|
th_write_file(TH_PATH(base, "error.log"), "error log\n");
|
|
th_write_file(TH_PATH(base, "webapp/src/app.js"), "const x = 1\n");
|
|
th_write_file(TH_PATH(base, "webapp/.output/data.js"), "output data\n");
|
|
|
|
cbm_discover_opts_t opts = {0};
|
|
cbm_file_info_t *files = NULL;
|
|
int count = 0;
|
|
int rc = cbm_discover(base, &opts, &files, &count);
|
|
ASSERT_EQ(rc, 0);
|
|
|
|
bool found_log = false;
|
|
bool found_output = false;
|
|
bool found_main = false;
|
|
bool found_app = false;
|
|
for (int i = 0; i < count; i++) {
|
|
if (strstr(files[i].rel_path, ".log"))
|
|
found_log = true;
|
|
if (strstr(files[i].rel_path, ".output"))
|
|
found_output = true;
|
|
if (strstr(files[i].rel_path, "main.go"))
|
|
found_main = true;
|
|
if (strstr(files[i].rel_path, "app.js"))
|
|
found_app = true;
|
|
}
|
|
ASSERT_FALSE(found_log);
|
|
ASSERT_FALSE(found_output);
|
|
ASSERT_TRUE(found_main);
|
|
ASSERT_TRUE(found_app);
|
|
|
|
cbm_discover_free(files, count);
|
|
th_cleanup(base);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Suite ─────────────────────────────────────────────────────── */
|
|
|
|
SUITE(discover) {
|
|
/* Directory skip — always */
|
|
RUN_TEST(skip_git);
|
|
RUN_TEST(skip_node_modules);
|
|
RUN_TEST(skip_pycache);
|
|
RUN_TEST(skip_venv);
|
|
RUN_TEST(skip_dist);
|
|
RUN_TEST(skip_target);
|
|
RUN_TEST(skip_vendor);
|
|
RUN_TEST(skip_vendored);
|
|
RUN_TEST(skip_terraform);
|
|
RUN_TEST(skip_coverage);
|
|
RUN_TEST(skip_idea);
|
|
RUN_TEST(skip_claude);
|
|
|
|
/* Not skipped */
|
|
RUN_TEST(no_skip_src);
|
|
RUN_TEST(no_skip_lib);
|
|
RUN_TEST(no_skip_docs_full);
|
|
RUN_TEST(no_skip_test_full);
|
|
|
|
/* Fast mode directory skips */
|
|
RUN_TEST(skip_fast_docs);
|
|
RUN_TEST(skip_fast_examples);
|
|
RUN_TEST(skip_fast_tests);
|
|
RUN_TEST(skip_fast_fixtures);
|
|
RUN_TEST(skip_fast_testdata);
|
|
RUN_TEST(skip_fast_generated);
|
|
RUN_TEST(skip_fast_assets);
|
|
RUN_TEST(skip_fast_3rdparty);
|
|
RUN_TEST(skip_fast_e2e);
|
|
|
|
/* Suffix filters */
|
|
RUN_TEST(suffix_pyc);
|
|
RUN_TEST(suffix_o);
|
|
RUN_TEST(suffix_so);
|
|
RUN_TEST(suffix_png);
|
|
RUN_TEST(suffix_jpg);
|
|
RUN_TEST(suffix_wasm);
|
|
RUN_TEST(suffix_db);
|
|
RUN_TEST(suffix_sqlite);
|
|
RUN_TEST(suffix_tmp);
|
|
RUN_TEST(suffix_tilde);
|
|
RUN_TEST(suffix_go);
|
|
RUN_TEST(suffix_py);
|
|
RUN_TEST(suffix_c);
|
|
RUN_TEST(suffix_fast_zip);
|
|
RUN_TEST(suffix_fast_pdf);
|
|
RUN_TEST(suffix_fast_mp3);
|
|
RUN_TEST(suffix_fast_pem);
|
|
|
|
/* Filename skip */
|
|
RUN_TEST(fn_skip_license);
|
|
RUN_TEST(fn_skip_changelog);
|
|
RUN_TEST(fn_skip_gosum);
|
|
RUN_TEST(fn_skip_yarnlock);
|
|
RUN_TEST(fn_skip_pkglock);
|
|
RUN_TEST(fn_no_skip_license_full);
|
|
|
|
/* Fast mode patterns */
|
|
RUN_TEST(pattern_dts);
|
|
RUN_TEST(pattern_pbgo);
|
|
RUN_TEST(pattern_pb2py);
|
|
RUN_TEST(pattern_mock);
|
|
RUN_TEST(pattern_test_dot);
|
|
RUN_TEST(pattern_spec);
|
|
RUN_TEST(pattern_stories);
|
|
RUN_TEST(pattern_dts_full);
|
|
|
|
/* Integration tests (cross-platform) */
|
|
RUN_TEST(discover_simple);
|
|
RUN_TEST(discover_skips_git_dir);
|
|
RUN_TEST(discover_with_gitignore);
|
|
RUN_TEST(discover_with_global_xdg_ignore);
|
|
RUN_TEST(discover_global_excludesfile_from_gitconfig_tilde);
|
|
RUN_TEST(discover_repo_local_excludesfile_is_ignored);
|
|
RUN_TEST(discover_missing_global_excludes_is_noop);
|
|
RUN_TEST(discover_cbmignore_negates_global_ignore);
|
|
RUN_TEST(discover_gitignore_dir_excluded_issue234);
|
|
RUN_TEST(discover_max_file_size);
|
|
RUN_TEST(discover_null_path);
|
|
RUN_TEST(discover_nonexistent_path);
|
|
RUN_TEST(discover_free_null);
|
|
|
|
/* Go test ports (cross-platform) */
|
|
RUN_TEST(discover_skips_worktrees);
|
|
RUN_TEST(discover_cbmignore);
|
|
RUN_TEST(discover_cbmignore_stacks);
|
|
RUN_TEST(discover_symlink_skipped);
|
|
RUN_TEST(discover_new_ignore_patterns);
|
|
RUN_TEST(discover_generic_dirs_full_mode);
|
|
RUN_TEST(discover_generic_dirs_fast_mode);
|
|
RUN_TEST(discover_deploy_excluded_full_mode);
|
|
RUN_TEST(discover_cbmignore_no_git);
|
|
|
|
/* .cbmignore negation vs built-in skip dirs (issue #500) */
|
|
RUN_TEST(discover_cbmignore_negates_always_skip_dir);
|
|
RUN_TEST(discover_cbmignore_negates_only_nested_skip_dir);
|
|
RUN_TEST(discover_cbmignore_negates_fast_skip_dir);
|
|
RUN_TEST(discover_cbmignore_negation_last_match_wins);
|
|
RUN_TEST(discover_cbmignore_negation_cannot_unskip_safety_core);
|
|
|
|
/* .git/info/exclude support (issue #489) */
|
|
RUN_TEST(discover_git_info_exclude);
|
|
RUN_TEST(discover_git_info_exclude_stacks_with_gitignore);
|
|
|
|
/* Linked-worktree ignore resolution (gitlink + commondir) */
|
|
RUN_TEST(discover_worktree_info_exclude);
|
|
RUN_TEST(discover_worktree_committed_gitignore);
|
|
|
|
/* Nested .gitignore tests (issue #178) */
|
|
RUN_TEST(discover_nested_gitignore);
|
|
RUN_TEST(discover_nested_gitignore_stacks_with_root);
|
|
}
|