From 39ecc07f8a654763c7aa710d14b8154c876e8819 Mon Sep 17 00:00:00 2001 From: "omer.roth" Date: Wed, 11 Mar 2026 14:40:53 +0200 Subject: [PATCH 1/3] CM-60869 added cleanup mechanism after sca file restore --- .../sca/base_restore_dependencies.py | 10 ++++++++ .../sca/maven/restore_maven_dependencies.py | 25 +++++++++++-------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/cycode/cli/files_collector/sca/base_restore_dependencies.py b/cycode/cli/files_collector/sca/base_restore_dependencies.py index ac391727..06431f72 100644 --- a/cycode/cli/files_collector/sca/base_restore_dependencies.py +++ b/cycode/cli/files_collector/sca/base_restore_dependencies.py @@ -92,7 +92,9 @@ def try_restore_dependencies(self, document: Document) -> Optional[Document]: ) if output is None: # one of the commands failed return None + file_was_generated = True else: + file_was_generated = False logger.debug( 'Lock file already exists, skipping restore commands, %s', {'restore_file_path': restore_file_path}, @@ -107,6 +109,14 @@ def try_restore_dependencies(self, document: Document) -> Optional[Document]: 'content_empty': not restore_file_content, }, ) + + if file_was_generated: + try: + Path(restore_file_path).unlink(missing_ok=True) + logger.debug('Cleaned up generated restore file, %s', {'restore_file_path': restore_file_path}) + except Exception as e: + logger.debug('Failed to clean up generated restore file', exc_info=e) + return Document(relative_restore_file_path, restore_file_content, self.is_git_diff) def get_manifest_dir(self, document: Document) -> Optional[str]: diff --git a/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py b/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py index 34499bdf..d461cc0e 100644 --- a/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py +++ b/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py @@ -1,4 +1,5 @@ from os import path +from pathlib import Path from typing import Optional import typer @@ -10,6 +11,9 @@ ) from cycode.cli.models import Document from cycode.cli.utils.path_utils import get_file_content, get_file_dir, join_paths +from cycode.logger import get_logger + +logger = get_logger('Maven Restore Dependencies') BUILD_MAVEN_FILE_NAME = 'pom.xml' MAVEN_CYCLONE_DEP_TREE_FILE_NAME = 'bom.json' @@ -42,15 +46,8 @@ def try_restore_dependencies(self, document: Document) -> Optional[Document]: if document.content is None: return self.restore_from_secondary_command(document, manifest_file_path) - restore_dependencies_document = super().try_restore_dependencies(document) - if restore_dependencies_document is None: - return None - - restore_dependencies_document.content = get_file_content( - join_paths(get_file_dir(manifest_file_path), self.get_lock_file_name()) - ) - - return restore_dependencies_document + # super() reads the content and cleans up any generated file; no re-read needed + return super().try_restore_dependencies(document) def restore_from_secondary_command(self, document: Document, manifest_file_path: str) -> Optional[Document]: restore_content = execute_commands( @@ -62,11 +59,17 @@ def restore_from_secondary_command(self, document: Document, manifest_file_path: return None restore_file_path = build_dep_tree_path(document.absolute_path, MAVEN_DEP_TREE_FILE_NAME) + content = get_file_content(restore_file_path) + + try: + Path(restore_file_path).unlink(missing_ok=True) + except Exception as e: + logger.debug('Failed to clean up generated maven dep tree file', exc_info=e) + return Document( path=build_dep_tree_path(document.path, MAVEN_DEP_TREE_FILE_NAME), - content=get_file_content(restore_file_path), + content=content, is_git_diff_format=self.is_git_diff, - absolute_path=restore_file_path, ) def create_secondary_restore_commands(self, manifest_file_path: str) -> list[list[str]]: From 6f2e1e361b056c1e09050f8e0d11be0bddce0904 Mon Sep 17 00:00:00 2001 From: "omer.roth" Date: Wed, 11 Mar 2026 14:41:17 +0200 Subject: [PATCH 2/3] CM-60869 added tests for sca restored file cleanup mechanism --- tests/cli/files_collector/sca/__init__.py | 0 tests/cli/files_collector/sca/go/__init__.py | 0 .../sca/go/test_restore_go_dependencies.py | 87 +++++++++++ .../cli/files_collector/sca/maven/__init__.py | 0 .../maven/test_restore_gradle_dependencies.py | 116 ++++++++++++++ .../maven/test_restore_maven_dependencies.py | 120 ++++++++++++++ .../sca/npm/test_restore_npm_dependencies.py | 38 +++++ .../sca/npm/test_restore_pnpm_dependencies.py | 43 ++++- .../sca/npm/test_restore_yarn_dependencies.py | 43 ++++- .../cli/files_collector/sca/nuget/__init__.py | 0 .../nuget/test_restore_nuget_dependencies.py | 88 +++++++++++ .../php/test_restore_composer_dependencies.py | 49 +++++- .../test_restore_pipenv_dependencies.py | 42 ++++- .../test_restore_poetry_dependencies.py | 49 +++++- .../cli/files_collector/sca/ruby/__init__.py | 0 .../ruby/test_restore_ruby_dependencies.py | 88 +++++++++++ tests/cli/files_collector/sca/sbt/__init__.py | 0 .../sca/sbt/test_restore_sbt_dependencies.py | 88 +++++++++++ .../sca/test_base_restore_dependencies.py | 147 ++++++++++++++++++ 19 files changed, 993 insertions(+), 5 deletions(-) create mode 100644 tests/cli/files_collector/sca/__init__.py create mode 100644 tests/cli/files_collector/sca/go/__init__.py create mode 100644 tests/cli/files_collector/sca/go/test_restore_go_dependencies.py create mode 100644 tests/cli/files_collector/sca/maven/__init__.py create mode 100644 tests/cli/files_collector/sca/maven/test_restore_gradle_dependencies.py create mode 100644 tests/cli/files_collector/sca/maven/test_restore_maven_dependencies.py create mode 100644 tests/cli/files_collector/sca/nuget/__init__.py create mode 100644 tests/cli/files_collector/sca/nuget/test_restore_nuget_dependencies.py create mode 100644 tests/cli/files_collector/sca/ruby/__init__.py create mode 100644 tests/cli/files_collector/sca/ruby/test_restore_ruby_dependencies.py create mode 100644 tests/cli/files_collector/sca/sbt/__init__.py create mode 100644 tests/cli/files_collector/sca/sbt/test_restore_sbt_dependencies.py create mode 100644 tests/cli/files_collector/sca/test_base_restore_dependencies.py diff --git a/tests/cli/files_collector/sca/__init__.py b/tests/cli/files_collector/sca/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/cli/files_collector/sca/go/__init__.py b/tests/cli/files_collector/sca/go/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/cli/files_collector/sca/go/test_restore_go_dependencies.py b/tests/cli/files_collector/sca/go/test_restore_go_dependencies.py new file mode 100644 index 00000000..3a985594 --- /dev/null +++ b/tests/cli/files_collector/sca/go/test_restore_go_dependencies.py @@ -0,0 +1,87 @@ +from pathlib import Path +from typing import Optional +from unittest.mock import MagicMock, patch + +import pytest +import typer + +from cycode.cli.files_collector.sca.go.restore_go_dependencies import ( + GO_RESTORE_FILE_NAME, + RestoreGoDependencies, +) +from cycode.cli.models import Document + +_BASE_MODULE = 'cycode.cli.files_collector.sca.base_restore_dependencies' + + +@pytest.fixture +def mock_ctx(tmp_path: Path) -> typer.Context: + ctx = MagicMock(spec=typer.Context) + ctx.obj = {'monitor': False} + ctx.params = {'path': str(tmp_path)} + return ctx + + +@pytest.fixture +def restore_go(mock_ctx: typer.Context) -> RestoreGoDependencies: + return RestoreGoDependencies(mock_ctx, is_git_diff=False, command_timeout=30) + + +class TestIsProject: + def test_go_mod_matches(self, restore_go: RestoreGoDependencies) -> None: + doc = Document('go.mod', 'module example.com/mymod\ngo 1.21\n') + assert restore_go.is_project(doc) is True + + def test_go_sum_matches(self, restore_go: RestoreGoDependencies) -> None: + doc = Document('go.sum', 'github.com/pkg/errors v0.9.1 h1:...\n') + assert restore_go.is_project(doc) is True + + def test_go_in_subdir_matches(self, restore_go: RestoreGoDependencies) -> None: + doc = Document('myapp/go.mod', 'module example.com/mymod\n') + assert restore_go.is_project(doc) is True + + def test_pom_xml_does_not_match(self, restore_go: RestoreGoDependencies) -> None: + doc = Document('pom.xml', '') + assert restore_go.is_project(doc) is False + + +class TestCleanup: + def test_generated_output_file_is_deleted_after_restore( + self, restore_go: RestoreGoDependencies, tmp_path: Path + ) -> None: + # Go handler requires both go.mod and go.sum to be present + (tmp_path / 'go.mod').write_text('module example.com/test\ngo 1.21\n') + (tmp_path / 'go.sum').write_text('github.com/pkg/errors v0.9.1 h1:abc\n') + doc = Document( + str(tmp_path / 'go.mod'), + 'module example.com/test\ngo 1.21\n', + absolute_path=str(tmp_path / 'go.mod'), + ) + output_path = tmp_path / GO_RESTORE_FILE_NAME + + def side_effect( + commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + ) -> str: + # Go uses create_output_file_manually=True; output_file_path is provided + target = output_file_path or str(output_path) + Path(target).write_text('example.com/test github.com/pkg/errors@v0.9.1\n') + return 'graph output' + + with patch(f'{_BASE_MODULE}.execute_commands', side_effect=side_effect): + result = restore_go.try_restore_dependencies(doc) + + assert result is not None + assert not output_path.exists(), f'{GO_RESTORE_FILE_NAME} must be deleted after restore' + + def test_missing_go_sum_returns_none(self, restore_go: RestoreGoDependencies, tmp_path: Path) -> None: + (tmp_path / 'go.mod').write_text('module example.com/test\ngo 1.21\n') + # go.sum intentionally absent + doc = Document( + str(tmp_path / 'go.mod'), + 'module example.com/test\ngo 1.21\n', + absolute_path=str(tmp_path / 'go.mod'), + ) + + result = restore_go.try_restore_dependencies(doc) + + assert result is None diff --git a/tests/cli/files_collector/sca/maven/__init__.py b/tests/cli/files_collector/sca/maven/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/cli/files_collector/sca/maven/test_restore_gradle_dependencies.py b/tests/cli/files_collector/sca/maven/test_restore_gradle_dependencies.py new file mode 100644 index 00000000..1a499b2c --- /dev/null +++ b/tests/cli/files_collector/sca/maven/test_restore_gradle_dependencies.py @@ -0,0 +1,116 @@ +from pathlib import Path +from typing import Optional +from unittest.mock import MagicMock, patch + +import pytest +import typer + +from cycode.cli.files_collector.sca.maven.restore_gradle_dependencies import ( + BUILD_GRADLE_DEP_TREE_FILE_NAME, + BUILD_GRADLE_FILE_NAME, + BUILD_GRADLE_KTS_FILE_NAME, + RestoreGradleDependencies, +) +from cycode.cli.models import Document + +_BASE_MODULE = 'cycode.cli.files_collector.sca.base_restore_dependencies' + + +@pytest.fixture +def mock_ctx(tmp_path: Path) -> typer.Context: + ctx = MagicMock(spec=typer.Context) + ctx.obj = {'monitor': False, 'gradle_all_sub_projects': False} + ctx.params = {'path': str(tmp_path)} + return ctx + + +@pytest.fixture +def restore_gradle(mock_ctx: typer.Context) -> RestoreGradleDependencies: + return RestoreGradleDependencies(mock_ctx, is_git_diff=False, command_timeout=30) + + +class TestIsProject: + def test_build_gradle_matches(self, restore_gradle: RestoreGradleDependencies) -> None: + doc = Document('build.gradle', 'apply plugin: "java"\n') + assert restore_gradle.is_project(doc) is True + + def test_build_gradle_kts_matches(self, restore_gradle: RestoreGradleDependencies) -> None: + doc = Document('build.gradle.kts', 'plugins { java }\n') + assert restore_gradle.is_project(doc) is True + + def test_pom_xml_does_not_match(self, restore_gradle: RestoreGradleDependencies) -> None: + doc = Document('pom.xml', '') + assert restore_gradle.is_project(doc) is False + + def test_settings_gradle_does_not_match(self, restore_gradle: RestoreGradleDependencies) -> None: + doc = Document('settings.gradle', 'rootProject.name = "test"') + assert restore_gradle.is_project(doc) is False + + +class TestCleanup: + def test_generated_dep_tree_file_is_deleted_after_restore( + self, restore_gradle: RestoreGradleDependencies, tmp_path: Path + ) -> None: + (tmp_path / BUILD_GRADLE_FILE_NAME).write_text('apply plugin: "java"\n') + doc = Document( + str(tmp_path / BUILD_GRADLE_FILE_NAME), + 'apply plugin: "java"\n', + absolute_path=str(tmp_path / BUILD_GRADLE_FILE_NAME), + ) + output_path = tmp_path / BUILD_GRADLE_DEP_TREE_FILE_NAME + + def side_effect( + commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + ) -> str: + # Gradle uses create_output_file_manually=True; output_file_path is provided + target = output_file_path or str(output_path) + Path(target).write_text('compileClasspath - Compile classpath:\n\\--- org.example:lib:1.0\n') + return 'dep tree output' + + with patch(f'{_BASE_MODULE}.execute_commands', side_effect=side_effect): + result = restore_gradle.try_restore_dependencies(doc) + + assert result is not None + assert not output_path.exists(), f'{BUILD_GRADLE_DEP_TREE_FILE_NAME} must be deleted after restore' + + def test_preexisting_dep_tree_file_is_not_deleted( + self, restore_gradle: RestoreGradleDependencies, tmp_path: Path + ) -> None: + dep_tree_content = 'compileClasspath - Compile classpath:\n\\--- org.example:lib:1.0\n' + (tmp_path / BUILD_GRADLE_FILE_NAME).write_text('apply plugin: "java"\n') + output_path = tmp_path / BUILD_GRADLE_DEP_TREE_FILE_NAME + output_path.write_text(dep_tree_content) + doc = Document( + str(tmp_path / BUILD_GRADLE_FILE_NAME), + 'apply plugin: "java"\n', + absolute_path=str(tmp_path / BUILD_GRADLE_FILE_NAME), + ) + + result = restore_gradle.try_restore_dependencies(doc) + + assert result is not None + assert output_path.exists(), f'Pre-existing {BUILD_GRADLE_DEP_TREE_FILE_NAME} must not be deleted' + + def test_kts_build_file_also_cleaned_up( + self, restore_gradle: RestoreGradleDependencies, tmp_path: Path + ) -> None: + (tmp_path / BUILD_GRADLE_KTS_FILE_NAME).write_text('plugins { java }\n') + doc = Document( + str(tmp_path / BUILD_GRADLE_KTS_FILE_NAME), + 'plugins { java }\n', + absolute_path=str(tmp_path / BUILD_GRADLE_KTS_FILE_NAME), + ) + output_path = tmp_path / BUILD_GRADLE_DEP_TREE_FILE_NAME + + def side_effect( + commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + ) -> str: + target = output_file_path or str(output_path) + Path(target).write_text('compileClasspath\n') + return 'output' + + with patch(f'{_BASE_MODULE}.execute_commands', side_effect=side_effect): + result = restore_gradle.try_restore_dependencies(doc) + + assert result is not None + assert not output_path.exists(), f'{BUILD_GRADLE_DEP_TREE_FILE_NAME} must be deleted after restore' diff --git a/tests/cli/files_collector/sca/maven/test_restore_maven_dependencies.py b/tests/cli/files_collector/sca/maven/test_restore_maven_dependencies.py new file mode 100644 index 00000000..5bd3baa4 --- /dev/null +++ b/tests/cli/files_collector/sca/maven/test_restore_maven_dependencies.py @@ -0,0 +1,120 @@ +from pathlib import Path +from typing import Optional +from unittest.mock import MagicMock, patch + +import pytest +import typer + +from cycode.cli.files_collector.sca.maven.restore_maven_dependencies import ( + BUILD_MAVEN_FILE_NAME, + MAVEN_CYCLONE_DEP_TREE_FILE_NAME, + MAVEN_DEP_TREE_FILE_NAME, + RestoreMavenDependencies, +) +from cycode.cli.models import Document + +_BASE_MODULE = 'cycode.cli.files_collector.sca.base_restore_dependencies' +_MAVEN_MODULE = 'cycode.cli.files_collector.sca.maven.restore_maven_dependencies' + + +@pytest.fixture +def mock_ctx(tmp_path: Path) -> typer.Context: + ctx = MagicMock(spec=typer.Context) + ctx.obj = {'monitor': False, 'maven_settings_file': None} + ctx.params = {'path': str(tmp_path)} + return ctx + + +@pytest.fixture +def restore_maven(mock_ctx: typer.Context) -> RestoreMavenDependencies: + return RestoreMavenDependencies(mock_ctx, is_git_diff=False, command_timeout=30) + + +class TestIsProject: + def test_pom_xml_matches(self, restore_maven: RestoreMavenDependencies) -> None: + doc = Document('pom.xml', '') + assert restore_maven.is_project(doc) is True + + def test_pom_xml_in_subdir_matches(self, restore_maven: RestoreMavenDependencies) -> None: + doc = Document('mymodule/pom.xml', '') + assert restore_maven.is_project(doc) is True + + def test_build_gradle_does_not_match(self, restore_maven: RestoreMavenDependencies) -> None: + doc = Document('build.gradle', '') + assert restore_maven.is_project(doc) is False + + +class TestCleanup: + def test_generated_bom_is_deleted_after_primary_restore( + self, restore_maven: RestoreMavenDependencies, tmp_path: Path + ) -> None: + """Primary path: super().try_restore_dependencies() generates target/bom.json and cleans it up.""" + pom_content = '4.0.0' + (tmp_path / BUILD_MAVEN_FILE_NAME).write_text(pom_content) + target_dir = tmp_path / 'target' + target_dir.mkdir() + bom_path = target_dir / MAVEN_CYCLONE_DEP_TREE_FILE_NAME + doc = Document( + str(tmp_path / BUILD_MAVEN_FILE_NAME), + pom_content, + absolute_path=str(tmp_path / BUILD_MAVEN_FILE_NAME), + ) + + def side_effect( + commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + ) -> str: + bom_path.write_text('{"bomFormat": "CycloneDX", "components": []}') + return 'output' + + with patch(f'{_BASE_MODULE}.execute_commands', side_effect=side_effect): + result = restore_maven.try_restore_dependencies(doc) + + assert result is not None + assert result.content is not None, 'Document content must be populated even after file deletion' + assert not bom_path.exists(), f'target/{MAVEN_CYCLONE_DEP_TREE_FILE_NAME} must be deleted after restore' + + def test_generated_dep_tree_is_deleted_after_secondary_restore( + self, restore_maven: RestoreMavenDependencies, tmp_path: Path + ) -> None: + """Secondary path (content=None): mvn dependency:tree generates bcde.mvndeps and it must be cleaned up.""" + (tmp_path / BUILD_MAVEN_FILE_NAME).write_text('') + dep_tree_path = tmp_path / MAVEN_DEP_TREE_FILE_NAME + # content=None triggers the secondary command path + doc = Document( + str(tmp_path / BUILD_MAVEN_FILE_NAME), + None, + absolute_path=str(tmp_path / BUILD_MAVEN_FILE_NAME), + ) + + def side_effect( + commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + ) -> str: + dep_tree_path.write_text('[INFO] com.example:my-app:jar:1.0.0\n') + return '[INFO] BUILD SUCCESS' + + with patch(f'{_MAVEN_MODULE}.execute_commands', side_effect=side_effect): + result = restore_maven.try_restore_dependencies(doc) + + assert result is not None + assert result.content is not None + assert not dep_tree_path.exists(), f'{MAVEN_DEP_TREE_FILE_NAME} must be deleted after restore' + + def test_preexisting_bom_is_not_deleted( + self, restore_maven: RestoreMavenDependencies, tmp_path: Path + ) -> None: + pom_content = '4.0.0' + (tmp_path / BUILD_MAVEN_FILE_NAME).write_text(pom_content) + target_dir = tmp_path / 'target' + target_dir.mkdir() + bom_path = target_dir / MAVEN_CYCLONE_DEP_TREE_FILE_NAME + bom_path.write_text('{"bomFormat": "CycloneDX", "components": [{"name": "requests"}]}') + doc = Document( + str(tmp_path / BUILD_MAVEN_FILE_NAME), + pom_content, + absolute_path=str(tmp_path / BUILD_MAVEN_FILE_NAME), + ) + + result = restore_maven.try_restore_dependencies(doc) + + assert result is not None + assert bom_path.exists(), f'Pre-existing target/{MAVEN_CYCLONE_DEP_TREE_FILE_NAME} must not be deleted' diff --git a/tests/cli/files_collector/sca/npm/test_restore_npm_dependencies.py b/tests/cli/files_collector/sca/npm/test_restore_npm_dependencies.py index aa145de3..d90fe879 100644 --- a/tests/cli/files_collector/sca/npm/test_restore_npm_dependencies.py +++ b/tests/cli/files_collector/sca/npm/test_restore_npm_dependencies.py @@ -1,4 +1,5 @@ from pathlib import Path +from typing import Optional from unittest.mock import MagicMock, patch import pytest @@ -99,6 +100,43 @@ def test_get_lock_file_names_contains_only_npm_lock(self, restore_npm: RestoreNp assert restore_npm.get_lock_file_names() == [NPM_LOCK_FILE_NAME] +_BASE_MODULE = 'cycode.cli.files_collector.sca.base_restore_dependencies' + + +class TestCleanup: + def test_generated_lockfile_is_deleted_after_restore( + self, restore_npm: RestoreNpmDependencies, tmp_path: Path + ) -> None: + (tmp_path / 'package.json').write_text('{"name": "test"}') + doc = Document(str(tmp_path / 'package.json'), '{"name": "test"}', absolute_path=str(tmp_path / 'package.json')) + lock_path = tmp_path / NPM_LOCK_FILE_NAME + + def side_effect( + commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + ) -> str: + lock_path.write_text('{"lockfileVersion": 3}') + return 'output' + + with patch(f'{_BASE_MODULE}.execute_commands', side_effect=side_effect): + result = restore_npm.try_restore_dependencies(doc) + + assert result is not None + assert not lock_path.exists(), f'{NPM_LOCK_FILE_NAME} must be deleted after restore' + + def test_preexisting_lockfile_is_not_deleted( + self, restore_npm: RestoreNpmDependencies, tmp_path: Path + ) -> None: + (tmp_path / 'package.json').write_text('{"name": "test"}') + lock_path = tmp_path / NPM_LOCK_FILE_NAME + lock_path.write_text('{"lockfileVersion": 3, "packages": {}}') + doc = Document(str(tmp_path / 'package.json'), '{"name": "test"}', absolute_path=str(tmp_path / 'package.json')) + + result = restore_npm.try_restore_dependencies(doc) + + assert result is not None + assert lock_path.exists(), f'Pre-existing {NPM_LOCK_FILE_NAME} must not be deleted' + + class TestPrepareManifestFilePath: def test_strips_package_json_filename(self, restore_npm: RestoreNpmDependencies) -> None: path = str(Path('/path/to/package.json')) diff --git a/tests/cli/files_collector/sca/npm/test_restore_pnpm_dependencies.py b/tests/cli/files_collector/sca/npm/test_restore_pnpm_dependencies.py index 312cce83..f0612731 100644 --- a/tests/cli/files_collector/sca/npm/test_restore_pnpm_dependencies.py +++ b/tests/cli/files_collector/sca/npm/test_restore_pnpm_dependencies.py @@ -1,5 +1,6 @@ from pathlib import Path -from unittest.mock import MagicMock +from typing import Optional +from unittest.mock import MagicMock, patch import pytest import typer @@ -89,3 +90,43 @@ def test_get_lock_file_name(self, restore_pnpm: RestorePnpmDependencies) -> None def test_get_commands_returns_pnpm_install(self, restore_pnpm: RestorePnpmDependencies) -> None: commands = restore_pnpm.get_commands('/path/to/package.json') assert commands == [['pnpm', 'install', '--ignore-scripts']] + + +_BASE_MODULE = 'cycode.cli.files_collector.sca.base_restore_dependencies' + + +class TestCleanup: + def test_generated_lockfile_is_deleted_after_restore( + self, restore_pnpm: RestorePnpmDependencies, tmp_path: Path + ) -> None: + # pnpm: no pre-existing pnpm-lock.yaml but package.json indicates pnpm + content = '{"name": "test", "packageManager": "pnpm@8.6.2"}' + (tmp_path / 'package.json').write_text(content) + doc = Document(str(tmp_path / 'package.json'), content, absolute_path=str(tmp_path / 'package.json')) + lock_path = tmp_path / PNPM_LOCK_FILE_NAME + + def side_effect( + commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + ) -> str: + lock_path.write_text('lockfileVersion: 5.4\n') + return 'output' + + with patch(f'{_BASE_MODULE}.execute_commands', side_effect=side_effect): + result = restore_pnpm.try_restore_dependencies(doc) + + assert result is not None + assert not lock_path.exists(), f'{PNPM_LOCK_FILE_NAME} must be deleted after restore' + + def test_preexisting_lockfile_is_not_deleted( + self, restore_pnpm: RestorePnpmDependencies, tmp_path: Path + ) -> None: + lock_content = 'lockfileVersion: 5.4\n\npackages:\n /pkg@1.0.0:\n resolution: {}\n' + (tmp_path / 'package.json').write_text('{"name": "test"}') + lock_path = tmp_path / PNPM_LOCK_FILE_NAME + lock_path.write_text(lock_content) + doc = Document(str(tmp_path / 'package.json'), '{"name": "test"}', absolute_path=str(tmp_path / 'package.json')) + + result = restore_pnpm.try_restore_dependencies(doc) + + assert result is not None + assert lock_path.exists(), f'Pre-existing {PNPM_LOCK_FILE_NAME} must not be deleted' diff --git a/tests/cli/files_collector/sca/npm/test_restore_yarn_dependencies.py b/tests/cli/files_collector/sca/npm/test_restore_yarn_dependencies.py index 13e321c9..b292c72a 100644 --- a/tests/cli/files_collector/sca/npm/test_restore_yarn_dependencies.py +++ b/tests/cli/files_collector/sca/npm/test_restore_yarn_dependencies.py @@ -1,5 +1,6 @@ from pathlib import Path -from unittest.mock import MagicMock +from typing import Optional +from unittest.mock import MagicMock, patch import pytest import typer @@ -89,3 +90,43 @@ def test_get_lock_file_name(self, restore_yarn: RestoreYarnDependencies) -> None def test_get_commands_returns_yarn_install(self, restore_yarn: RestoreYarnDependencies) -> None: commands = restore_yarn.get_commands('/path/to/package.json') assert commands == [['yarn', 'install', '--ignore-scripts']] + + +_BASE_MODULE = 'cycode.cli.files_collector.sca.base_restore_dependencies' + + +class TestCleanup: + def test_generated_lockfile_is_deleted_after_restore( + self, restore_yarn: RestoreYarnDependencies, tmp_path: Path + ) -> None: + # Yarn: no pre-existing yarn.lock but package.json indicates yarn + content = '{"name": "test", "packageManager": "yarn@4.0.2"}' + (tmp_path / 'package.json').write_text(content) + doc = Document(str(tmp_path / 'package.json'), content, absolute_path=str(tmp_path / 'package.json')) + lock_path = tmp_path / YARN_LOCK_FILE_NAME + + def side_effect( + commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + ) -> str: + lock_path.write_text('# yarn lockfile v1\n') + return 'output' + + with patch(f'{_BASE_MODULE}.execute_commands', side_effect=side_effect): + result = restore_yarn.try_restore_dependencies(doc) + + assert result is not None + assert not lock_path.exists(), f'{YARN_LOCK_FILE_NAME} must be deleted after restore' + + def test_preexisting_lockfile_is_not_deleted( + self, restore_yarn: RestoreYarnDependencies, tmp_path: Path + ) -> None: + lock_content = '# yarn lockfile v1\n\npackage@1.0.0:\n resolved "https://example.com"\n' + (tmp_path / 'package.json').write_text('{"name": "test"}') + lock_path = tmp_path / YARN_LOCK_FILE_NAME + lock_path.write_text(lock_content) + doc = Document(str(tmp_path / 'package.json'), '{"name": "test"}', absolute_path=str(tmp_path / 'package.json')) + + result = restore_yarn.try_restore_dependencies(doc) + + assert result is not None + assert lock_path.exists(), f'Pre-existing {YARN_LOCK_FILE_NAME} must not be deleted' diff --git a/tests/cli/files_collector/sca/nuget/__init__.py b/tests/cli/files_collector/sca/nuget/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/cli/files_collector/sca/nuget/test_restore_nuget_dependencies.py b/tests/cli/files_collector/sca/nuget/test_restore_nuget_dependencies.py new file mode 100644 index 00000000..f3d60daf --- /dev/null +++ b/tests/cli/files_collector/sca/nuget/test_restore_nuget_dependencies.py @@ -0,0 +1,88 @@ +from pathlib import Path +from typing import Optional +from unittest.mock import MagicMock, patch + +import pytest +import typer + +from cycode.cli.files_collector.sca.nuget.restore_nuget_dependencies import ( + NUGET_LOCK_FILE_NAME, + RestoreNugetDependencies, +) +from cycode.cli.models import Document + +_BASE_MODULE = 'cycode.cli.files_collector.sca.base_restore_dependencies' + + +@pytest.fixture +def mock_ctx(tmp_path: Path) -> typer.Context: + ctx = MagicMock(spec=typer.Context) + ctx.obj = {'monitor': False} + ctx.params = {'path': str(tmp_path)} + return ctx + + +@pytest.fixture +def restore_nuget(mock_ctx: typer.Context) -> RestoreNugetDependencies: + return RestoreNugetDependencies(mock_ctx, is_git_diff=False, command_timeout=30) + + +class TestIsProject: + def test_csproj_matches(self, restore_nuget: RestoreNugetDependencies) -> None: + doc = Document('MyProject.csproj', '') + assert restore_nuget.is_project(doc) is True + + def test_vbproj_matches(self, restore_nuget: RestoreNugetDependencies) -> None: + doc = Document('MyProject.vbproj', '') + assert restore_nuget.is_project(doc) is True + + def test_sln_does_not_match(self, restore_nuget: RestoreNugetDependencies) -> None: + doc = Document('MySolution.sln', '') + assert restore_nuget.is_project(doc) is False + + def test_packages_json_does_not_match(self, restore_nuget: RestoreNugetDependencies) -> None: + doc = Document('packages.json', '{}') + assert restore_nuget.is_project(doc) is False + + +class TestCleanup: + def test_generated_lockfile_is_deleted_after_restore( + self, restore_nuget: RestoreNugetDependencies, tmp_path: Path + ) -> None: + (tmp_path / 'MyProject.csproj').write_text('') + doc = Document( + str(tmp_path / 'MyProject.csproj'), + '', + absolute_path=str(tmp_path / 'MyProject.csproj'), + ) + lock_path = tmp_path / NUGET_LOCK_FILE_NAME + + def side_effect( + commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + ) -> str: + lock_path.write_text('{"version": 1, "dependencies": {}}') + return 'output' + + with patch(f'{_BASE_MODULE}.execute_commands', side_effect=side_effect): + result = restore_nuget.try_restore_dependencies(doc) + + assert result is not None + assert not lock_path.exists(), f'{NUGET_LOCK_FILE_NAME} must be deleted after restore' + + def test_preexisting_lockfile_is_not_deleted( + self, restore_nuget: RestoreNugetDependencies, tmp_path: Path + ) -> None: + lock_content = '{"version": 1, "dependencies": {"net8.0": {}}}' + (tmp_path / 'MyProject.csproj').write_text('') + lock_path = tmp_path / NUGET_LOCK_FILE_NAME + lock_path.write_text(lock_content) + doc = Document( + str(tmp_path / 'MyProject.csproj'), + '', + absolute_path=str(tmp_path / 'MyProject.csproj'), + ) + + result = restore_nuget.try_restore_dependencies(doc) + + assert result is not None + assert lock_path.exists(), f'Pre-existing {NUGET_LOCK_FILE_NAME} must not be deleted' diff --git a/tests/cli/files_collector/sca/php/test_restore_composer_dependencies.py b/tests/cli/files_collector/sca/php/test_restore_composer_dependencies.py index 463eeddb..3c4558b9 100644 --- a/tests/cli/files_collector/sca/php/test_restore_composer_dependencies.py +++ b/tests/cli/files_collector/sca/php/test_restore_composer_dependencies.py @@ -1,5 +1,6 @@ from pathlib import Path -from unittest.mock import MagicMock +from typing import Optional +from unittest.mock import MagicMock, patch import pytest import typer @@ -68,6 +69,52 @@ def test_existing_composer_lock_returned_directly( def test_get_lock_file_name(self, restore_composer: RestoreComposerDependencies) -> None: assert restore_composer.get_lock_file_name() == COMPOSER_LOCK_FILE_NAME +_BASE_MODULE = 'cycode.cli.files_collector.sca.base_restore_dependencies' + + +class TestCleanup: + def test_generated_lockfile_is_deleted_after_restore( + self, restore_composer: RestoreComposerDependencies, tmp_path: Path + ) -> None: + manifest_content = '{"name": "vendor/project"}\n' + (tmp_path / 'composer.json').write_text(manifest_content) + doc = Document( + str(tmp_path / 'composer.json'), manifest_content, absolute_path=str(tmp_path / 'composer.json') + ) + lock_path = tmp_path / COMPOSER_LOCK_FILE_NAME + + def side_effect( + commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + ) -> str: + lock_path.write_text('{"_readme": [], "packages": []}') + return 'output' + + with patch(f'{_BASE_MODULE}.execute_commands', side_effect=side_effect): + result = restore_composer.try_restore_dependencies(doc) + + assert result is not None + assert not lock_path.exists(), f'{COMPOSER_LOCK_FILE_NAME} must be deleted after restore' + + def test_preexisting_lockfile_is_not_deleted( + self, restore_composer: RestoreComposerDependencies, tmp_path: Path + ) -> None: + lock_content = '{\n "_readme": ["This file is @generated by Composer"],\n "packages": []\n}\n' + (tmp_path / 'composer.json').write_text('{"name": "vendor/project"}\n') + lock_path = tmp_path / COMPOSER_LOCK_FILE_NAME + lock_path.write_text(lock_content) + doc = Document( + str(tmp_path / 'composer.json'), + '{"name": "vendor/project"}\n', + absolute_path=str(tmp_path / 'composer.json'), + ) + + result = restore_composer.try_restore_dependencies(doc) + + assert result is not None + assert lock_path.exists(), f'Pre-existing {COMPOSER_LOCK_FILE_NAME} must not be deleted' + + +class TestGetCommands: def test_get_commands_returns_composer_update(self, restore_composer: RestoreComposerDependencies) -> None: commands = restore_composer.get_commands('/path/to/composer.json') assert commands == [ diff --git a/tests/cli/files_collector/sca/python/test_restore_pipenv_dependencies.py b/tests/cli/files_collector/sca/python/test_restore_pipenv_dependencies.py index 9d34a7e3..baa05398 100644 --- a/tests/cli/files_collector/sca/python/test_restore_pipenv_dependencies.py +++ b/tests/cli/files_collector/sca/python/test_restore_pipenv_dependencies.py @@ -1,5 +1,6 @@ from pathlib import Path -from unittest.mock import MagicMock +from typing import Optional +from unittest.mock import MagicMock, patch import pytest import typer @@ -71,3 +72,42 @@ def test_get_lock_file_name(self, restore_pipenv: RestorePipenvDependencies) -> def test_get_commands_returns_pipenv_lock(self, restore_pipenv: RestorePipenvDependencies) -> None: commands = restore_pipenv.get_commands('/path/to/Pipfile') assert commands == [['pipenv', 'lock']] + + +_BASE_MODULE = 'cycode.cli.files_collector.sca.base_restore_dependencies' + + +class TestCleanup: + def test_generated_lockfile_is_deleted_after_restore( + self, restore_pipenv: RestorePipenvDependencies, tmp_path: Path + ) -> None: + manifest_content = '[[source]]\nname = "pypi"\n' + (tmp_path / 'Pipfile').write_text(manifest_content) + doc = Document(str(tmp_path / 'Pipfile'), manifest_content, absolute_path=str(tmp_path / 'Pipfile')) + lock_path = tmp_path / PIPENV_LOCK_FILE_NAME + + def side_effect( + commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + ) -> str: + lock_path.write_text('{"_meta": {}, "default": {}, "develop": {}}') + return 'output' + + with patch(f'{_BASE_MODULE}.execute_commands', side_effect=side_effect): + result = restore_pipenv.try_restore_dependencies(doc) + + assert result is not None + assert not lock_path.exists(), f'{PIPENV_LOCK_FILE_NAME} must be deleted after restore' + + def test_preexisting_lockfile_is_not_deleted( + self, restore_pipenv: RestorePipenvDependencies, tmp_path: Path + ) -> None: + lock_content = '{"_meta": {"hash": {"sha256": "abc"}}, "default": {}, "develop": {}}\n' + (tmp_path / 'Pipfile').write_text('[[source]]\nname = "pypi"\n') + lock_path = tmp_path / PIPENV_LOCK_FILE_NAME + lock_path.write_text(lock_content) + doc = Document(str(tmp_path / 'Pipfile'), '[[source]]\nname = "pypi"\n', absolute_path=str(tmp_path / 'Pipfile')) + + result = restore_pipenv.try_restore_dependencies(doc) + + assert result is not None + assert lock_path.exists(), f'Pre-existing {PIPENV_LOCK_FILE_NAME} must not be deleted' diff --git a/tests/cli/files_collector/sca/python/test_restore_poetry_dependencies.py b/tests/cli/files_collector/sca/python/test_restore_poetry_dependencies.py index 73f0d14f..55d5080b 100644 --- a/tests/cli/files_collector/sca/python/test_restore_poetry_dependencies.py +++ b/tests/cli/files_collector/sca/python/test_restore_poetry_dependencies.py @@ -1,5 +1,6 @@ from pathlib import Path -from unittest.mock import MagicMock +from typing import Optional +from unittest.mock import MagicMock, patch import pytest import typer @@ -97,3 +98,49 @@ def test_get_lock_file_name(self, restore_poetry: RestorePoetryDependencies) -> def test_get_commands_returns_poetry_lock(self, restore_poetry: RestorePoetryDependencies) -> None: commands = restore_poetry.get_commands('/path/to/pyproject.toml') assert commands == [['poetry', 'lock']] + + +_BASE_MODULE = 'cycode.cli.files_collector.sca.base_restore_dependencies' + + +class TestCleanup: + def test_generated_lockfile_is_deleted_after_restore( + self, restore_poetry: RestorePoetryDependencies, tmp_path: Path + ) -> None: + # Poetry: no pre-existing poetry.lock but pyproject.toml indicates poetry + manifest_content = '[tool.poetry]\nname = "test"\nversion = "1.0.0"\n' + (tmp_path / 'pyproject.toml').write_text(manifest_content) + doc = Document( + str(tmp_path / 'pyproject.toml'), manifest_content, absolute_path=str(tmp_path / 'pyproject.toml') + ) + lock_path = tmp_path / POETRY_LOCK_FILE_NAME + + def side_effect( + commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + ) -> str: + lock_path.write_text('# This file is generated by Poetry\n') + return 'output' + + with patch(f'{_BASE_MODULE}.execute_commands', side_effect=side_effect): + result = restore_poetry.try_restore_dependencies(doc) + + assert result is not None + assert not lock_path.exists(), f'{POETRY_LOCK_FILE_NAME} must be deleted after restore' + + def test_preexisting_lockfile_is_not_deleted( + self, restore_poetry: RestorePoetryDependencies, tmp_path: Path + ) -> None: + lock_content = '# This file is generated by Poetry\n\n[[package]]\nname = "requests"\n' + (tmp_path / 'pyproject.toml').write_text('[tool.poetry]\nname = "test"\n') + lock_path = tmp_path / POETRY_LOCK_FILE_NAME + lock_path.write_text(lock_content) + doc = Document( + str(tmp_path / 'pyproject.toml'), + '[tool.poetry]\nname = "test"\n', + absolute_path=str(tmp_path / 'pyproject.toml'), + ) + + result = restore_poetry.try_restore_dependencies(doc) + + assert result is not None + assert lock_path.exists(), f'Pre-existing {POETRY_LOCK_FILE_NAME} must not be deleted' diff --git a/tests/cli/files_collector/sca/ruby/__init__.py b/tests/cli/files_collector/sca/ruby/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/cli/files_collector/sca/ruby/test_restore_ruby_dependencies.py b/tests/cli/files_collector/sca/ruby/test_restore_ruby_dependencies.py new file mode 100644 index 00000000..35da7871 --- /dev/null +++ b/tests/cli/files_collector/sca/ruby/test_restore_ruby_dependencies.py @@ -0,0 +1,88 @@ +from pathlib import Path +from typing import Optional +from unittest.mock import MagicMock, patch + +import pytest +import typer + +from cycode.cli.files_collector.sca.ruby.restore_ruby_dependencies import ( + RUBY_LOCK_FILE_NAME, + RestoreRubyDependencies, +) +from cycode.cli.models import Document + +_BASE_MODULE = 'cycode.cli.files_collector.sca.base_restore_dependencies' + + +@pytest.fixture +def mock_ctx(tmp_path: Path) -> typer.Context: + ctx = MagicMock(spec=typer.Context) + ctx.obj = {'monitor': False} + ctx.params = {'path': str(tmp_path)} + return ctx + + +@pytest.fixture +def restore_ruby(mock_ctx: typer.Context) -> RestoreRubyDependencies: + return RestoreRubyDependencies(mock_ctx, is_git_diff=False, command_timeout=30) + + +class TestIsProject: + def test_gemfile_matches(self, restore_ruby: RestoreRubyDependencies) -> None: + doc = Document('Gemfile', "source 'https://rubygems.org'\n") + assert restore_ruby.is_project(doc) is True + + def test_gemfile_in_subdir_matches(self, restore_ruby: RestoreRubyDependencies) -> None: + doc = Document('myapp/Gemfile', "source 'https://rubygems.org'\n") + assert restore_ruby.is_project(doc) is True + + def test_gemfile_lock_does_not_match(self, restore_ruby: RestoreRubyDependencies) -> None: + doc = Document('Gemfile.lock', 'GEM\n remote: https://rubygems.org/\n') + assert restore_ruby.is_project(doc) is False + + def test_other_file_does_not_match(self, restore_ruby: RestoreRubyDependencies) -> None: + doc = Document('Rakefile', '') + assert restore_ruby.is_project(doc) is False + + +class TestCleanup: + def test_generated_lockfile_is_deleted_after_restore( + self, restore_ruby: RestoreRubyDependencies, tmp_path: Path + ) -> None: + (tmp_path / 'Gemfile').write_text("source 'https://rubygems.org'\n") + doc = Document( + str(tmp_path / 'Gemfile'), + "source 'https://rubygems.org'\n", + absolute_path=str(tmp_path / 'Gemfile'), + ) + lock_path = tmp_path / RUBY_LOCK_FILE_NAME + + def side_effect( + commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + ) -> str: + lock_path.write_text('GEM\n remote: https://rubygems.org/\n specs:\n') + return 'output' + + with patch(f'{_BASE_MODULE}.execute_commands', side_effect=side_effect): + result = restore_ruby.try_restore_dependencies(doc) + + assert result is not None + assert not lock_path.exists(), f'{RUBY_LOCK_FILE_NAME} must be deleted after restore' + + def test_preexisting_lockfile_is_not_deleted( + self, restore_ruby: RestoreRubyDependencies, tmp_path: Path + ) -> None: + lock_content = 'GEM\n remote: https://rubygems.org/\n specs:\n rake (13.0.6)\n' + (tmp_path / 'Gemfile').write_text("source 'https://rubygems.org'\ngem 'rake'\n") + lock_path = tmp_path / RUBY_LOCK_FILE_NAME + lock_path.write_text(lock_content) + doc = Document( + str(tmp_path / 'Gemfile'), + "source 'https://rubygems.org'\ngem 'rake'\n", + absolute_path=str(tmp_path / 'Gemfile'), + ) + + result = restore_ruby.try_restore_dependencies(doc) + + assert result is not None + assert lock_path.exists(), f'Pre-existing {RUBY_LOCK_FILE_NAME} must not be deleted' diff --git a/tests/cli/files_collector/sca/sbt/__init__.py b/tests/cli/files_collector/sca/sbt/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/cli/files_collector/sca/sbt/test_restore_sbt_dependencies.py b/tests/cli/files_collector/sca/sbt/test_restore_sbt_dependencies.py new file mode 100644 index 00000000..a5de7fb7 --- /dev/null +++ b/tests/cli/files_collector/sca/sbt/test_restore_sbt_dependencies.py @@ -0,0 +1,88 @@ +from pathlib import Path +from typing import Optional +from unittest.mock import MagicMock, patch + +import pytest +import typer + +from cycode.cli.files_collector.sca.sbt.restore_sbt_dependencies import ( + SBT_LOCK_FILE_NAME, + RestoreSbtDependencies, +) +from cycode.cli.models import Document + +_BASE_MODULE = 'cycode.cli.files_collector.sca.base_restore_dependencies' + + +@pytest.fixture +def mock_ctx(tmp_path: Path) -> typer.Context: + ctx = MagicMock(spec=typer.Context) + ctx.obj = {'monitor': False} + ctx.params = {'path': str(tmp_path)} + return ctx + + +@pytest.fixture +def restore_sbt(mock_ctx: typer.Context) -> RestoreSbtDependencies: + return RestoreSbtDependencies(mock_ctx, is_git_diff=False, command_timeout=30) + + +class TestIsProject: + def test_sbt_file_matches(self, restore_sbt: RestoreSbtDependencies) -> None: + doc = Document('build.sbt', 'name := "my-project"\n') + assert restore_sbt.is_project(doc) is True + + def test_sbt_in_subdir_matches(self, restore_sbt: RestoreSbtDependencies) -> None: + doc = Document('myapp/build.sbt', 'name := "my-project"\n') + assert restore_sbt.is_project(doc) is True + + def test_build_gradle_does_not_match(self, restore_sbt: RestoreSbtDependencies) -> None: + doc = Document('build.gradle', '') + assert restore_sbt.is_project(doc) is False + + def test_pom_xml_does_not_match(self, restore_sbt: RestoreSbtDependencies) -> None: + doc = Document('pom.xml', '') + assert restore_sbt.is_project(doc) is False + + +class TestCleanup: + def test_generated_lockfile_is_deleted_after_restore( + self, restore_sbt: RestoreSbtDependencies, tmp_path: Path + ) -> None: + (tmp_path / 'build.sbt').write_text('name := "test"\n') + doc = Document( + str(tmp_path / 'build.sbt'), + 'name := "test"\n', + absolute_path=str(tmp_path / 'build.sbt'), + ) + lock_path = tmp_path / SBT_LOCK_FILE_NAME + + def side_effect( + commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + ) -> str: + lock_path.write_text('[{"org": "org.typelevel", "name": "cats-core", "version": "2.10.0"}]') + return 'output' + + with patch(f'{_BASE_MODULE}.execute_commands', side_effect=side_effect): + result = restore_sbt.try_restore_dependencies(doc) + + assert result is not None + assert not lock_path.exists(), f'{SBT_LOCK_FILE_NAME} must be deleted after restore' + + def test_preexisting_lockfile_is_not_deleted( + self, restore_sbt: RestoreSbtDependencies, tmp_path: Path + ) -> None: + lock_content = '[{"org": "org.typelevel", "name": "cats-core", "version": "2.10.0"}]' + (tmp_path / 'build.sbt').write_text('name := "test"\n') + lock_path = tmp_path / SBT_LOCK_FILE_NAME + lock_path.write_text(lock_content) + doc = Document( + str(tmp_path / 'build.sbt'), + 'name := "test"\n', + absolute_path=str(tmp_path / 'build.sbt'), + ) + + result = restore_sbt.try_restore_dependencies(doc) + + assert result is not None + assert lock_path.exists(), f'Pre-existing {SBT_LOCK_FILE_NAME} must not be deleted' diff --git a/tests/cli/files_collector/sca/test_base_restore_dependencies.py b/tests/cli/files_collector/sca/test_base_restore_dependencies.py new file mode 100644 index 00000000..65c5f3e5 --- /dev/null +++ b/tests/cli/files_collector/sca/test_base_restore_dependencies.py @@ -0,0 +1,147 @@ +"""Tests for BaseRestoreDependencies cleanup behavior. + +Verifies that lock files generated by restore commands are deleted after +scanning, while pre-existing lock files are left untouched. +""" +from pathlib import Path +from typing import Optional +from unittest.mock import MagicMock, patch + +import pytest +import typer + +from cycode.cli.files_collector.sca.base_restore_dependencies import BaseRestoreDependencies +from cycode.cli.models import Document + +_LOCK_FILE_NAME = 'generated.lock' +_MANIFEST_FILE_NAME = 'manifest.txt' +_LOCK_CONTENT = 'generated lock content' + +_BASE_MODULE = 'cycode.cli.files_collector.sca.base_restore_dependencies' + + +class _MinimalRestoreHandler(BaseRestoreDependencies): + """Minimal concrete subclass for directly testing BaseRestoreDependencies.""" + + def is_project(self, document: Document) -> bool: + return document.path.endswith(_MANIFEST_FILE_NAME) + + def get_commands(self, manifest_file_path: str) -> list[list[str]]: + return [['echo', 'fake']] + + def get_lock_file_name(self) -> str: + return _LOCK_FILE_NAME + + def get_lock_file_names(self) -> list[str]: + return [_LOCK_FILE_NAME] + + +@pytest.fixture +def mock_ctx(tmp_path: Path) -> typer.Context: + ctx = MagicMock(spec=typer.Context) + ctx.obj = {'monitor': False} + ctx.params = {'path': str(tmp_path)} + return ctx + + +@pytest.fixture +def handler(mock_ctx: typer.Context) -> _MinimalRestoreHandler: + return _MinimalRestoreHandler(mock_ctx, is_git_diff=False, command_timeout=30) + + +def _make_doc(tmp_path: Path) -> Document: + manifest = tmp_path / _MANIFEST_FILE_NAME + manifest.write_text('content') + return Document(str(manifest), 'content', absolute_path=str(manifest)) + + +def _make_execute_side_effect(lock_path: Path, content: str = _LOCK_CONTENT): + """Returns an execute_commands side_effect that writes the lock file.""" + + def side_effect( + commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + ) -> str: + lock_path.write_text(content) + return 'output' + + return side_effect + + +class TestCleanupGeneratedFile: + def test_generated_lockfile_is_deleted_after_restore(self, handler: _MinimalRestoreHandler, tmp_path: Path) -> None: + doc = _make_doc(tmp_path) + lock_path = tmp_path / _LOCK_FILE_NAME + + with patch(f'{_BASE_MODULE}.execute_commands', side_effect=_make_execute_side_effect(lock_path)): + result = handler.try_restore_dependencies(doc) + + assert result is not None + assert result.content == _LOCK_CONTENT + assert not lock_path.exists(), 'Generated lock file must be deleted after restore' + + def test_preexisting_lockfile_is_not_deleted(self, handler: _MinimalRestoreHandler, tmp_path: Path) -> None: + doc = _make_doc(tmp_path) + lock_path = tmp_path / _LOCK_FILE_NAME + lock_path.write_text('pre-existing content') + + result = handler.try_restore_dependencies(doc) + + assert result is not None + assert result.content == 'pre-existing content' + assert lock_path.exists(), 'Pre-existing lock file must not be deleted' + + def test_returned_document_content_matches_generated_file( + self, handler: _MinimalRestoreHandler, tmp_path: Path + ) -> None: + doc = _make_doc(tmp_path) + expected = '{"dependencies": {"requests": "^2.31"}}' + lock_path = tmp_path / _LOCK_FILE_NAME + + with patch(f'{_BASE_MODULE}.execute_commands', side_effect=_make_execute_side_effect(lock_path, expected)): + result = handler.try_restore_dependencies(doc) + + assert result is not None + assert result.content == expected + + def test_cleanup_does_not_raise_when_generated_file_missing( + self, handler: _MinimalRestoreHandler, tmp_path: Path + ) -> None: + """unlink(missing_ok=True) must not raise even if the command didn't create the file.""" + doc = _make_doc(tmp_path) + + def side_effect(commands, timeout, output_file_path=None, working_directory=None): + return 'output' # returned non-None but didn't create the file + + with patch(f'{_BASE_MODULE}.execute_commands', side_effect=side_effect): + result = handler.try_restore_dependencies(doc) + + # File was never created; content is None but no exception raised + assert result is not None + assert result.content is None + + def test_failed_command_returns_none_and_no_file_created( + self, handler: _MinimalRestoreHandler, tmp_path: Path + ) -> None: + doc = _make_doc(tmp_path) + lock_path = tmp_path / _LOCK_FILE_NAME + + with patch(f'{_BASE_MODULE}.execute_commands', return_value=None): + result = handler.try_restore_dependencies(doc) + + assert result is None + assert not lock_path.exists() + + def test_generated_file_content_available_in_document_after_deletion( + self, handler: _MinimalRestoreHandler, tmp_path: Path + ) -> None: + """The Document must carry the file content even after the file is removed.""" + doc = _make_doc(tmp_path) + lock_path = tmp_path / _LOCK_FILE_NAME + expected = 'important scan data' + + with patch(f'{_BASE_MODULE}.execute_commands', side_effect=_make_execute_side_effect(lock_path, expected)): + result = handler.try_restore_dependencies(doc) + + assert not lock_path.exists() + assert result is not None + assert result.content == expected From a7aa2d051ca87703acaf27b50ab37a42570dcc9f Mon Sep 17 00:00:00 2001 From: "omer.roth" Date: Wed, 11 Mar 2026 14:51:12 +0200 Subject: [PATCH 3/3] CM-60869 fixed linting --- .../sca/maven/restore_maven_dependencies.py | 2 +- .../sca/go/test_restore_go_dependencies.py | 5 ++++- .../sca/maven/test_restore_gradle_dependencies.py | 14 +++++++++----- .../sca/maven/test_restore_maven_dependencies.py | 14 +++++++++----- .../sca/npm/test_restore_npm_dependencies.py | 9 +++++---- .../sca/npm/test_restore_pnpm_dependencies.py | 9 +++++---- .../sca/npm/test_restore_yarn_dependencies.py | 9 +++++---- .../sca/nuget/test_restore_nuget_dependencies.py | 9 +++++---- .../sca/php/test_restore_composer_dependencies.py | 10 ++++++---- .../sca/python/test_restore_pipenv_dependencies.py | 9 +++++++-- .../sca/python/test_restore_poetry_dependencies.py | 5 ++++- .../sca/ruby/test_restore_ruby_dependencies.py | 9 +++++---- .../sca/sbt/test_restore_sbt_dependencies.py | 9 +++++---- .../sca/test_base_restore_dependencies.py | 5 +++-- 14 files changed, 73 insertions(+), 45 deletions(-) diff --git a/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py b/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py index d461cc0e..740ccca9 100644 --- a/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py +++ b/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py @@ -10,7 +10,7 @@ execute_commands, ) from cycode.cli.models import Document -from cycode.cli.utils.path_utils import get_file_content, get_file_dir, join_paths +from cycode.cli.utils.path_utils import get_file_content, join_paths from cycode.logger import get_logger logger = get_logger('Maven Restore Dependencies') diff --git a/tests/cli/files_collector/sca/go/test_restore_go_dependencies.py b/tests/cli/files_collector/sca/go/test_restore_go_dependencies.py index 3a985594..633d24e8 100644 --- a/tests/cli/files_collector/sca/go/test_restore_go_dependencies.py +++ b/tests/cli/files_collector/sca/go/test_restore_go_dependencies.py @@ -60,7 +60,10 @@ def test_generated_output_file_is_deleted_after_restore( output_path = tmp_path / GO_RESTORE_FILE_NAME def side_effect( - commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + commands: list, + timeout: int, + output_file_path: Optional[str] = None, + working_directory: Optional[str] = None, ) -> str: # Go uses create_output_file_manually=True; output_file_path is provided target = output_file_path or str(output_path) diff --git a/tests/cli/files_collector/sca/maven/test_restore_gradle_dependencies.py b/tests/cli/files_collector/sca/maven/test_restore_gradle_dependencies.py index 1a499b2c..72ca8a7d 100644 --- a/tests/cli/files_collector/sca/maven/test_restore_gradle_dependencies.py +++ b/tests/cli/files_collector/sca/maven/test_restore_gradle_dependencies.py @@ -60,7 +60,10 @@ def test_generated_dep_tree_file_is_deleted_after_restore( output_path = tmp_path / BUILD_GRADLE_DEP_TREE_FILE_NAME def side_effect( - commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + commands: list, + timeout: int, + output_file_path: Optional[str] = None, + working_directory: Optional[str] = None, ) -> str: # Gradle uses create_output_file_manually=True; output_file_path is provided target = output_file_path or str(output_path) @@ -91,9 +94,7 @@ def test_preexisting_dep_tree_file_is_not_deleted( assert result is not None assert output_path.exists(), f'Pre-existing {BUILD_GRADLE_DEP_TREE_FILE_NAME} must not be deleted' - def test_kts_build_file_also_cleaned_up( - self, restore_gradle: RestoreGradleDependencies, tmp_path: Path - ) -> None: + def test_kts_build_file_also_cleaned_up(self, restore_gradle: RestoreGradleDependencies, tmp_path: Path) -> None: (tmp_path / BUILD_GRADLE_KTS_FILE_NAME).write_text('plugins { java }\n') doc = Document( str(tmp_path / BUILD_GRADLE_KTS_FILE_NAME), @@ -103,7 +104,10 @@ def test_kts_build_file_also_cleaned_up( output_path = tmp_path / BUILD_GRADLE_DEP_TREE_FILE_NAME def side_effect( - commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + commands: list, + timeout: int, + output_file_path: Optional[str] = None, + working_directory: Optional[str] = None, ) -> str: target = output_file_path or str(output_path) Path(target).write_text('compileClasspath\n') diff --git a/tests/cli/files_collector/sca/maven/test_restore_maven_dependencies.py b/tests/cli/files_collector/sca/maven/test_restore_maven_dependencies.py index 5bd3baa4..f365bd92 100644 --- a/tests/cli/files_collector/sca/maven/test_restore_maven_dependencies.py +++ b/tests/cli/files_collector/sca/maven/test_restore_maven_dependencies.py @@ -61,7 +61,10 @@ def test_generated_bom_is_deleted_after_primary_restore( ) def side_effect( - commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + commands: list, + timeout: int, + output_file_path: Optional[str] = None, + working_directory: Optional[str] = None, ) -> str: bom_path.write_text('{"bomFormat": "CycloneDX", "components": []}') return 'output' @@ -87,7 +90,10 @@ def test_generated_dep_tree_is_deleted_after_secondary_restore( ) def side_effect( - commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + commands: list, + timeout: int, + output_file_path: Optional[str] = None, + working_directory: Optional[str] = None, ) -> str: dep_tree_path.write_text('[INFO] com.example:my-app:jar:1.0.0\n') return '[INFO] BUILD SUCCESS' @@ -99,9 +105,7 @@ def side_effect( assert result.content is not None assert not dep_tree_path.exists(), f'{MAVEN_DEP_TREE_FILE_NAME} must be deleted after restore' - def test_preexisting_bom_is_not_deleted( - self, restore_maven: RestoreMavenDependencies, tmp_path: Path - ) -> None: + def test_preexisting_bom_is_not_deleted(self, restore_maven: RestoreMavenDependencies, tmp_path: Path) -> None: pom_content = '4.0.0' (tmp_path / BUILD_MAVEN_FILE_NAME).write_text(pom_content) target_dir = tmp_path / 'target' diff --git a/tests/cli/files_collector/sca/npm/test_restore_npm_dependencies.py b/tests/cli/files_collector/sca/npm/test_restore_npm_dependencies.py index d90fe879..c418b659 100644 --- a/tests/cli/files_collector/sca/npm/test_restore_npm_dependencies.py +++ b/tests/cli/files_collector/sca/npm/test_restore_npm_dependencies.py @@ -112,7 +112,10 @@ def test_generated_lockfile_is_deleted_after_restore( lock_path = tmp_path / NPM_LOCK_FILE_NAME def side_effect( - commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + commands: list, + timeout: int, + output_file_path: Optional[str] = None, + working_directory: Optional[str] = None, ) -> str: lock_path.write_text('{"lockfileVersion": 3}') return 'output' @@ -123,9 +126,7 @@ def side_effect( assert result is not None assert not lock_path.exists(), f'{NPM_LOCK_FILE_NAME} must be deleted after restore' - def test_preexisting_lockfile_is_not_deleted( - self, restore_npm: RestoreNpmDependencies, tmp_path: Path - ) -> None: + def test_preexisting_lockfile_is_not_deleted(self, restore_npm: RestoreNpmDependencies, tmp_path: Path) -> None: (tmp_path / 'package.json').write_text('{"name": "test"}') lock_path = tmp_path / NPM_LOCK_FILE_NAME lock_path.write_text('{"lockfileVersion": 3, "packages": {}}') diff --git a/tests/cli/files_collector/sca/npm/test_restore_pnpm_dependencies.py b/tests/cli/files_collector/sca/npm/test_restore_pnpm_dependencies.py index f0612731..88502578 100644 --- a/tests/cli/files_collector/sca/npm/test_restore_pnpm_dependencies.py +++ b/tests/cli/files_collector/sca/npm/test_restore_pnpm_dependencies.py @@ -106,7 +106,10 @@ def test_generated_lockfile_is_deleted_after_restore( lock_path = tmp_path / PNPM_LOCK_FILE_NAME def side_effect( - commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + commands: list, + timeout: int, + output_file_path: Optional[str] = None, + working_directory: Optional[str] = None, ) -> str: lock_path.write_text('lockfileVersion: 5.4\n') return 'output' @@ -117,9 +120,7 @@ def side_effect( assert result is not None assert not lock_path.exists(), f'{PNPM_LOCK_FILE_NAME} must be deleted after restore' - def test_preexisting_lockfile_is_not_deleted( - self, restore_pnpm: RestorePnpmDependencies, tmp_path: Path - ) -> None: + def test_preexisting_lockfile_is_not_deleted(self, restore_pnpm: RestorePnpmDependencies, tmp_path: Path) -> None: lock_content = 'lockfileVersion: 5.4\n\npackages:\n /pkg@1.0.0:\n resolution: {}\n' (tmp_path / 'package.json').write_text('{"name": "test"}') lock_path = tmp_path / PNPM_LOCK_FILE_NAME diff --git a/tests/cli/files_collector/sca/npm/test_restore_yarn_dependencies.py b/tests/cli/files_collector/sca/npm/test_restore_yarn_dependencies.py index b292c72a..88175031 100644 --- a/tests/cli/files_collector/sca/npm/test_restore_yarn_dependencies.py +++ b/tests/cli/files_collector/sca/npm/test_restore_yarn_dependencies.py @@ -106,7 +106,10 @@ def test_generated_lockfile_is_deleted_after_restore( lock_path = tmp_path / YARN_LOCK_FILE_NAME def side_effect( - commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + commands: list, + timeout: int, + output_file_path: Optional[str] = None, + working_directory: Optional[str] = None, ) -> str: lock_path.write_text('# yarn lockfile v1\n') return 'output' @@ -117,9 +120,7 @@ def side_effect( assert result is not None assert not lock_path.exists(), f'{YARN_LOCK_FILE_NAME} must be deleted after restore' - def test_preexisting_lockfile_is_not_deleted( - self, restore_yarn: RestoreYarnDependencies, tmp_path: Path - ) -> None: + def test_preexisting_lockfile_is_not_deleted(self, restore_yarn: RestoreYarnDependencies, tmp_path: Path) -> None: lock_content = '# yarn lockfile v1\n\npackage@1.0.0:\n resolved "https://example.com"\n' (tmp_path / 'package.json').write_text('{"name": "test"}') lock_path = tmp_path / YARN_LOCK_FILE_NAME diff --git a/tests/cli/files_collector/sca/nuget/test_restore_nuget_dependencies.py b/tests/cli/files_collector/sca/nuget/test_restore_nuget_dependencies.py index f3d60daf..0ec13441 100644 --- a/tests/cli/files_collector/sca/nuget/test_restore_nuget_dependencies.py +++ b/tests/cli/files_collector/sca/nuget/test_restore_nuget_dependencies.py @@ -58,7 +58,10 @@ def test_generated_lockfile_is_deleted_after_restore( lock_path = tmp_path / NUGET_LOCK_FILE_NAME def side_effect( - commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + commands: list, + timeout: int, + output_file_path: Optional[str] = None, + working_directory: Optional[str] = None, ) -> str: lock_path.write_text('{"version": 1, "dependencies": {}}') return 'output' @@ -69,9 +72,7 @@ def side_effect( assert result is not None assert not lock_path.exists(), f'{NUGET_LOCK_FILE_NAME} must be deleted after restore' - def test_preexisting_lockfile_is_not_deleted( - self, restore_nuget: RestoreNugetDependencies, tmp_path: Path - ) -> None: + def test_preexisting_lockfile_is_not_deleted(self, restore_nuget: RestoreNugetDependencies, tmp_path: Path) -> None: lock_content = '{"version": 1, "dependencies": {"net8.0": {}}}' (tmp_path / 'MyProject.csproj').write_text('') lock_path = tmp_path / NUGET_LOCK_FILE_NAME diff --git a/tests/cli/files_collector/sca/php/test_restore_composer_dependencies.py b/tests/cli/files_collector/sca/php/test_restore_composer_dependencies.py index 3c4558b9..6e3ea53b 100644 --- a/tests/cli/files_collector/sca/php/test_restore_composer_dependencies.py +++ b/tests/cli/files_collector/sca/php/test_restore_composer_dependencies.py @@ -69,6 +69,7 @@ def test_existing_composer_lock_returned_directly( def test_get_lock_file_name(self, restore_composer: RestoreComposerDependencies) -> None: assert restore_composer.get_lock_file_name() == COMPOSER_LOCK_FILE_NAME + _BASE_MODULE = 'cycode.cli.files_collector.sca.base_restore_dependencies' @@ -78,13 +79,14 @@ def test_generated_lockfile_is_deleted_after_restore( ) -> None: manifest_content = '{"name": "vendor/project"}\n' (tmp_path / 'composer.json').write_text(manifest_content) - doc = Document( - str(tmp_path / 'composer.json'), manifest_content, absolute_path=str(tmp_path / 'composer.json') - ) + doc = Document(str(tmp_path / 'composer.json'), manifest_content, absolute_path=str(tmp_path / 'composer.json')) lock_path = tmp_path / COMPOSER_LOCK_FILE_NAME def side_effect( - commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + commands: list, + timeout: int, + output_file_path: Optional[str] = None, + working_directory: Optional[str] = None, ) -> str: lock_path.write_text('{"_readme": [], "packages": []}') return 'output' diff --git a/tests/cli/files_collector/sca/python/test_restore_pipenv_dependencies.py b/tests/cli/files_collector/sca/python/test_restore_pipenv_dependencies.py index baa05398..a6d97320 100644 --- a/tests/cli/files_collector/sca/python/test_restore_pipenv_dependencies.py +++ b/tests/cli/files_collector/sca/python/test_restore_pipenv_dependencies.py @@ -87,7 +87,10 @@ def test_generated_lockfile_is_deleted_after_restore( lock_path = tmp_path / PIPENV_LOCK_FILE_NAME def side_effect( - commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + commands: list, + timeout: int, + output_file_path: Optional[str] = None, + working_directory: Optional[str] = None, ) -> str: lock_path.write_text('{"_meta": {}, "default": {}, "develop": {}}') return 'output' @@ -105,7 +108,9 @@ def test_preexisting_lockfile_is_not_deleted( (tmp_path / 'Pipfile').write_text('[[source]]\nname = "pypi"\n') lock_path = tmp_path / PIPENV_LOCK_FILE_NAME lock_path.write_text(lock_content) - doc = Document(str(tmp_path / 'Pipfile'), '[[source]]\nname = "pypi"\n', absolute_path=str(tmp_path / 'Pipfile')) + doc = Document( + str(tmp_path / 'Pipfile'), '[[source]]\nname = "pypi"\n', absolute_path=str(tmp_path / 'Pipfile') + ) result = restore_pipenv.try_restore_dependencies(doc) diff --git a/tests/cli/files_collector/sca/python/test_restore_poetry_dependencies.py b/tests/cli/files_collector/sca/python/test_restore_poetry_dependencies.py index 55d5080b..cf4c312b 100644 --- a/tests/cli/files_collector/sca/python/test_restore_poetry_dependencies.py +++ b/tests/cli/files_collector/sca/python/test_restore_poetry_dependencies.py @@ -116,7 +116,10 @@ def test_generated_lockfile_is_deleted_after_restore( lock_path = tmp_path / POETRY_LOCK_FILE_NAME def side_effect( - commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + commands: list, + timeout: int, + output_file_path: Optional[str] = None, + working_directory: Optional[str] = None, ) -> str: lock_path.write_text('# This file is generated by Poetry\n') return 'output' diff --git a/tests/cli/files_collector/sca/ruby/test_restore_ruby_dependencies.py b/tests/cli/files_collector/sca/ruby/test_restore_ruby_dependencies.py index 35da7871..ac3e9d73 100644 --- a/tests/cli/files_collector/sca/ruby/test_restore_ruby_dependencies.py +++ b/tests/cli/files_collector/sca/ruby/test_restore_ruby_dependencies.py @@ -58,7 +58,10 @@ def test_generated_lockfile_is_deleted_after_restore( lock_path = tmp_path / RUBY_LOCK_FILE_NAME def side_effect( - commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + commands: list, + timeout: int, + output_file_path: Optional[str] = None, + working_directory: Optional[str] = None, ) -> str: lock_path.write_text('GEM\n remote: https://rubygems.org/\n specs:\n') return 'output' @@ -69,9 +72,7 @@ def side_effect( assert result is not None assert not lock_path.exists(), f'{RUBY_LOCK_FILE_NAME} must be deleted after restore' - def test_preexisting_lockfile_is_not_deleted( - self, restore_ruby: RestoreRubyDependencies, tmp_path: Path - ) -> None: + def test_preexisting_lockfile_is_not_deleted(self, restore_ruby: RestoreRubyDependencies, tmp_path: Path) -> None: lock_content = 'GEM\n remote: https://rubygems.org/\n specs:\n rake (13.0.6)\n' (tmp_path / 'Gemfile').write_text("source 'https://rubygems.org'\ngem 'rake'\n") lock_path = tmp_path / RUBY_LOCK_FILE_NAME diff --git a/tests/cli/files_collector/sca/sbt/test_restore_sbt_dependencies.py b/tests/cli/files_collector/sca/sbt/test_restore_sbt_dependencies.py index a5de7fb7..415e5f94 100644 --- a/tests/cli/files_collector/sca/sbt/test_restore_sbt_dependencies.py +++ b/tests/cli/files_collector/sca/sbt/test_restore_sbt_dependencies.py @@ -58,7 +58,10 @@ def test_generated_lockfile_is_deleted_after_restore( lock_path = tmp_path / SBT_LOCK_FILE_NAME def side_effect( - commands: list, timeout: int, output_file_path: Optional[str] = None, working_directory: Optional[str] = None + commands: list, + timeout: int, + output_file_path: Optional[str] = None, + working_directory: Optional[str] = None, ) -> str: lock_path.write_text('[{"org": "org.typelevel", "name": "cats-core", "version": "2.10.0"}]') return 'output' @@ -69,9 +72,7 @@ def side_effect( assert result is not None assert not lock_path.exists(), f'{SBT_LOCK_FILE_NAME} must be deleted after restore' - def test_preexisting_lockfile_is_not_deleted( - self, restore_sbt: RestoreSbtDependencies, tmp_path: Path - ) -> None: + def test_preexisting_lockfile_is_not_deleted(self, restore_sbt: RestoreSbtDependencies, tmp_path: Path) -> None: lock_content = '[{"org": "org.typelevel", "name": "cats-core", "version": "2.10.0"}]' (tmp_path / 'build.sbt').write_text('name := "test"\n') lock_path = tmp_path / SBT_LOCK_FILE_NAME diff --git a/tests/cli/files_collector/sca/test_base_restore_dependencies.py b/tests/cli/files_collector/sca/test_base_restore_dependencies.py index 65c5f3e5..b291a95f 100644 --- a/tests/cli/files_collector/sca/test_base_restore_dependencies.py +++ b/tests/cli/files_collector/sca/test_base_restore_dependencies.py @@ -3,6 +3,7 @@ Verifies that lock files generated by restore commands are deleted after scanning, while pre-existing lock files are left untouched. """ + from pathlib import Path from typing import Optional from unittest.mock import MagicMock, patch @@ -55,7 +56,7 @@ def _make_doc(tmp_path: Path) -> Document: return Document(str(manifest), 'content', absolute_path=str(manifest)) -def _make_execute_side_effect(lock_path: Path, content: str = _LOCK_CONTENT): +def _make_execute_side_effect(lock_path: Path, content: str = _LOCK_CONTENT) -> object: """Returns an execute_commands side_effect that writes the lock file.""" def side_effect( @@ -109,7 +110,7 @@ def test_cleanup_does_not_raise_when_generated_file_missing( """unlink(missing_ok=True) must not raise even if the command didn't create the file.""" doc = _make_doc(tmp_path) - def side_effect(commands, timeout, output_file_path=None, working_directory=None): + def side_effect(**_kwargs: object) -> str: return 'output' # returned non-None but didn't create the file with patch(f'{_BASE_MODULE}.execute_commands', side_effect=side_effect):