From 96aa566275494778670dbc4d1173bba8a64929dd Mon Sep 17 00:00:00 2001 From: Long Ho Date: Mon, 18 May 2026 10:32:13 -0400 Subject: [PATCH] test: add Bazel runfiles source aggregation --- crates/codescythe/analyze.rs | 15 +- crates/codescythe_cli/main.rs | 2 +- tests/bazel/BUILD.bazel | 1 + tests/bazel/codescythe_test.bzl | 175 ++++++++++++++++++ .../runfiles-false-positive/BUILD.bazel | 48 +++++ .../runfiles-false-positive/codescythe.json | 15 ++ .../generated/client.ts | 1 + .../runfiles-false-positive/package.json | 7 + .../runfiles-false-positive/src/main.ts | 5 + .../runfiles-false-positive/src/used.ts | 1 + tests/fixtures/runfiles-fixture/BUILD.bazel | 80 ++++++++ tests/fixtures/runfiles-fixture/README.md | 6 + .../fixtures/runfiles-fixture/codescythe.json | 17 ++ tests/fixtures/runfiles-fixture/package.json | 8 + .../protobuf/generated/client.ts | 2 + .../runfiles-fixture/protobuf/wrong/client.ts | 1 + .../runfiles-fixture/typespec/schema.ts | 1 + .../workspace/frontend/apps/client/index.html | 1 + .../frontend/apps/client/platform/index.html | 1 + .../apps/client/platform/platformRuntime.ts | 6 + .../frontend/apps/client/spa/index.html | 1 + .../workspace/frontend/dead.ts | 1 + .../workspace/frontend/lib/runtime.ts | 2 + 23 files changed, 393 insertions(+), 4 deletions(-) create mode 100644 tests/bazel/BUILD.bazel create mode 100644 tests/bazel/codescythe_test.bzl create mode 100644 tests/fixtures/runfiles-false-positive/BUILD.bazel create mode 100644 tests/fixtures/runfiles-false-positive/codescythe.json create mode 100644 tests/fixtures/runfiles-false-positive/generated/client.ts create mode 100644 tests/fixtures/runfiles-false-positive/package.json create mode 100644 tests/fixtures/runfiles-false-positive/src/main.ts create mode 100644 tests/fixtures/runfiles-false-positive/src/used.ts create mode 100644 tests/fixtures/runfiles-fixture/BUILD.bazel create mode 100644 tests/fixtures/runfiles-fixture/README.md create mode 100644 tests/fixtures/runfiles-fixture/codescythe.json create mode 100644 tests/fixtures/runfiles-fixture/package.json create mode 100644 tests/fixtures/runfiles-fixture/protobuf/generated/client.ts create mode 100644 tests/fixtures/runfiles-fixture/protobuf/wrong/client.ts create mode 100644 tests/fixtures/runfiles-fixture/typespec/schema.ts create mode 100644 tests/fixtures/runfiles-fixture/workspace/frontend/apps/client/index.html create mode 100644 tests/fixtures/runfiles-fixture/workspace/frontend/apps/client/platform/index.html create mode 100644 tests/fixtures/runfiles-fixture/workspace/frontend/apps/client/platform/platformRuntime.ts create mode 100644 tests/fixtures/runfiles-fixture/workspace/frontend/apps/client/spa/index.html create mode 100644 tests/fixtures/runfiles-fixture/workspace/frontend/dead.ts create mode 100644 tests/fixtures/runfiles-fixture/workspace/frontend/lib/runtime.ts diff --git a/crates/codescythe/analyze.rs b/crates/codescythe/analyze.rs index 2e75fa2..bcc9255 100644 --- a/crates/codescythe/analyze.rs +++ b/crates/codescythe/analyze.rs @@ -86,7 +86,7 @@ pub fn analyze_path( config: &CodescytheConfig, options: AnalysisOptions, ) -> Result { - let cwd = normalize_path(cwd); + let cwd = absolute_normalize_path(cwd)?; if !cwd.exists() { anyhow::bail!("analysis root does not exist: {}", cwd.display()); } @@ -321,7 +321,7 @@ fn discover_project_files(cwd: &Path, config: &CodescytheConfig) -> Result Result { } fn should_enter(entry: &DirEntry) -> bool { - if !entry.file_type().is_dir() { + if !entry.path().is_dir() { return true; } !matches!( @@ -1277,6 +1277,15 @@ fn relative_path(cwd: &Path, path: &Path) -> String { .replace('\\', "/") } +fn absolute_normalize_path(path: &Path) -> Result { + let path = if path.is_absolute() { + path.to_path_buf() + } else { + env::current_dir()?.join(path) + }; + Ok(normalize_path(&path)) +} + fn normalize_path(path: &Path) -> PathBuf { let mut normalized = PathBuf::new(); for component in path.components() { diff --git a/crates/codescythe_cli/main.rs b/crates/codescythe_cli/main.rs index d538bcc..e956a6a 100644 --- a/crates/codescythe_cli/main.rs +++ b/crates/codescythe_cli/main.rs @@ -156,7 +156,7 @@ mod tests { #[test] fn explicit_directory_overrides_config_parent() { let directory = Path::new("/tmp/runfiles/_main"); - let config = Path::new("/tmp/runfiles/_main/pplx/frontend/codescythe.json"); + let config = Path::new("/tmp/runfiles/_main/workspace/frontend/codescythe.json"); let analysis_root = analysis_root(Some(directory), Some(config)).unwrap(); diff --git a/tests/bazel/BUILD.bazel b/tests/bazel/BUILD.bazel new file mode 100644 index 0000000..ffd0fb0 --- /dev/null +++ b/tests/bazel/BUILD.bazel @@ -0,0 +1 @@ +package(default_visibility = ["//visibility:public"]) diff --git a/tests/bazel/codescythe_test.bzl b/tests/bazel/codescythe_test.bzl new file mode 100644 index 0000000..7b41841 --- /dev/null +++ b/tests/bazel/codescythe_test.bzl @@ -0,0 +1,175 @@ +CodescytheSourcesInfo = provider( + doc = "Source files collected for a Codescythe runfiles test.", + fields = { + "sources": "depset of source files", + }, +) + +def _source_group_impl(ctx): + return [ + DefaultInfo( + files = depset( + ctx.files.srcs, + transitive = [dep[DefaultInfo].files for dep in ctx.attr.deps], + ), + ), + ] + +source_group = rule( + implementation = _source_group_impl, + attrs = { + "deps": attr.label_list(), + "srcs": attr.label_list(allow_files = True), + }, +) + +def _codescythe_sources_aspect_impl(target, ctx): + transitive = [] + if hasattr(ctx.rule.attr, "deps"): + transitive.extend([ + dep[CodescytheSourcesInfo].sources + for dep in ctx.rule.attr.deps + if CodescytheSourcesInfo in dep + ]) + + direct = [] + if hasattr(ctx.rule.attr, "srcs"): + for src in ctx.rule.attr.srcs: + direct.extend(src.files.to_list()) + + return [ + CodescytheSourcesInfo( + sources = depset(direct, transitive = transitive), + ), + ] + +_codescythe_sources_aspect = aspect( + implementation = _codescythe_sources_aspect_impl, + attr_aspects = ["deps"], +) + +def _codescythe_test_impl(ctx): + source_depsets = [ + target[CodescytheSourcesInfo].sources + for target in ctx.attr.targets + if CodescytheSourcesInfo in target + ] + script = ctx.actions.declare_file(ctx.label.name + ".sh") + ctx.actions.write( + output = script, + is_executable = True, + content = _test_script( + codescythe = ctx.executable._codescythe.short_path, + config = ctx.file.config.short_path, + expected_exit_code = ctx.attr.expected_exit_code, + must_contain = ctx.attr.must_contain, + must_not_contain = ctx.attr.must_not_contain, + ), + ) + + runfiles = ctx.runfiles( + files = [ctx.executable._codescythe, ctx.file.config] + ctx.files.data, + transitive_files = depset(transitive = source_depsets), + ).merge(ctx.attr._codescythe[DefaultInfo].default_runfiles) + + return [DefaultInfo(executable = script, runfiles = runfiles)] + +codescythe_test = rule( + implementation = _codescythe_test_impl, + attrs = { + "config": attr.label(allow_single_file = True, mandatory = True), + "data": attr.label_list(allow_files = True), + "expected_exit_code": attr.int(default = 0), + "must_contain": attr.string_list(), + "must_not_contain": attr.string_list(), + "targets": attr.label_list( + aspects = [_codescythe_sources_aspect], + mandatory = True, + ), + "_codescythe": attr.label( + default = Label("//crates/codescythe_cli:codescythe"), + executable = True, + cfg = "exec", + ), + }, + test = True, +) + +def _test_script(codescythe, config, expected_exit_code, must_contain, must_not_contain): + return """#!/usr/bin/env bash +set -euo pipefail + +runfile() {{ + local path="$1" + if [[ -n "${{RUNFILES_DIR:-}}" && -e "${{RUNFILES_DIR}}/${{path}}" ]]; then + printf '%s\\n' "${{RUNFILES_DIR}}/${{path}}" + return + fi + if [[ -n "${{RUNFILES_DIR:-}}" ]]; then + for workspace in "${{TEST_WORKSPACE:-_main}}" _main codescythe; do + if [[ -e "${{RUNFILES_DIR}}/${{workspace}}/${{path}}" ]]; then + printf '%s\\n' "${{RUNFILES_DIR}}/${{workspace}}/${{path}}" + return + fi + done + fi + if [[ -n "${{TEST_SRCDIR:-}}" ]]; then + for workspace in "${{TEST_WORKSPACE:-_main}}" _main codescythe; do + if [[ -e "${{TEST_SRCDIR}}/${{workspace}}/${{path}}" ]]; then + printf '%s\\n' "${{TEST_SRCDIR}}/${{workspace}}/${{path}}" + return + fi + done + fi + printf '%s\\n' "${{path}}" +}} + +codescythe="$(runfile {codescythe})" +config="$(runfile {config})" +stdout="${{TEST_TMPDIR}}/codescythe.stdout.json" +stderr="${{TEST_TMPDIR}}/codescythe.stderr.txt" + +set +e +"${{codescythe}}" --config "${{config}}" --json --compact-json >"${{stdout}}" 2>"${{stderr}}" +status="$?" +set -e + +if [[ "${{status}}" -ne {expected_exit_code} ]]; then + echo "expected exit code {expected_exit_code}, got ${{status}}" >&2 + cat "${{stdout}}" >&2 + cat "${{stderr}}" >&2 + exit 1 +fi + +if [[ -s "${{stderr}}" ]]; then + cat "${{stderr}}" >&2 + exit 1 +fi + +must_contain=({must_contain}) +for needle in "${{must_contain[@]}}"; do + if ! grep -F -- "${{needle}}" "${{stdout}}" >/dev/null; then + echo "expected Codescythe output to contain: ${{needle}}" >&2 + cat "${{stdout}}" >&2 + exit 1 + fi +done + +must_not_contain=({must_not_contain}) +for needle in "${{must_not_contain[@]}}"; do + if grep -F -- "${{needle}}" "${{stdout}}" >/dev/null; then + echo "expected Codescythe output not to contain: ${{needle}}" >&2 + cat "${{stdout}}" >&2 + exit 1 + fi +done +""".format( + codescythe = _shell_quote(codescythe), + config = _shell_quote(config), + expected_exit_code = expected_exit_code, + must_contain = " ".join([_shell_quote(value) for value in must_contain]), + must_not_contain = " ".join([_shell_quote(value) for value in must_not_contain]), + ) + +def _shell_quote(value): + return "'" + value.replace("'", "'\\''") + "'" diff --git a/tests/fixtures/runfiles-false-positive/BUILD.bazel b/tests/fixtures/runfiles-false-positive/BUILD.bazel new file mode 100644 index 0000000..541d767 --- /dev/null +++ b/tests/fixtures/runfiles-false-positive/BUILD.bazel @@ -0,0 +1,48 @@ +load("//tests/bazel:codescythe_test.bzl", "codescythe_test", "source_group") + +source_group( + name = "app_sources", + srcs = [ + "src/main.ts", + ], + deps = [ + ":generated_sources", + ":library_sources", + ], +) + +source_group( + name = "generated_sources", + srcs = [ + "generated/client.ts", + ], +) + +source_group( + name = "library_sources", + srcs = [ + "src/used.ts", + ], +) + +source_group( + name = "all_sources", + deps = [ + ":app_sources", + ], +) + +codescythe_test( + name = "codescythe_false_positive_test", + config = "codescythe.json", + data = ["package.json"], + expected_exit_code = 0, + must_contain = [ + "\"processed\":3", + "\"total\":3", + ], + must_not_contain = [ + "#virtual_generated/api/foo", + ], + targets = [":all_sources"], +) diff --git a/tests/fixtures/runfiles-false-positive/codescythe.json b/tests/fixtures/runfiles-false-positive/codescythe.json new file mode 100644 index 0000000..589fe86 --- /dev/null +++ b/tests/fixtures/runfiles-false-positive/codescythe.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../../codescythe.schema.json", + "entry": "src/main.ts", + "project": [ + "src/**/*.ts", + "generated/**/*.ts" + ], + "aliases": { + "#generated/*": "./generated/*.ts" + }, + "unresolvedImports": { + "mode": "report", + "ignore": ["#virtual_generated/**"] + } +} diff --git a/tests/fixtures/runfiles-false-positive/generated/client.ts b/tests/fixtures/runfiles-false-positive/generated/client.ts new file mode 100644 index 0000000..440fd31 --- /dev/null +++ b/tests/fixtures/runfiles-false-positive/generated/client.ts @@ -0,0 +1 @@ +export const client = 'client'; diff --git a/tests/fixtures/runfiles-false-positive/package.json b/tests/fixtures/runfiles-false-positive/package.json new file mode 100644 index 0000000..2213092 --- /dev/null +++ b/tests/fixtures/runfiles-false-positive/package.json @@ -0,0 +1,7 @@ +{ + "name": "codescythe-bazel-runfiles-false-positive-fixture", + "type": "module", + "imports": { + "#app/*": "./src/*.ts" + } +} diff --git a/tests/fixtures/runfiles-false-positive/src/main.ts b/tests/fixtures/runfiles-false-positive/src/main.ts new file mode 100644 index 0000000..0fd58db --- /dev/null +++ b/tests/fixtures/runfiles-false-positive/src/main.ts @@ -0,0 +1,5 @@ +import { used } from '#app/used'; +import { client } from '#generated/client'; +import '#virtual_generated/api/foo'; + +console.log(used, client); diff --git a/tests/fixtures/runfiles-false-positive/src/used.ts b/tests/fixtures/runfiles-false-positive/src/used.ts new file mode 100644 index 0000000..a81bcab --- /dev/null +++ b/tests/fixtures/runfiles-false-positive/src/used.ts @@ -0,0 +1 @@ +export const used = 'used'; diff --git a/tests/fixtures/runfiles-fixture/BUILD.bazel b/tests/fixtures/runfiles-fixture/BUILD.bazel new file mode 100644 index 0000000..3425038 --- /dev/null +++ b/tests/fixtures/runfiles-fixture/BUILD.bazel @@ -0,0 +1,80 @@ +load("//tests/bazel:codescythe_test.bzl", "codescythe_test", "source_group") + +source_group( + name = "entry_sources", + srcs = [ + "workspace/frontend/apps/client/platform/platformRuntime.ts", + ], + deps = [ + ":generated_sources", + ":library_sources", + ], +) + +source_group( + name = "generated_sources", + srcs = [ + "protobuf/generated/client.ts", + ], +) + +source_group( + name = "library_sources", + srcs = [ + "workspace/frontend/lib/runtime.ts", + ], +) + +source_group( + name = "unused_sources", + srcs = [ + "protobuf/wrong/client.ts", + "typespec/schema.ts", + "workspace/frontend/dead.ts", + ], +) + +source_group( + name = "all_sources", + deps = [ + ":entry_sources", + ":unused_sources", + ], +) + +filegroup( + name = "fixture_files", + srcs = [ + ":all_sources", + "codescythe.json", + "package.json", + "workspace/frontend/apps/client/index.html", + "workspace/frontend/apps/client/platform/index.html", + "workspace/frontend/apps/client/spa/index.html", + ], + visibility = ["//visibility:public"], +) + +codescythe_test( + name = "codescythe_runfiles_test", + config = "codescythe.json", + data = [ + "package.json", + "workspace/frontend/apps/client/index.html", + "workspace/frontend/apps/client/platform/index.html", + "workspace/frontend/apps/client/spa/index.html", + ], + expected_exit_code = 1, + must_contain = [ + "./missing", + "\"processed\":6", + "\"total\":6", + "protobuf/wrong/client.ts", + "typespec/schema.ts", + "workspace/frontend/dead.ts", + ], + must_not_contain = [ + "#virtual_generated/api/foo", + ], + targets = [":all_sources"], +) diff --git a/tests/fixtures/runfiles-fixture/README.md b/tests/fixtures/runfiles-fixture/README.md new file mode 100644 index 0000000..965c521 --- /dev/null +++ b/tests/fixtures/runfiles-fixture/README.md @@ -0,0 +1,6 @@ +# Bazel Runfiles Fixture + +This fixture mirrors a Bazel runfiles setup: the CLI is pointed at a root config +file, root `package.json#imports` are available, explicit aliases can override +package imports, generated namespaces can be ignored, and runtime-only leaves +remain explicit entries. diff --git a/tests/fixtures/runfiles-fixture/codescythe.json b/tests/fixtures/runfiles-fixture/codescythe.json new file mode 100644 index 0000000..c163342 --- /dev/null +++ b/tests/fixtures/runfiles-fixture/codescythe.json @@ -0,0 +1,17 @@ +{ + "$schema": "../../../codescythe.schema.json", + "entry": [ + "workspace/frontend/apps/client/index.html", + "workspace/frontend/apps/client/spa/index.html", + "workspace/frontend/apps/client/platform/index.html", + "workspace/frontend/apps/client/platform/platformRuntime.ts" + ], + "project": "**/*.ts", + "aliases": { + "#bazel_generated/*": "./protobuf/generated/*.ts" + }, + "unresolvedImports": { + "mode": "report", + "ignore": ["#virtual_generated/**"] + } +} diff --git a/tests/fixtures/runfiles-fixture/package.json b/tests/fixtures/runfiles-fixture/package.json new file mode 100644 index 0000000..944ea70 --- /dev/null +++ b/tests/fixtures/runfiles-fixture/package.json @@ -0,0 +1,8 @@ +{ + "name": "codescythe-bazel-runfiles-fixture", + "type": "module", + "imports": { + "#app/*": "./workspace/frontend/lib/*.ts", + "#bazel_generated/*": "./protobuf/wrong/*.ts" + } +} diff --git a/tests/fixtures/runfiles-fixture/protobuf/generated/client.ts b/tests/fixtures/runfiles-fixture/protobuf/generated/client.ts new file mode 100644 index 0000000..14f613d --- /dev/null +++ b/tests/fixtures/runfiles-fixture/protobuf/generated/client.ts @@ -0,0 +1,2 @@ +export const client = 'client'; +export const unusedClient = 'unused-client'; diff --git a/tests/fixtures/runfiles-fixture/protobuf/wrong/client.ts b/tests/fixtures/runfiles-fixture/protobuf/wrong/client.ts new file mode 100644 index 0000000..1d70a57 --- /dev/null +++ b/tests/fixtures/runfiles-fixture/protobuf/wrong/client.ts @@ -0,0 +1 @@ +export const client = 'wrong-client'; diff --git a/tests/fixtures/runfiles-fixture/typespec/schema.ts b/tests/fixtures/runfiles-fixture/typespec/schema.ts new file mode 100644 index 0000000..fe33a2b --- /dev/null +++ b/tests/fixtures/runfiles-fixture/typespec/schema.ts @@ -0,0 +1 @@ +export const schema = 'schema'; diff --git a/tests/fixtures/runfiles-fixture/workspace/frontend/apps/client/index.html b/tests/fixtures/runfiles-fixture/workspace/frontend/apps/client/index.html new file mode 100644 index 0000000..8998d3c --- /dev/null +++ b/tests/fixtures/runfiles-fixture/workspace/frontend/apps/client/index.html @@ -0,0 +1 @@ + diff --git a/tests/fixtures/runfiles-fixture/workspace/frontend/apps/client/platform/index.html b/tests/fixtures/runfiles-fixture/workspace/frontend/apps/client/platform/index.html new file mode 100644 index 0000000..1fd7b38 --- /dev/null +++ b/tests/fixtures/runfiles-fixture/workspace/frontend/apps/client/platform/index.html @@ -0,0 +1 @@ + diff --git a/tests/fixtures/runfiles-fixture/workspace/frontend/apps/client/platform/platformRuntime.ts b/tests/fixtures/runfiles-fixture/workspace/frontend/apps/client/platform/platformRuntime.ts new file mode 100644 index 0000000..376695b --- /dev/null +++ b/tests/fixtures/runfiles-fixture/workspace/frontend/apps/client/platform/platformRuntime.ts @@ -0,0 +1,6 @@ +import { runtime } from '#app/runtime'; +import { client } from '#bazel_generated/client'; +import '#virtual_generated/api/foo'; +import './missing'; + +console.log(runtime, client); diff --git a/tests/fixtures/runfiles-fixture/workspace/frontend/apps/client/spa/index.html b/tests/fixtures/runfiles-fixture/workspace/frontend/apps/client/spa/index.html new file mode 100644 index 0000000..0c17a84 --- /dev/null +++ b/tests/fixtures/runfiles-fixture/workspace/frontend/apps/client/spa/index.html @@ -0,0 +1 @@ + diff --git a/tests/fixtures/runfiles-fixture/workspace/frontend/dead.ts b/tests/fixtures/runfiles-fixture/workspace/frontend/dead.ts new file mode 100644 index 0000000..c313dac --- /dev/null +++ b/tests/fixtures/runfiles-fixture/workspace/frontend/dead.ts @@ -0,0 +1 @@ +export const dead = 'dead'; diff --git a/tests/fixtures/runfiles-fixture/workspace/frontend/lib/runtime.ts b/tests/fixtures/runfiles-fixture/workspace/frontend/lib/runtime.ts new file mode 100644 index 0000000..2db76df --- /dev/null +++ b/tests/fixtures/runfiles-fixture/workspace/frontend/lib/runtime.ts @@ -0,0 +1,2 @@ +export const runtime = 'runtime'; +export const unusedRuntime = 'unused-runtime';