Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions cycode/cli/files_collector/sca/base_restore_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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]:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from os import path
from pathlib import Path
from typing import Optional

import typer
Expand All @@ -9,7 +10,10 @@
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')

BUILD_MAVEN_FILE_NAME = 'pom.xml'
MAVEN_CYCLONE_DEP_TREE_FILE_NAME = 'bom.json'
Expand Down Expand Up @@ -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(
Expand All @@ -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]]:
Expand Down
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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', '<project/>')
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
Empty file.
Original file line number Diff line number Diff line change
@@ -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_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', '<project/>')
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'
Loading
Loading