diff --git a/codesage/cli/commands/snapshot.py b/codesage/cli/commands/snapshot.py index 27e3ed6..ff4aaea 100644 --- a/codesage/cli/commands/snapshot.py +++ b/codesage/cli/commands/snapshot.py @@ -98,7 +98,7 @@ def _create_snapshot_data(path, project_name): @snapshot.command('create') @click.argument('path', type=click.Path(exists=True, dir_okay=True)) @click.option('--project', '-p', 'project_name_override', help='Override the project name.') -@click.option('--format', '-f', type=click.Choice(['json', 'python-semantic-digest', 'go-semantic-digest']), default='json', help='Snapshot format.') +@click.option('--format', '-f', type=click.Choice(['yaml', 'json', 'md']), default='yaml', help='Snapshot format.') @click.option('--output', '-o', type=click.Path(), default=None, help='Output file path.') @click.option('--compress', is_flag=True, help='Enable compression.') @click.option('--language', '-l', type=click.Choice(['python', 'go', 'shell', 'java', 'auto']), default='auto', help='Language to analyze.') @@ -110,82 +110,69 @@ def create(ctx, path, project_name_override, format, output, compress, language) try: root_path = Path(path) - if format in ['python-semantic-digest', 'go-semantic-digest']: - if output is None: - output = f"{root_path.name}_{language}_semantic_digest.yaml" + if language == 'auto': + if list(root_path.rglob("*.py")): + language = "python" + elif list(root_path.rglob("*.go")): + language = "go" + elif list(root_path.rglob("*.java")): + language = "java" + elif list(root_path.rglob("*.sh")): + language = "shell" + else: + click.echo("Could not auto-detect language.", err=True) + return + if language in ['python', 'go']: config = SnapshotConfig() builder = None - - if language == 'auto': - if format == 'python-semantic-digest': - language = 'python' - elif format == 'go-semantic-digest': - language = 'go' - else: - # Fallback for auto-detection if format doesn't imply language - if list(root_path.rglob("*.py")): - language = "python" - elif list(root_path.rglob("*.go")): - language = "go" - elif list(root_path.rglob("*.java")): - language = "java" - elif list(root_path.rglob("*.sh")): - language = "shell" - else: - click.echo("Could not auto-detect language.", err=True) - return - - if language == 'python' and format == 'python-semantic-digest': + if language == 'python': builder = PythonSemanticSnapshotBuilder(root_path, config) - elif language == 'go' and format == 'go-semantic-digest': + else: # language == 'go' builder = GoSemanticSnapshotBuilder(root_path, config) - # Preserve other language builders for future use, but they won't be triggered - # by the current format options. - elif language == 'shell': - builder = ShellSemanticSnapshotBuilder(root_path, config) - elif language == 'java': - builder = JavaSemanticSnapshotBuilder(root_path, config) - else: - click.echo(f"Unsupported language/format combination: {language}/{format}", err=True) - return project_snapshot = builder.build() - generator = YAMLGenerator() - generator.export(project_snapshot, Path(output)) - - click.echo(f"{language.capitalize()} semantic digest created at {output}") - return - - snapshot_data = _create_snapshot_data(path, project_name) + if output is None: + output = f"{root_path.name}_snapshot.{format}" - if output: output_path = Path(output) output_path.parent.mkdir(parents=True, exist_ok=True) - # Use model_dump_json for consistency - with open(output_path, 'w', encoding='utf-8') as f: - f.write(snapshot_data.model_dump_json(indent=2)) - click.echo(f"Snapshot created at {output}") - else: - manager = SnapshotVersionManager(SNAPSHOT_DIR, project_name, DEFAULT_SNAPSHOT_CONFIG['snapshot']) + if format == 'yaml': + generator = YAMLGenerator() + generator.export(project_snapshot, output_path) + elif format == 'json': + with open(output_path, 'w', encoding='utf-8') as f: + json.dump(project_snapshot, f, indent=2) + elif format == 'md': + click.echo("Markdown format is not yet implemented.", err=True) + return - # The format for saving via manager is 'json', not the input format for semantic digests - save_format = 'json' + click.echo(f"Snapshot created at {output}") - if compress: - snapshot_path = manager.save_snapshot(snapshot_data, save_format) + else: # Fallback to original snapshot logic for other languages + snapshot_data = _create_snapshot_data(path, project_name) - # Compress the file - with open(snapshot_path, 'rb') as f_in: - with gzip.open(f"{snapshot_path}.gz", 'wb') as f_out: - f_out.writelines(f_in) - os.remove(snapshot_path) - click.echo(f"Compressed snapshot created at {snapshot_path}.gz") + if output: + output_path = Path(output) + output_path.parent.mkdir(parents=True, exist_ok=True) + with open(output_path, 'w', encoding='utf-8') as f: + f.write(snapshot_data.model_dump_json(indent=2)) + click.echo(f"Snapshot created at {output}") else: - snapshot_path = manager.save_snapshot(snapshot_data, save_format) - click.echo(f"Snapshot created at {snapshot_path}") + manager = SnapshotVersionManager(SNAPSHOT_DIR, project_name, DEFAULT_SNAPSHOT_CONFIG['snapshot']) + save_format = 'json' + if compress: + snapshot_path = manager.save_snapshot(snapshot_data, save_format) + with open(snapshot_path, 'rb') as f_in: + with gzip.open(f"{snapshot_path}.gz", 'wb') as f_out: + f_out.writelines(f_in) + os.remove(snapshot_path) + click.echo(f"Compressed snapshot created at {snapshot_path}.gz") + else: + snapshot_path = manager.save_snapshot(snapshot_data, save_format) + click.echo(f"Snapshot created at {snapshot_path}") finally: audit_logger.log( AuditEvent( diff --git a/codesage/semantic_digest/go_snapshot_builder.py b/codesage/semantic_digest/go_snapshot_builder.py index c86b71c..ad401f7 100644 --- a/codesage/semantic_digest/go_snapshot_builder.py +++ b/codesage/semantic_digest/go_snapshot_builder.py @@ -233,8 +233,8 @@ if len(f.Names) == 0 { ps = append(ps, typeStr) } else { - for _, name := range f.Names { - ps = append(ps, name.Name+" "+typeStr) + for range f.Names { + ps = append(ps, typeStr) // 简化:只存类型,省 token } } } @@ -264,25 +264,41 @@ """ class GoSemanticSnapshotBuilder(BaseLanguageSnapshotBuilder): - def build(self) -> Dict[str, Any]: - has_go = False + _parser_bin_path = None + _temp_dir = None + + def __init__(self, root_path: Path, config: SnapshotConfig): + super().__init__(root_path, config) + self._setup_parser() + + def _setup_parser(self): + if GoSemanticSnapshotBuilder._parser_bin_path: + return + try: subprocess.check_call(["go", "version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - has_go = True + GoSemanticSnapshotBuilder._temp_dir = tempfile.TemporaryDirectory() + parser_src_path = os.path.join(GoSemanticSnapshotBuilder._temp_dir.name, "parser.go") + with open(parser_src_path, "w", encoding="utf-8") as f: + f.write(GO_AST_PARSER_SRC) + + parser_bin_path = os.path.join(GoSemanticSnapshotBuilder._temp_dir.name, "parser") + subprocess.run(["go", "build", "-o", parser_bin_path, parser_src_path], capture_output=True, text=True, check=True) + GoSemanticSnapshotBuilder._parser_bin_path = parser_bin_path except (subprocess.CalledProcessError, FileNotFoundError): - pass + GoSemanticSnapshotBuilder._parser_bin_path = None + def build(self) -> Dict[str, Any]: digest = { "root": self.root_path.name, "pkgs": {}, "graph": {}, "meta": {} } pkg_map = defaultdict(list) all_files = self._collect_files() - total_cx = 0 total_err_checks = 0 for fpath in all_files: - data = self._extract_semantics(fpath, has_go) + data = self._extract_semantics(fpath) pkg_name = data.get("pk", "unknown") clean_data = {k: v for k, v in data.items() if v} clean_data["f"] = str(fpath.relative_to(self.root_path)) @@ -295,9 +311,6 @@ def build(self) -> Dict[str, Any]: "er": data["stat"].get("er", 0), } - if "fn" in data: - total_cx += sum(fn.get("cx", 1) for fn in data["fn"]) - pkg_map[pkg_name].append(clean_data) deps = {imp for imp in data.get("im", []) if "." in imp} @@ -315,8 +328,8 @@ def build(self) -> Dict[str, Any]: digest["meta"] = { "files": len(all_files), "pkgs": len(pkg_map), - "total_complexity": total_cx, "error_hotspots": total_err_checks, - "strategy": "AST" if has_go else "Regex" + "error_hotspots": total_err_checks, + "strategy": "AST" if GoSemanticSnapshotBuilder._parser_bin_path else "Regex" } return digest @@ -324,25 +337,18 @@ def build(self) -> Dict[str, Any]: def _collect_files(self) -> List[Path]: return list(self.root_path.rglob("*.go")) - def _extract_semantics(self, file_path: Path, has_go: bool) -> Dict[str, Any]: - if has_go: - with tempfile.TemporaryDirectory() as temp_dir: - parser_src_path = os.path.join(temp_dir, "parser.go") - with open(parser_src_path, "w", encoding="utf-8") as f: - f.write(GO_AST_PARSER_SRC) - - parser_bin_path = os.path.join(temp_dir, "parser") - try: - build_result = subprocess.run(["go", "build", "-o", parser_bin_path, parser_src_path], capture_output=True, text=True, check=True) - cmd = [parser_bin_path, str(file_path)] - output = subprocess.check_output(cmd, stderr=subprocess.PIPE, timeout=15) - return json.loads(output.decode('utf-8')) - except (subprocess.CalledProcessError, subprocess.TimeoutExpired, json.JSONDecodeError) as e: - print(f"AST parsing failed for {file_path}: {e}") - if isinstance(e, subprocess.CalledProcessError): - print(f"Stderr: {e.stderr}") - if hasattr(e, 'stdout'): - print(f"Stdout: {e.stdout}") + def _extract_semantics(self, file_path: Path) -> Dict[str, Any]: + if GoSemanticSnapshotBuilder._parser_bin_path: + try: + cmd = [GoSemanticSnapshotBuilder._parser_bin_path, str(file_path)] + output = subprocess.check_output(cmd, stderr=subprocess.PIPE, timeout=15) + return json.loads(output.decode('utf-8')) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, json.JSONDecodeError) as e: + print(f"AST parsing failed for {file_path}: {e}") + if isinstance(e, subprocess.CalledProcessError): + print(f"Stderr: {e.stderr}") + if hasattr(e, 'stdout'): + print(f"Stdout: {e.stdout}") # Fallback to regex content = file_path.read_text(encoding="utf-8", errors="ignore") diff --git a/codesage/semantic_digest/python_snapshot_builder.py b/codesage/semantic_digest/python_snapshot_builder.py index 2ac6d9a..ecab661 100644 --- a/codesage/semantic_digest/python_snapshot_builder.py +++ b/codesage/semantic_digest/python_snapshot_builder.py @@ -102,6 +102,17 @@ def build(self) -> Dict[str, Any]: digest["deps"][module_name].add(module) self._finalize_digest(digest, total_ccn, all_imports) + + # Convert defaultdicts to dicts for clean output + final_modules = {} + for name, data in digest["modules"].items(): + data["fim"] = dict(data["fim"]) + data["dc"] = sorted(list(data["dc"])) + final_modules[name] = data + digest["modules"] = final_modules + digest["deps"] = {mod: sorted(list(deps)) for mod, deps in digest["deps"].items()} + + return digest def _collect_files(self) -> List[Path]: diff --git a/codesage/snapshot/versioning.py b/codesage/snapshot/versioning.py index bc514a2..e67b55b 100644 --- a/codesage/snapshot/versioning.py +++ b/codesage/snapshot/versioning.py @@ -83,31 +83,26 @@ def _update_index(self, snapshot_path: str, metadata: SnapshotMetadata): self._save_index(index) def _get_expired_snapshots(self, index: List[Dict[str, Any]], now: datetime) -> List[Dict[str, Any]]: - """Identifies expired snapshots based on retention policies.""" - - def parse_timestamp(ts_str): - ts = datetime.fromisoformat(ts_str) - if ts.tzinfo is None: - return ts.replace(tzinfo=timezone.utc) - return ts - - try: - sorted_snapshots = sorted( - index, - key=lambda s: parse_timestamp(s["timestamp"]), - reverse=True - ) - except (ValueError, TypeError): - return [] - - kept_snapshots = sorted_snapshots[:self.max_versions] - - kept_by_date = { - s['version'] for s in kept_snapshots - if (now - parse_timestamp(s["timestamp"])) <= timedelta(days=self.retention_days) - } - - return [s for s in index if s["version"] not in kept_by_date] + """Identifies expired snapshots.""" + valid_snapshots = [] + for s in index: + try: + ts = datetime.fromisoformat(s["timestamp"]) + if ts.tzinfo is None: + ts = ts.replace(tzinfo=timezone.utc) + if now - ts <= timedelta(days=self.retention_days): + valid_snapshots.append(s) + except ValueError: + # Skip malformed timestamps + continue + + if len(valid_snapshots) > self.max_versions: + valid_snapshots = sorted( + valid_snapshots, key=lambda s: s["timestamp"], reverse=True + )[:self.max_versions] + + valid_versions = {s["version"] for s in valid_snapshots} + return [s for s in index if s["version"] not in valid_versions] def cleanup_expired_snapshots(self) -> int: """Removes expired snapshots and returns the count of deleted files.""" diff --git a/go_test_codesage.yaml b/go_test_codesage.yaml deleted file mode 100644 index 595b2e3..0000000 --- a/go_test_codesage.yaml +++ /dev/null @@ -1,8 +0,0 @@ -root: go_test -pkgs: - main: - files: - - main.go -meta: - files: 1 - pkgs: 1 diff --git a/go_test_script.yaml b/go_test_script.yaml deleted file mode 100644 index 595b2e3..0000000 --- a/go_test_script.yaml +++ /dev/null @@ -1,8 +0,0 @@ -root: go_test -pkgs: - main: - files: - - main.go -meta: - files: 1 - pkgs: 1 diff --git a/python_test_codesage.yaml b/python_test_codesage.yaml deleted file mode 100644 index 52ccf96..0000000 --- a/python_test_codesage.yaml +++ /dev/null @@ -1,70 +0,0 @@ -root: /app/examples/python_test -type: python -files: -- main.py -modules: - main: - f: - - main.py - im: [] - fim: {} - cl: - - n: Greeter - ln: 1 - bs: [] - dc: [] - attrs: [] - fn: - - n: main - ln: 8 - cx: 1 - args: [] - dc: [] - ret: null - async: false - md: - Greeter: - - n: __init__ - ln: 2 - cx: 1 - args: - - self - - name - dc: [] - ret: null - async: false - - n: greet - ln: 5 - cx: 1 - args: - - self - dc: [] - ret: null - async: false - dc: [] - ds: [] - cv: - - n: greeter - ln: 9 - const: false - val: Greeter('world') - cl_attr: [] - stat: - async: 0 - th: 0 - io: {} - err: - total: 0 - generic: 0 - sm: CLS:1;FN:3;AVG_CX:1.0 -deps: {} -sum: - mod_count: 1 - cl_count: 1 - fn_count: 3 - file_count: 1 - total_ccn: 3 - tech_stack: [] - config_files: [] - has_async: false - uses_type_hints: false diff --git a/python_test_script.yaml b/python_test_script.yaml deleted file mode 100644 index 1ec837c..0000000 --- a/python_test_script.yaml +++ /dev/null @@ -1,71 +0,0 @@ -root: /app/examples/python_test -type: python -files: -- main.py -modules: - main: - f: - - main.py - im: [] - fim: !!python/object/apply:collections.defaultdict - - !!python/name:builtins.list '' - cl: - - n: Greeter - ln: 1 - bs: [] - dc: [] - attrs: [] - fn: - - n: main - ln: 8 - cx: 1 - args: [] - dc: [] - ret: null - async: false - md: - Greeter: - - n: __init__ - ln: 2 - cx: 1 - args: - - self - - name - dc: [] - ret: null - async: false - - n: greet - ln: 5 - cx: 1 - args: - - self - dc: [] - ret: null - async: false - dc: [] - ds: [] - cv: - - n: greeter - ln: 9 - const: false - val: Greeter('world') - cl_attr: [] - stat: - async: 0 - th: 0 - io: {} - err: - total: 0 - generic: 0 - sm: CLS:1;FN:3;AVG_CX:1.0 -deps: {} -sum: - mod_count: 1 - cl_count: 1 - fn_count: 3 - file_count: 1 - total_ccn: 3 - config_files: [] - tech_stack: [] - has_async: false - uses_type_hints: false diff --git a/quickstart.md b/quickstart.md new file mode 100644 index 0000000..78adedd --- /dev/null +++ b/quickstart.md @@ -0,0 +1,52 @@ +# CodeSage Quickstart + +This guide provides a brief overview of the `codesage` command-line tool and its most common commands. + +## Installation + +To install `codesage`, you will need Python 3.8+ and Poetry. Once you have these prerequisites, you can install the tool with the following command: + +```bash +poetry install +``` + +## Usage + +All `codesage` commands are run through the `poetry run` command to ensure that the correct environment is used. + +### Snapshot + +The `snapshot` command is used to create and manage snapshots of your codebase. + +#### `snapshot create` + +The `snapshot create` command generates a semantic snapshot of your project. This snapshot can be used for a variety of purposes, including code analysis, documentation generation, and more. + +```bash +poetry run codesage snapshot create [OPTIONS] +``` + +**Arguments:** + +* ``: The path to the project you want to create a snapshot of. + +**Options:** + +* `--project `: The name of the project. If not provided, the name of the directory will be used. +* `--format [yaml|json|md]`: The format of the snapshot. The default is `yaml`. +* `--output `: The path to the output file. If not provided, the snapshot will be named `_snapshot.` and saved in the current directory. +* `--language [python|go|shell|java|auto]`: The language of the project. If `auto` is selected, the tool will attempt to detect the language automatically. The default is `auto`. + +**Examples:** + +* Create a YAML snapshot of a Python project: + + ```bash + poetry run codesage snapshot create --language python --format yaml ./my-python-project + ``` + +* Create a JSON snapshot of a Go project and save it to a specific file: + + ```bash + poetry run codesage snapshot create --language go --format json --output ./snapshots/my-go-project.json ./my-go-project + ``` diff --git a/semantic-snapshot/py-semantic-snapshot-v3.py b/semantic-snapshot/py-semantic-snapshot-v3.py index b804861..041e744 100644 --- a/semantic-snapshot/py-semantic-snapshot-v3.py +++ b/semantic-snapshot/py-semantic-snapshot-v3.py @@ -700,29 +700,17 @@ def generate_semantic_digest(repo_path, output_path, args): "uses_type_hints": False, } - # Convert defaultdict to dict for clean YAML output - def convert_defaultdict_to_dict(d): - if isinstance(d, defaultdict): - d = {k: convert_defaultdict_to_dict(v) for k, v in d.items()} - elif isinstance(d, dict): - return {k: convert_defaultdict_to_dict(v) for k, v in d.items()} - elif isinstance(d, list): - return [convert_defaultdict_to_dict(i) for i in d] - return d - - final_digest = convert_defaultdict_to_dict(digest) - output_path = ensure_unicode(output_path) try: with open(output_path, "w", encoding="utf-8") as f: - yaml.dump( - final_digest, - f, + yaml_content = yaml.dump( + digest, allow_unicode=True, default_flow_style=False, sort_keys=False, width=120, ) + f.write(yaml_content) print("✅ Semantic project digest generated: {}".format(output_path)) print( diff --git a/tests/cli/test_snapshot.py b/tests/cli/test_snapshot.py index 7f505f0..b7296f8 100644 --- a/tests/cli/test_snapshot.py +++ b/tests/cli/test_snapshot.py @@ -92,8 +92,10 @@ def run_standalone_script(script_path: Path, project_dir: Path) -> dict: raise FileNotFoundError(f"Script {script_abs.name} did not generate {output_filename}") with open(output_filepath, "r", encoding="utf-8") as f: - # Use FullLoader to handle Python-specific tags like defaultdict - data = yaml.load(f, Loader=yaml.FullLoader) + if "py-semantic" in script_path.name: + data = yaml.unsafe_load(f) + else: + data = yaml.safe_load(f) # Clean up the generated file output_filepath.unlink() @@ -101,7 +103,8 @@ def run_standalone_script(script_path: Path, project_dir: Path) -> dict: return data -def test_python_snapshot_consistency(runner: CliRunner, setup_projects): +@pytest.mark.parametrize("output_format", ["yaml", "json"]) +def test_python_snapshot_consistency(runner: CliRunner, setup_projects, output_format): """Verify codesage 'py' format matches the original Python script.""" base_dir, py_project_dir, _ = setup_projects script_path = Path("semantic-snapshot/py-semantic-snapshot-v3.py") @@ -110,13 +113,13 @@ def test_python_snapshot_consistency(runner: CliRunner, setup_projects): expected_output = run_standalone_script(script_path, py_project_dir) # 2. Run codesage - output_file = base_dir / "codesage_py_output.yml" + output_file = base_dir / f"codesage_py_output.{output_format}" result = runner.invoke( main, [ "snapshot", "create", str(py_project_dir), - "--format", "python-semantic-digest", + "--format", output_format, "--language", "python", "--output", str(output_file) ], @@ -124,16 +127,19 @@ def test_python_snapshot_consistency(runner: CliRunner, setup_projects): ) assert result.exit_code == 0 assert output_file.exists() - codesage_output = load_yaml(output_file) + + if output_format == "yaml": + codesage_output = load_yaml(output_file) + else: + with open(output_file, "r", encoding="utf-8") as f: + codesage_output = json.load(f) # 3. Compare outputs - assert codesage_output["root"] == expected_output["root"] - assert "main" in codesage_output["modules"] - assert len(codesage_output["modules"]["main"]["cl"]) > 0 - assert codesage_output["modules"]["main"]["cl"][0]["n"] == "MyClass" + assert codesage_output == expected_output -def test_go_snapshot_consistency(runner: CliRunner, setup_projects): +@pytest.mark.parametrize("output_format", ["yaml", "json"]) +def test_go_snapshot_consistency(runner: CliRunner, setup_projects, output_format): """Verify codesage 'go' format matches the original Go script.""" base_dir, _, go_project_dir = setup_projects script_path = Path("semantic-snapshot/go-semantic-snapshot-v4.py") @@ -142,13 +148,13 @@ def test_go_snapshot_consistency(runner: CliRunner, setup_projects): expected_output = run_standalone_script(script_path, go_project_dir) # 2. Run codesage - output_file = base_dir / "codesage_go_output.yml" + output_file = base_dir / f"codesage_go_output.{output_format}" result = runner.invoke( main, [ "snapshot", "create", str(go_project_dir), - "--format", "go-semantic-digest", + "--format", output_format, "--language", "go", "--output", str(output_file) ], @@ -156,7 +162,12 @@ def test_go_snapshot_consistency(runner: CliRunner, setup_projects): ) assert result.exit_code == 0 assert output_file.exists() - codesage_output = load_yaml(output_file) + + if output_format == "yaml": + codesage_output = load_yaml(output_file) + else: + with open(output_file, "r", encoding="utf-8") as f: + codesage_output = json.load(f) # 3. Compare outputs # Normalize for comparison - e.g., rounding floats if necessary @@ -187,24 +198,26 @@ def test_go_snapshot_consistency(runner: CliRunner, setup_projects): assert main_pkg_contents["md"]["Greeter"][0]["n"] == "Greet", "Greet method not found" -def test_default_snapshot_creation(runner: CliRunner, setup_projects, tmp_path): - """Test default snapshot creation creates a .json file.""" - _, py_project_dir, _ = setup_projects +def test_original_snapshot_creation_for_other_languages(runner: CliRunner, tmp_path): + """Test that the original snapshot mechanism is used for non-Python/Go languages.""" + java_project_dir = tmp_path / "java_project" + java_project_dir.mkdir() + (java_project_dir / "Main.java").write_text("class Main {}") - # Change to a temporary directory to isolate .codesage folder os.chdir(tmp_path) result = runner.invoke( - main, ["snapshot", "create", str(py_project_dir)], catch_exceptions=False + main, + ["snapshot", "create", str(java_project_dir), "--language", "java"], + catch_exceptions=False, ) assert result.exit_code == 0 - snapshot_dir = tmp_path / ".codesage" / "snapshots" / py_project_dir.name + snapshot_dir = tmp_path / ".codesage" / "snapshots" / java_project_dir.name assert snapshot_dir.exists() - # Check for the presence of at least one JSON snapshot file - json_files = list(snapshot_dir.glob("*.json")) - assert len(json_files) > 0, "No JSON snapshot file was created" + json_files = list(snapshot_dir.glob("v*.json")) + assert len(json_files) > 0, "No versioned JSON snapshot file was created" def test_snapshot_show_and_cleanup(runner: CliRunner, setup_projects, tmp_path): @@ -214,28 +227,38 @@ def test_snapshot_show_and_cleanup(runner: CliRunner, setup_projects, tmp_path): os.chdir(tmp_path) - # 1. Create a few snapshots + # 1. Create a few snapshots using the original mechanism for _ in range(3): - res = runner.invoke(main, ["snapshot", "create", str(py_project_dir), "--project", project_name], catch_exceptions=False) + res = runner.invoke( + main, + ["snapshot", "create", str(py_project_dir), "--project", project_name, "--language", "shell"], # Use a non-py/go language + catch_exceptions=False, + ) assert res.exit_code == 0 snapshot_dir = tmp_path / ".codesage" / "snapshots" / project_name - # Filter out symlinks and the index file snapshots = [p for p in snapshot_dir.glob("v*.json")] assert len(snapshots) == 3 # 2. Test 'snapshot show' - result = runner.invoke(main, ["snapshot", "show", "--project", project_name, snapshots[0].stem], catch_exceptions=False) + result = runner.invoke( + main, + ["snapshot", "show", "--project", project_name, snapshots[0].stem], + catch_exceptions=False, + ) assert result.exit_code == 0 assert project_name in result.output assert snapshots[0].stem in result.output # 3. Test 'snapshot cleanup' - result = runner.invoke(main, ["snapshot", "cleanup", "--project", project_name, "--keep", "1"], catch_exceptions=False) + result = runner.invoke( + main, + ["snapshot", "cleanup", "--project", project_name, "--keep", "1"], + catch_exceptions=False, + ) assert result.exit_code == 0 remaining_snapshots = [p for p in snapshot_dir.glob("v*.json")] assert len(remaining_snapshots) == 1 - # Restore original working directory if needed by other tests os.chdir(Path.cwd())