diff --git a/.githooks/pre-push b/.githooks/pre-push index f019f8b..24b619c 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -8,14 +8,13 @@ trap 'exit 1' ERR # Run unit tests poetry run pytest -# Test with test packages locally -poetry run python -m ossprey --dry-run-safe --package test/npm_simple_math/ --mode npm -poetry run python -m ossprey --dry-run-safe --package test/yarn_simple_math/ --mode yarn -poetry run python -m ossprey --dry-run-safe --package test/yarn_massive_math/ --mode yarn -poetry run python -m ossprey --dry-run-safe --package test/python_simple_math/ --mode python-requirements +# Run Smoke tests (fast, non-network subset) +# For the full smoke suite including network/slow tests, use: +# poetry run pytest test/smoke/test_smoke.py +poetry run pytest test/smoke/test_smoke.py -m "smoke and not network and not slow" # Test malicious detection (should fail with exit code 1) -if poetry run python -m ossprey --dry-run-malicious --package test/npm_simple_math/ --mode npm; then +if poetry run python -m ossprey --dry-run-malicious --package test/test_packages/npm_simple_math/ --mode npm; then echo "ERROR: Malicious package detection failed - should have detected malware!" exit 1 else diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d03ca0d..3cbc5ab 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,6 +33,10 @@ jobs: - name: Run Runtime Tests run: | - poetry run python -m ossprey --dry-run-safe --package test/npm_simple_math/ --mode npm - poetry run python -m ossprey --dry-run-safe --package test/yarn_simple_math/ --mode yarn - poetry run python -m ossprey --dry-run-safe --package test/python_simple_math/ --mode python-requirements \ No newline at end of file + poetry run python -m ossprey --dry-run-safe --package test/test_packages/npm_simple_math/ --mode npm + poetry run python -m ossprey --dry-run-safe --package test/test_packages/yarn_simple_math/ --mode yarn + poetry run python -m ossprey --dry-run-safe --package test/test_packages/python_simple_math/ --mode python-requirements + + - name: Run smoke tests + run: | + poetry run pytest test/smoke/test_smoke.py -m "not network and not slow" \ No newline at end of file diff --git a/ossprey/sbom_python.py b/ossprey/sbom_python.py index a5708d9..de4d734 100644 --- a/ossprey/sbom_python.py +++ b/ossprey/sbom_python.py @@ -13,6 +13,8 @@ from ossbom.model.ossbom import OSSBOM from ossbom.model.component import Component +from ossprey.virtualenv import VirtualEnv + logger = logging.getLogger(__name__) @@ -34,11 +36,11 @@ def create_sbom_from_requirements(requirements_file: str) -> OSSBOM: cmd = get_cyclonedx_binary() # This command generates an SBOM for the active virtual environment in JSON format result = subprocess.run( - [cmd, 'requirements', requirements_file], + [cmd, "requirements", requirements_file], check=True, capture_output=True, text=True, - env=os.environ.copy() + env=os.environ.copy(), ) ret = result.stdout @@ -60,7 +62,7 @@ def create_sbom_from_requirements(requirements_file: str) -> OSSBOM: def update_sbom_from_requirements(ossbom: OSSBOM, requirements_file: str) -> OSSBOM: sbom = create_sbom_from_requirements(requirements_file) ossbom.add_components(sbom.get_components()) - + return ossbom @@ -70,11 +72,11 @@ def create_sbom_from_env() -> OSSBOM: cmd = get_cyclonedx_binary() # This command generates an SBOM for the active virtual environment in JSON format result = subprocess.run( - [cmd, 'environment'], + [cmd, "environment"], check=True, capture_output=True, text=True, - env=os.environ.copy() + env=os.environ.copy(), ) ret = result.stdout @@ -110,16 +112,35 @@ def get_poetry_purls_from_lock(lockfile: str = "poetry.lock") -> list[PackageURL def update_sbom_from_poetry(ossbom: OSSBOM, package_dir: str) -> OSSBOM: if not os.path.exists(os.path.join(package_dir, "poetry.lock")): - # Run poetry install to generate the poetry.lock file + # Run poetry install to generate the poetry.lock file try: - subprocess.run(['poetry', 'install'], cwd=package_dir, check=True) + subprocess.run(["poetry", "install"], cwd=package_dir, check=True) except subprocess.CalledProcessError as e: logger.error(f"Error running poetry install: {e}") raise e - + # Get the packages from the poetry.lock file purls = get_poetry_purls_from_lock(os.path.join(package_dir, "poetry.lock")) - ossbom.add_components([Component.create(name=purl.name, version=purl.version, source="poetry", type="pypi") for purl in purls]) + ossbom.add_components( + [ + Component.create( + name=purl.name, version=purl.version, source="poetry", type="pypi" + ) + for purl in purls + ] + ) + + return ossbom + + +def update_sbom_from_virtualenv(ossbom: OSSBOM, package_name: str) -> OSSBOM: + venv = VirtualEnv() + try: + venv.install_package(package_name) + requirements_file = venv.create_requirements_file_from_env() + ossbom = update_sbom_from_requirements(ossbom, requirements_file) + finally: + venv.exit() return ossbom diff --git a/ossprey/scan.py b/ossprey/scan.py index 9cdbabd..eb0871a 100644 --- a/ossprey/scan.py +++ b/ossprey/scan.py @@ -2,16 +2,18 @@ import json import logging import os -from packageurl import PackageURL +from subprocess import CalledProcessError from ossprey.environment import get_environment_details -from ossprey.log import init_logging from ossprey.modes import get_modes, get_all_modes -from ossprey.sbom_python import update_sbom_from_requirements, update_sbom_from_poetry +from ossprey.sbom_python import ( + update_sbom_from_requirements, + update_sbom_from_poetry, + update_sbom_from_virtualenv, +) from ossprey.sbom_javascript import update_sbom_from_npm, update_sbom_from_yarn from ossprey.sbom_filesystem import update_sbom_from_filesystem from ossprey.ossprey import Ossprey -from ossprey.virtualenv import VirtualEnv from ossbom.converters.factory import SBOMConverterFactory from ossbom.model.ossbom import OSSBOM @@ -20,6 +22,49 @@ logger = logging.getLogger(__name__) +def scan_python(modes: list[str], sbom: OSSBOM, package_name: str) -> OSSBOM: + + if "python-requirements" in modes: + sbom = update_sbom_from_requirements(sbom, package_name + "/requirements.txt") + + if "poetry" in modes: + try: + sbom = update_sbom_from_poetry(sbom, package_name) + except CalledProcessError as e: + logger.error( + "Error running poetry scan, skipping poetry scan, attempting pipenv scan instead" + ) + logger.debug(e.stderr) + logger.debug("--") + logger.debug(e.stdout) + + # Appending pipenv to modes to attempt to get some python dependencies if poetry fails + modes.append("pipenv") + + if "pipenv" in modes: + sbom = update_sbom_from_virtualenv(sbom, package_name) + + return sbom + + +def scan_javascript(modes: list[str], sbom: OSSBOM, package_name: str) -> OSSBOM: + if "npm" in modes: + sbom = update_sbom_from_npm(sbom, package_name) + + if "yarn" in modes: + sbom = update_sbom_from_yarn(sbom, package_name) + + return sbom + + +def scan_filesystem(modes: list[str], sbom: OSSBOM, package_name: str) -> OSSBOM: + + if "fs" in modes: + sbom = update_sbom_from_filesystem(sbom, package_name) + + return sbom + + def scan( package_name: str, mode: str = "auto", @@ -50,37 +95,16 @@ def scan( if any(mode not in get_all_modes() for mode in modes) or len(modes) == 0: raise Exception("Invalid scanning method: " + str(modes)) - if "pipenv" in modes: - venv = VirtualEnv() - venv.enter() - - venv.install_package(package_name) - requirements_file = venv.create_requirements_file_from_env() - - sbom = update_sbom_from_requirements(sbom, requirements_file) - - venv.exit() - - if "python-requirements" in modes: - sbom = update_sbom_from_requirements(sbom, package_name + "/requirements.txt") - - if "poetry" in modes: - sbom = update_sbom_from_poetry(sbom, package_name) - - if "npm" in modes: - sbom = update_sbom_from_npm(sbom, package_name) - - if "yarn" in modes: - sbom = update_sbom_from_yarn(sbom, package_name) - - if "fs" in modes: - sbom = update_sbom_from_filesystem(sbom, package_name) + # Enrich the sbom based on the scans provided + sbom = scan_python(modes, sbom, package_name) + sbom = scan_javascript(modes, sbom, package_name) + sbom = scan_filesystem(modes, sbom, package_name) # Update sbom to contain the local environment env = get_environment_details(package_name) sbom.update_environment(env) - logger.info(f"Scanning {len(sbom.get_components())}") + logger.info(f"Scanning {len(sbom.get_components())} components") if not local_scan: ossprey = Ossprey(url, api_key) diff --git a/ossprey/test_main.py b/ossprey/test_main.py index da312b4..53d7c55 100644 --- a/ossprey/test_main.py +++ b/ossprey/test_main.py @@ -7,7 +7,7 @@ def test_main_function(monkeypatch, capsys): monkeypatch.setattr("sys.argv", ["script.py"]) - monkeypatch.setenv("INPUT_PACKAGE", "test/python_simple_math") + monkeypatch.setenv("INPUT_PACKAGE", "test/test_packages/python_simple_math") monkeypatch.setenv("INPUT_MODE", "python-requirements") monkeypatch.setenv("INPUT_DRY_RUN_SAFE", "True") @@ -23,7 +23,7 @@ def test_main_function(monkeypatch, capsys): def test_main_function_with_output(monkeypatch, capsys): monkeypatch.setattr("sys.argv", ["script.py"]) - monkeypatch.setenv("INPUT_PACKAGE", "test/python_simple_math") + monkeypatch.setenv("INPUT_PACKAGE", "test/test_packages/python_simple_math") monkeypatch.setenv("INPUT_MODE", "python-requirements") monkeypatch.setenv("INPUT_DRY_RUN_SAFE", "True") monkeypatch.setenv("INPUT_OUTPUT", "sbom_output.json") @@ -44,12 +44,12 @@ def test_main_function_with_output(monkeypatch, capsys): os.remove("sbom_output.json") -@pytest.mark.parametrize("soft_error, expected_ret", [ - ("True", 0), - ("False", 1)]) +@pytest.mark.parametrize("soft_error, expected_ret", [("True", 0), ("False", 1)]) def test_main_function_soft_error(monkeypatch, soft_error, expected_ret): monkeypatch.setattr("sys.argv", ["script.py"]) - monkeypatch.setenv("INPUT_PACKAGE", "test/python_simple_math_no_exist") + monkeypatch.setenv( + "INPUT_PACKAGE", "test/test_packages/python_simple_math_no_exist" + ) monkeypatch.setenv("INPUT_MODE", "python-requirements") monkeypatch.setenv("INPUT_DRY_RUN_SAFE", "True") monkeypatch.setenv("INPUT_SOFT_ERROR", soft_error) @@ -59,3 +59,13 @@ def test_main_function_soft_error(monkeypatch, soft_error, expected_ret): assert excinfo.value.code == expected_ret + +def test_main_function_with_broken_poetry(monkeypatch, capsys): + monkeypatch.setattr("sys.argv", ["script.py"]) + monkeypatch.setenv("INPUT_PACKAGE", "test/test_packages/poetry_broken_simple_math") + monkeypatch.setenv("INPUT_DRY_RUN_SAFE", "True") + + with pytest.raises(SystemExit) as excinfo: + main() + + assert excinfo.value.code == 0 diff --git a/ossprey/test_mode_fs.py b/ossprey/test_mode_fs.py index 0940962..282606b 100644 --- a/ossprey/test_mode_fs.py +++ b/ossprey/test_mode_fs.py @@ -21,7 +21,7 @@ def cleanup(): "docker_folder, expected_packages, not_expected_packages, no_of_packages", [ ( - "../test/docker_js_simple_math", + "../test/test_packages/docker_js_simple_math", { "npm": ["lodash", "axios", "@types/ms", "ms"], "github": ["ossprey/example_malicious_javascript"], @@ -32,7 +32,7 @@ def cleanup(): 509, ), ( - "../test/docker_py_simple_math", + "../test/test_packages/docker_py_simple_math", { "pypi": ["fastapi", "uvicorn", "solana", "solders", "pydantic"], "github": ["ossprey/example_malicious_python"], diff --git a/ossprey/test_sbom_python.py b/ossprey/test_sbom_python.py index 6d0f5db..af7c66f 100644 --- a/ossprey/test_sbom_python.py +++ b/ossprey/test_sbom_python.py @@ -4,14 +4,18 @@ import pytest from unittest.mock import patch -from ossprey.sbom_python import create_sbom_from_env, create_sbom_from_requirements, get_cyclonedx_binary +from ossprey.sbom_python import ( + create_sbom_from_env, + create_sbom_from_requirements, + get_cyclonedx_binary, +) from ossprey.virtualenv import VirtualEnv def test_get_sbom(): sbom = create_sbom_from_env() - assert sbom.format == 'OSSBOM' + assert sbom.format == "OSSBOM" def test_get_sbom_from_venv(): @@ -20,16 +24,16 @@ def test_get_sbom_from_venv(): venv.enter() # Install a package - venv.install_package('numpy') + venv.install_package("numpy") requirements_file = venv.create_requirements_file_from_env() # Get the SBOM sbom = create_sbom_from_requirements(requirements_file) - assert sbom.format == 'OSSBOM' + assert sbom.format == "OSSBOM" assert len(sbom.components) == 1 - assert any(map(lambda x: x.name == 'numpy', sbom.components.values())) + assert any(map(lambda x: x.name == "numpy", sbom.components.values())) def test_get_sbom_from_venv_local_package(): @@ -38,16 +42,16 @@ def test_get_sbom_from_venv_local_package(): venv.enter() # Install a package - venv.install_package('test/python_simple_math') - + venv.install_package("test/test_packages/python_simple_math") + requirements_file = venv.create_requirements_file_from_env() # Get the SBOM sbom = create_sbom_from_requirements(requirements_file) - assert sbom.format == 'OSSBOM' + assert sbom.format == "OSSBOM" assert len(sbom.components) == 7 - assert any(map(lambda x: x.name == 'simple_math', sbom.components.values())) + assert any(map(lambda x: x.name == "simple_math", sbom.components.values())) @patch("shutil.which") diff --git a/ossprey/test_scan.py b/ossprey/test_scan.py index d07589c..3f45122 100644 --- a/ossprey/test_scan.py +++ b/ossprey/test_scan.py @@ -6,13 +6,13 @@ @pytest.mark.parametrize("mode", ["python-requirements", "auto"]) def test_scan_py_success(mode: str) -> None: - ret = scan("test/python_simple_math", mode=mode, local_scan=True) + ret = scan("test/test_packages/python_simple_math", mode=mode, local_scan=True) assert isinstance(ret, OSSBOM) assert [comp.name for comp in ret.get_components()] == ["numpy", "requests"] def test_scan_py_success_pipenv() -> None: - ret = scan("test/python_simple_math", mode="pipenv", local_scan=True) + ret = scan("test/test_packages/python_simple_math", mode="pipenv", local_scan=True) assert isinstance(ret, OSSBOM) result = [ @@ -29,7 +29,7 @@ def test_scan_py_success_pipenv() -> None: @pytest.mark.parametrize("mode", ["auto", "poetry"]) def test_scan_poetry_success(mode: str) -> None: - ret = scan("test/poetry_simple_math", mode=mode, local_scan=True) + ret = scan("test/test_packages/poetry_simple_math", mode=mode, local_scan=True) assert isinstance(ret, OSSBOM) result = ["certifi", "charset-normalizer", "idna", "numpy", "requests", "urllib3"] @@ -38,7 +38,7 @@ def test_scan_poetry_success(mode: str) -> None: @pytest.mark.parametrize("mode", ["pipenv"]) def test_scan_poetry_success_pipenv(mode: str) -> None: - ret = scan("test/poetry_simple_math", mode=mode, local_scan=True) + ret = scan("test/test_packages/poetry_simple_math", mode=mode, local_scan=True) assert isinstance(ret, OSSBOM) result = [ @@ -55,14 +55,14 @@ def test_scan_poetry_success_pipenv(mode: str) -> None: @pytest.mark.parametrize("mode", ["npm", "auto"]) def test_scan_npm_success(mode: str) -> None: - ret = scan("test/npm_simple_math", mode=mode, local_scan=True) + ret = scan("test/test_packages/npm_simple_math", mode=mode, local_scan=True) assert isinstance(ret, OSSBOM) assert len(ret.get_components()) == 322 @pytest.mark.parametrize(["mode", "num_components"], [("yarn", 323), ("auto", 323)]) def test_scan_yarn_success(mode: str, num_components: int) -> None: - ret = scan("test/yarn_simple_math", mode=mode, local_scan=True) + ret = scan("test/test_packages/yarn_simple_math", mode=mode, local_scan=True) assert isinstance(ret, OSSBOM) assert len(ret.get_components()) == num_components @@ -70,16 +70,21 @@ def test_scan_yarn_success(mode: str, num_components: int) -> None: def test_scan_failure() -> None: with pytest.raises(Exception) as excinfo: scan( - "test/python_simple_math_no_exist", + "test/test_packages/python_simple_math_no_exist", mode="python-requirements", local_scan=True, ) - assert "Package test/python_simple_math_no_exist does not exist" in str( - excinfo.value + assert ( + "Package test/test_packages/python_simple_math_no_exist does not exist" + in str(excinfo.value) ) def test_scan_invalid_mode() -> None: with pytest.raises(Exception) as excinfo: - scan("test/python_simple_math", mode="invalid-mode", local_scan=True) + scan( + "test/test_packages/python_simple_math", + mode="invalid-mode", + local_scan=True, + ) assert "Invalid scanning method" in str(excinfo.value) diff --git a/ossprey/test_virtualenv.py b/ossprey/test_virtualenv.py index 04145ae..571c797 100644 --- a/ossprey/test_virtualenv.py +++ b/ossprey/test_virtualenv.py @@ -10,7 +10,7 @@ def test_virtual_env_creation() -> None: venv = VirtualEnv() # Check if the virtual environment files exist - assert os.path.isdir(os.path.join(venv.get_venv_dir(), 'bin')) + assert os.path.isdir(os.path.join(venv.get_venv_dir(), "bin")) def test_accessing_virtual_env() -> None: @@ -24,7 +24,7 @@ def test_accessing_virtual_env() -> None: # Make sure we are in a new venv assert len(installed_packages) == 1 - assert installed_packages[0]['name'] == 'pip' + assert installed_packages[0]["name"] == "pip" def test_package_installation() -> None: @@ -32,27 +32,27 @@ def test_package_installation() -> None: venv.enter() # Install a package - venv.install_package('numpy') + venv.install_package("numpy") installed_packages = venv.list_installed_packages() # Make sure we are in a new venv assert len(installed_packages) == 2 - assert any(map(lambda x: x['name'] == 'numpy', installed_packages)) + assert any(map(lambda x: x["name"] == "numpy", installed_packages)) def test_local_requirements_file() -> None: venv = VirtualEnv() venv.enter() - venv.install_package('numpy') + venv.install_package("numpy") requirements_file = venv.create_requirements_file_from_env() # Read the file - with open(requirements_file, 'r') as f: + with open(requirements_file, "r") as f: lines = f.readlines() assert len(lines) == 1 - assert lines[0].startswith('numpy') + assert lines[0].startswith("numpy") def test_local_installation() -> None: @@ -60,10 +60,10 @@ def test_local_installation() -> None: venv.enter() # Install a package - venv.install_package('test/python_simple_math') + venv.install_package("test/test_packages/python_simple_math") installed_packages = venv.list_installed_packages() # Make sure we are in a new venv assert len(installed_packages) == 8 - assert any(map(lambda x: x['name'] == 'numpy', installed_packages)) + assert any(map(lambda x: x["name"] == "numpy", installed_packages)) diff --git a/ossprey/virtualenv.py b/ossprey/virtualenv.py index bfc4512..0741a1f 100644 --- a/ossprey/virtualenv.py +++ b/ossprey/virtualenv.py @@ -125,6 +125,14 @@ def create_requirements_file_from_env(self) -> str: return requirements_file.name + def __enter__(self): + self.enter() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.exit() + return False + def enter(self) -> None: """ Replaces sys.path with paths from the specified virtual environment. diff --git a/test/smoke/conftest.py b/test/smoke/conftest.py new file mode 100644 index 0000000..21c2474 --- /dev/null +++ b/test/smoke/conftest.py @@ -0,0 +1,31 @@ +"""Fixtures and markers for ossprey smoke tests.""" + +from __future__ import annotations + +import os +import pytest +from pathlib import Path + + +def pytest_configure(config): + config.addinivalue_line("markers", "smoke: mark test as a smoke test") + config.addinivalue_line("markers", "slow: mark test as slow (installs packages)") + config.addinivalue_line("markers", "network: mark test as requiring network access") + + +@pytest.fixture +def project_root() -> Path: + """Return the absolute path to the project root.""" + return Path(__file__).resolve().parent.parent.parent + + +@pytest.fixture +def test_packages_dir(project_root) -> Path: + """Return the absolute path to test/test_packages/.""" + return project_root / "test" / "test_packages" + + +@pytest.fixture +def has_api_key() -> bool: + """Check if API_KEY env var is set.""" + return bool(os.environ.get("API_KEY")) diff --git a/test/smoke/test_smoke.py b/test/smoke/test_smoke.py new file mode 100644 index 0000000..1ad9903 --- /dev/null +++ b/test/smoke/test_smoke.py @@ -0,0 +1,459 @@ +""" +Smoke tests for ossprey — end-to-end CLI verification. + +Invokes `python -m ossprey` via subprocess to exercise the full scanning pipeline. + +By default uses --dry-run-safe (no API key needed). +If API_KEY env var is set, also runs a real API scan pass. + +Usage: + poetry run pytest test/smoke/ -v + poetry run pytest test/smoke/ -v -m "smoke and not slow" + poetry run pytest test/smoke/ -v -m "smoke and not network" + API_KEY= poetry run pytest test/smoke/ -v +""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +import textwrap +from pathlib import Path + +import pytest + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +PYTHON = sys.executable + + +def run_ossprey( + package_dir: str | Path, + *, + mode: str = "auto", + dry_run: str = "safe", + output_file: str | Path | None = None, + expect_exit: int = 0, + extra_args: list[str] | None = None, + timeout: int = 300, +) -> subprocess.CompletedProcess: + """Run ``python -m ossprey`` as a subprocess and assert the exit code. + + Parameters + ---------- + package_dir: + Directory to scan. + mode: + Scanning mode (auto, pipenv, python-requirements, poetry, npm, yarn, fs). + dry_run: + "safe", "malicious", or None. When None and API_KEY is available a + real API scan is made, otherwise falls back to "safe". + output_file: + If provided, pass ``-o `` so ossprey writes the SBOM JSON. + expect_exit: + Expected process exit code. + extra_args: + Additional CLI flags. + timeout: + Subprocess timeout in seconds. + """ + cmd = [PYTHON, "-m", "ossprey", "--package", str(package_dir), "--mode", mode, "-v"] + + if dry_run == "safe": + cmd.append("--dry-run-safe") + elif dry_run == "malicious": + cmd.append("--dry-run-malicious") + else: + # Real API scan — requires API_KEY + api_key = os.environ.get("API_KEY") + if api_key: + cmd.extend(["--api-key", api_key]) + else: + # Fall back to dry-run-safe when no key is present + cmd.append("--dry-run-safe") + + if output_file is not None: + cmd.extend(["-o", str(output_file)]) + + if extra_args: + cmd.extend(extra_args) + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=timeout, + cwd=str(Path(__file__).resolve().parent.parent.parent), # project root + ) + + assert result.returncode == expect_exit, ( + f"ossprey exited {result.returncode}, expected {expect_exit}.\n" + f"CMD: {' '.join(cmd)}\n" + f"STDOUT:\n{result.stdout[-2000:]}\n" + f"STDERR:\n{result.stderr[-2000:]}" + ) + return result + + +def assert_sbom_has_components(sbom_path: Path, min_components: int = 1): + """Load an SBOM JSON file and assert it contains components.""" + assert sbom_path.exists(), f"SBOM output file not found: {sbom_path}" + data = json.loads(sbom_path.read_text()) + components = data.get("components", []) + assert ( + len(components) >= min_components + ), f"Expected >= {min_components} components, got {len(components)} in {sbom_path}" + return data + + +def maybe_run_api_scan(package_dir: str | Path, output_file: Path | None = None): + """If API_KEY is set, also run a real API scan (non-dry-run).""" + if os.environ.get("API_KEY"): + run_ossprey(package_dir, dry_run=None, output_file=output_file) + + +# --------------------------------------------------------------------------- +# 1. Scan all existing test_packages +# --------------------------------------------------------------------------- + +EXISTING_PACKAGES = [ + "python_simple_math", + "poetry_simple_math", + "poetry_broken_simple_math", + "npm_simple_math", + "yarn_simple_math", + "yarn_massive_math", +] + + +@pytest.mark.smoke +@pytest.mark.parametrize("pkg_name", EXISTING_PACKAGES) +def test_existing_test_packages(pkg_name, test_packages_dir, tmp_path): + """Scan each non-Docker test package and verify a clean SBOM is produced.""" + pkg_dir = test_packages_dir / pkg_name + assert pkg_dir.is_dir(), f"Test package not found: {pkg_dir}" + + sbom_file = tmp_path / "sbom.json" + result = run_ossprey(pkg_dir, output_file=sbom_file) + + # Verify stdout reports clean + assert "No malware found" in result.stdout + + # Verify SBOM file has components + assert_sbom_has_components(sbom_file) + + # Optionally exercise real API + maybe_run_api_scan(pkg_dir) + + +# --------------------------------------------------------------------------- +# 2. Dry-run malicious path +# --------------------------------------------------------------------------- + + +@pytest.mark.smoke +def test_dry_run_malicious(test_packages_dir, tmp_path): + """Verify --dry-run-malicious injects a vulnerability and exits 1.""" + pkg_dir = test_packages_dir / "python_simple_math" + sbom_file = tmp_path / "sbom.json" + + result = run_ossprey( + pkg_dir, + dry_run="malicious", + output_file=sbom_file, + expect_exit=1, + ) + + assert "malware" in result.stdout.lower() or "warning" in result.stdout.lower() + + # SBOM should contain a vulnerability + data = json.loads(sbom_file.read_text()) + vulns = data.get("vulnerabilities", []) + assert len(vulns) >= 1, "Expected at least 1 vulnerability in malicious dry-run" + + +# --------------------------------------------------------------------------- +# 3. Installable test packages (pip-installable projects scanned via auto) +# --------------------------------------------------------------------------- + +INSTALLABLE_PACKAGES = [ + "python_simple_math", # has setup.py + requirements.txt + "poetry_simple_math", # has pyproject.toml with build-system +] + + +@pytest.mark.smoke +@pytest.mark.slow +@pytest.mark.parametrize("pkg_name", INSTALLABLE_PACKAGES) +def test_installable_existing_packages(pkg_name, test_packages_dir, tmp_path): + """Scan pip-installable test packages via auto mode.""" + pkg_dir = test_packages_dir / pkg_name + sbom_file = tmp_path / "sbom.json" + + result = run_ossprey(pkg_dir, output_file=sbom_file, timeout=600) + + assert "No malware found" in result.stdout + assert_sbom_has_components(sbom_file, min_components=2) + + +# --------------------------------------------------------------------------- +# 4. Real-world Python packages (requirements.txt scanning) +# --------------------------------------------------------------------------- + +REAL_PYTHON_PACKAGES = [ + ("flask", "flask==3.0.0"), + ("django", "django==5.0"), + ("fastapi", "fastapi==0.109.0"), + ("boto3", "boto3==1.34.0"), + ("requests", "requests==2.31.0"), + ("numpy", "numpy==1.26.3"), +] + + +@pytest.mark.smoke +@pytest.mark.network +@pytest.mark.parametrize( + "name, spec", REAL_PYTHON_PACKAGES, ids=[p[0] for p in REAL_PYTHON_PACKAGES] +) +def test_real_world_python_requirements(name, spec, tmp_path): + """Create a requirements.txt with a real package and scan it.""" + pkg_dir = tmp_path / name + pkg_dir.mkdir() + (pkg_dir / "requirements.txt").write_text(spec + "\n") + + sbom_file = tmp_path / f"{name}_sbom.json" + result = run_ossprey(pkg_dir, output_file=sbom_file) + + assert "No malware found" in result.stdout + data = assert_sbom_has_components(sbom_file) + + # The named package should appear in components + comp_names = [c.get("name", "").lower() for c in data["components"]] + assert name in comp_names, f"Expected '{name}' in components: {comp_names}" + + +# --------------------------------------------------------------------------- +# 5. Real-world Python packages (installable — full install via auto) +# --------------------------------------------------------------------------- + +REAL_INSTALLABLE_PACKAGES = [ + ("flask_pkg", ["flask==3.0.0"], "flask"), + ("django_pkg", ["django==5.0"], "django"), + ("requests_numpy_pkg", ["requests==2.31.0", "numpy==1.26.3"], "requests"), + ("boto3_pkg", ["boto3==1.34.0"], "boto3"), + ("fastapi_pkg", ["fastapi==0.109.0"], "fastapi"), +] + + +def _create_installable_package( + base_dir: Path, name: str, install_requires: list[str] +) -> Path: + """Create a minimal pip-installable package with a requirements.txt for auto-detection.""" + pkg_dir = base_dir / name + pkg_dir.mkdir() + + inner_pkg = pkg_dir / "mypkg" + inner_pkg.mkdir() + (inner_pkg / "__init__.py").write_text("") + + deps_str = ", ".join(f'"{d}"' for d in install_requires) + setup_py = textwrap.dedent( + f"""\ + from setuptools import setup, find_packages + setup( + name="{name}", + version="0.0.1", + packages=find_packages(), + install_requires=[{deps_str}], + ) + """ + ) + (pkg_dir / "setup.py").write_text(setup_py) + + # Also write requirements.txt so auto mode detects python-requirements + (pkg_dir / "requirements.txt").write_text("\n".join(install_requires) + "\n") + + return pkg_dir + + +@pytest.mark.smoke +@pytest.mark.slow +@pytest.mark.network +@pytest.mark.parametrize( + "name, deps, expected_component", + REAL_INSTALLABLE_PACKAGES, + ids=[p[0] for p in REAL_INSTALLABLE_PACKAGES], +) +def test_real_world_installable_packages(name, deps, expected_component, tmp_path): + """Create a minimal installable package with requirements.txt, scan via auto mode.""" + pkg_dir = _create_installable_package(tmp_path, name, deps) + sbom_file = tmp_path / f"{name}_sbom.json" + + result = run_ossprey(pkg_dir, output_file=sbom_file, timeout=600) + + assert "No malware found" in result.stdout + + data = assert_sbom_has_components(sbom_file) + comp_names = [c.get("name", "").lower() for c in data["components"]] + assert ( + expected_component in comp_names + ), f"Expected '{expected_component}' in components: {comp_names}" + + +# --------------------------------------------------------------------------- +# 6. Real-world npm packages +# --------------------------------------------------------------------------- + +REAL_NPM_PACKAGES = [ + ("express", {"express": "4.18.2"}), + ("react", {"react": "18.2.0"}), + ("axios", {"axios": "1.6.5"}), + ("lodash", {"lodash": "4.17.21"}), + ("typescript", {"typescript": "5.3.3"}), +] + + +def _create_npm_project(base_dir: Path, name: str, dependencies: dict) -> Path: + """Create a directory with a package.json and run npm install.""" + pkg_dir = base_dir / name + pkg_dir.mkdir() + + package_json = { + "name": f"smoke-test-{name}", + "version": "1.0.0", + "private": True, + "dependencies": dependencies, + } + (pkg_dir / "package.json").write_text(json.dumps(package_json, indent=2)) + + # npm install to generate package-lock.json and node_modules + subprocess.run( + ["npm", "install", "--ignore-scripts"], + cwd=str(pkg_dir), + check=True, + capture_output=True, + text=True, + timeout=120, + ) + return pkg_dir + + +@pytest.mark.smoke +@pytest.mark.network +@pytest.mark.parametrize( + "name, deps", REAL_NPM_PACKAGES, ids=[p[0] for p in REAL_NPM_PACKAGES] +) +def test_real_world_npm_packages(name, deps, tmp_path): + """Install a real npm package and scan it.""" + pkg_dir = _create_npm_project(tmp_path, name, deps) + sbom_file = tmp_path / f"{name}_sbom.json" + + result = run_ossprey(pkg_dir, output_file=sbom_file) + + assert "No malware found" in result.stdout + data = assert_sbom_has_components(sbom_file) + + comp_names = [c.get("name", "").lower() for c in data["components"]] + assert name in comp_names, f"Expected '{name}' in components: {comp_names}" + + +# --------------------------------------------------------------------------- +# 7. Real-world GitHub repos +# --------------------------------------------------------------------------- + +GITHUB_REPOS = [ + # (repo_url, pre_scan_cmd, expected_component) + ( + "https://github.com/pallets/click", + None, + # click itself won't appear — it's the root package; check a known dep + "colorama", + ), + ( + "https://github.com/expressjs/express", + ["npm", "install", "--ignore-scripts"], + # express itself won't appear — it's the root package; check a known dep + "body-parser", + ), + ( + "https://github.com/psf/requests", + None, + "requests", + ), +] + + +def _clone_repo(url: str, dest: Path, timeout: int = 120) -> Path: + """Shallow-clone a GitHub repo.""" + subprocess.run( + ["git", "clone", "--depth", "1", url, str(dest)], + check=True, + capture_output=True, + text=True, + timeout=timeout, + ) + return dest + + +@pytest.mark.smoke +@pytest.mark.slow +@pytest.mark.network +@pytest.mark.parametrize( + "repo_url, pre_cmd, expected_component", + GITHUB_REPOS, + ids=[r[0].split("/")[-1] for r in GITHUB_REPOS], +) +def test_real_world_github_repos(repo_url, pre_cmd, expected_component, tmp_path): + """Clone a GitHub repo and scan it end-to-end.""" + repo_name = repo_url.rstrip("/").split("/")[-1] + repo_dir = tmp_path / repo_name + + _clone_repo(repo_url, repo_dir) + + # Run any prerequisite command (e.g. npm install) + if pre_cmd: + subprocess.run( + pre_cmd, + cwd=str(repo_dir), + check=True, + capture_output=True, + text=True, + timeout=180, + ) + + sbom_file = tmp_path / f"{repo_name}_sbom.json" + result = run_ossprey(repo_dir, output_file=sbom_file, timeout=600) + + assert "No malware found" in result.stdout + + data = assert_sbom_has_components(sbom_file) + comp_names = [c.get("name", "").lower() for c in data["components"]] + assert ( + expected_component in comp_names + ), f"Expected '{expected_component}' in components: {comp_names}" + + +# --------------------------------------------------------------------------- +# 8. Combined scan — verify API path if key is available +# --------------------------------------------------------------------------- + + +@pytest.mark.smoke +@pytest.mark.network +def test_api_scan_if_key_available(test_packages_dir, tmp_path): + """If API_KEY is set, run a full API scan on python_simple_math.""" + api_key = os.environ.get("API_KEY") + if not api_key: + pytest.skip("API_KEY not set — skipping real API scan test") + + pkg_dir = test_packages_dir / "python_simple_math" + sbom_file = tmp_path / "api_sbom.json" + + result = run_ossprey(pkg_dir, dry_run=None, output_file=sbom_file) + + assert "No malware found" in result.stdout + assert_sbom_has_components(sbom_file) diff --git a/test/docker_js_simple_math/Dockerfile b/test/test_packages/docker_js_simple_math/Dockerfile similarity index 100% rename from test/docker_js_simple_math/Dockerfile rename to test/test_packages/docker_js_simple_math/Dockerfile diff --git a/test/docker_js_simple_math/build.sh b/test/test_packages/docker_js_simple_math/build.sh similarity index 100% rename from test/docker_js_simple_math/build.sh rename to test/test_packages/docker_js_simple_math/build.sh diff --git a/test/docker_js_simple_math/javascript/.gitignore b/test/test_packages/docker_js_simple_math/javascript/.gitignore similarity index 100% rename from test/docker_js_simple_math/javascript/.gitignore rename to test/test_packages/docker_js_simple_math/javascript/.gitignore diff --git a/test/docker_js_simple_math/javascript/README.md b/test/test_packages/docker_js_simple_math/javascript/README.md similarity index 100% rename from test/docker_js_simple_math/javascript/README.md rename to test/test_packages/docker_js_simple_math/javascript/README.md diff --git a/test/docker_js_simple_math/javascript/package-lock.json b/test/test_packages/docker_js_simple_math/javascript/package-lock.json similarity index 100% rename from test/docker_js_simple_math/javascript/package-lock.json rename to test/test_packages/docker_js_simple_math/javascript/package-lock.json diff --git a/test/docker_js_simple_math/javascript/package.json b/test/test_packages/docker_js_simple_math/javascript/package.json similarity index 100% rename from test/docker_js_simple_math/javascript/package.json rename to test/test_packages/docker_js_simple_math/javascript/package.json diff --git a/test/docker_js_simple_math/javascript/src/index.js b/test/test_packages/docker_js_simple_math/javascript/src/index.js similarity index 100% rename from test/docker_js_simple_math/javascript/src/index.js rename to test/test_packages/docker_js_simple_math/javascript/src/index.js diff --git a/test/docker_js_simple_math/javascript/test/simpleMath.test.js b/test/test_packages/docker_js_simple_math/javascript/test/simpleMath.test.js similarity index 100% rename from test/docker_js_simple_math/javascript/test/simpleMath.test.js rename to test/test_packages/docker_js_simple_math/javascript/test/simpleMath.test.js diff --git a/test/docker_js_simple_math/javascript/yarn.lock b/test/test_packages/docker_js_simple_math/javascript/yarn.lock similarity index 100% rename from test/docker_js_simple_math/javascript/yarn.lock rename to test/test_packages/docker_js_simple_math/javascript/yarn.lock diff --git a/test/docker_py_simple_math/Dockerfile b/test/test_packages/docker_py_simple_math/Dockerfile similarity index 100% rename from test/docker_py_simple_math/Dockerfile rename to test/test_packages/docker_py_simple_math/Dockerfile diff --git a/test/docker_py_simple_math/build.sh b/test/test_packages/docker_py_simple_math/build.sh similarity index 100% rename from test/docker_py_simple_math/build.sh rename to test/test_packages/docker_py_simple_math/build.sh diff --git a/test/docker_py_simple_math/python/README.md b/test/test_packages/docker_py_simple_math/python/README.md similarity index 100% rename from test/docker_py_simple_math/python/README.md rename to test/test_packages/docker_py_simple_math/python/README.md diff --git a/test/docker_py_simple_math/python/main.py b/test/test_packages/docker_py_simple_math/python/main.py similarity index 100% rename from test/docker_py_simple_math/python/main.py rename to test/test_packages/docker_py_simple_math/python/main.py diff --git a/test/docker_py_simple_math/python/requirements.txt b/test/test_packages/docker_py_simple_math/python/requirements.txt similarity index 100% rename from test/docker_py_simple_math/python/requirements.txt rename to test/test_packages/docker_py_simple_math/python/requirements.txt diff --git a/test/npm_simple_math/.gitignore b/test/test_packages/npm_simple_math/.gitignore similarity index 100% rename from test/npm_simple_math/.gitignore rename to test/test_packages/npm_simple_math/.gitignore diff --git a/test/npm_simple_math/README.md b/test/test_packages/npm_simple_math/README.md similarity index 100% rename from test/npm_simple_math/README.md rename to test/test_packages/npm_simple_math/README.md diff --git a/test/npm_simple_math/package-lock.json b/test/test_packages/npm_simple_math/package-lock.json similarity index 100% rename from test/npm_simple_math/package-lock.json rename to test/test_packages/npm_simple_math/package-lock.json diff --git a/test/npm_simple_math/package.json b/test/test_packages/npm_simple_math/package.json similarity index 100% rename from test/npm_simple_math/package.json rename to test/test_packages/npm_simple_math/package.json diff --git a/test/npm_simple_math/src/index.js b/test/test_packages/npm_simple_math/src/index.js similarity index 100% rename from test/npm_simple_math/src/index.js rename to test/test_packages/npm_simple_math/src/index.js diff --git a/test/npm_simple_math/test/simpleMath.test.js b/test/test_packages/npm_simple_math/test/simpleMath.test.js similarity index 100% rename from test/npm_simple_math/test/simpleMath.test.js rename to test/test_packages/npm_simple_math/test/simpleMath.test.js diff --git a/test/poetry_simple_math/.gitignore b/test/test_packages/poetry_broken_simple_math/.gitignore similarity index 100% rename from test/poetry_simple_math/.gitignore rename to test/test_packages/poetry_broken_simple_math/.gitignore diff --git a/test/poetry_simple_math/README.md b/test/test_packages/poetry_broken_simple_math/README.md similarity index 100% rename from test/poetry_simple_math/README.md rename to test/test_packages/poetry_broken_simple_math/README.md diff --git a/test/poetry_simple_math/poetry_simple_math/__init__.py b/test/test_packages/poetry_broken_simple_math/poetry_simple_math/__init__.py similarity index 100% rename from test/poetry_simple_math/poetry_simple_math/__init__.py rename to test/test_packages/poetry_broken_simple_math/poetry_simple_math/__init__.py diff --git a/test/poetry_simple_math/poetry_simple_math/operations.py b/test/test_packages/poetry_broken_simple_math/poetry_simple_math/operations.py similarity index 100% rename from test/poetry_simple_math/poetry_simple_math/operations.py rename to test/test_packages/poetry_broken_simple_math/poetry_simple_math/operations.py diff --git a/test/test_packages/poetry_broken_simple_math/pyproject.toml b/test/test_packages/poetry_broken_simple_math/pyproject.toml new file mode 100644 index 0000000..bd95d31 --- /dev/null +++ b/test/test_packages/poetry_broken_simple_math/pyproject.toml @@ -0,0 +1,62 @@ +[tool.coverage.run] +source = ["transformers"] +omit = [ + "*/convert_*", + "*/__main__.py" +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "raise", + "except", + "register_parameter" +] + +[tool.ruff] +target-version = "py39" +line-length = 119 + +[tool.ruff.lint] +# Never enforce `E501` (line length violations). +ignore = ["C901", "E501", "E741", "F402", "F823"] +# RUF013: Checks for the use of implicit Optional +# in type annotations when the default parameter value is None. +select = ["C", "E", "F", "I", "W", "RUF013", "UP006", "PERF102", "PLC1802", "PLC0208"] +extend-safe-fixes = ["UP006"] + +# Ignore import violations in all `__init__.py` files. +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["E402", "F401", "F403", "F811"] +"src/transformers/file_utils.py" = ["F401"] +"src/transformers/utils/dummy_*.py" = ["F401"] + +[tool.ruff.lint.isort] +lines-after-imports = 2 +known-first-party = ["transformers"] + +[tool.ruff.format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +[tool.pytest.ini_options] +addopts = "--doctest-glob='**/*.md'" +doctest_optionflags="NUMBER NORMALIZE_WHITESPACE ELLIPSIS" +markers = [ + "flash_attn_3_test: marks tests related to flash attention 3 (deselect with '-m \"not flash_attn_3_test\"')", + "flash_attn_test: marks tests related to flash attention (deselect with '-m \"not flash_attn_test\"')", + "bitsandbytes: select (or deselect with `not`) bitsandbytes integration tests", + "generate: marks tests that use the GenerationTesterMixin" +] +log_cli = 1 +log_cli_level = "WARNING" +asyncio_default_fixture_loop_scope = "function" \ No newline at end of file diff --git a/test/python_simple_math/setup.py b/test/test_packages/poetry_broken_simple_math/setup.py similarity index 100% rename from test/python_simple_math/setup.py rename to test/test_packages/poetry_broken_simple_math/setup.py diff --git a/test/test_packages/poetry_simple_math/.gitignore b/test/test_packages/poetry_simple_math/.gitignore new file mode 100644 index 0000000..c04bc49 --- /dev/null +++ b/test/test_packages/poetry_simple_math/.gitignore @@ -0,0 +1 @@ +poetry.lock diff --git a/test/python_simple_math/simple_math/__init__.py b/test/test_packages/poetry_simple_math/README.md similarity index 100% rename from test/python_simple_math/simple_math/__init__.py rename to test/test_packages/poetry_simple_math/README.md diff --git a/test/test_packages/poetry_simple_math/poetry_simple_math/__init__.py b/test/test_packages/poetry_simple_math/poetry_simple_math/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/python_simple_math/simple_math/operations.py b/test/test_packages/poetry_simple_math/poetry_simple_math/operations.py similarity index 100% rename from test/python_simple_math/simple_math/operations.py rename to test/test_packages/poetry_simple_math/poetry_simple_math/operations.py diff --git a/test/poetry_simple_math/pyproject.toml b/test/test_packages/poetry_simple_math/pyproject.toml similarity index 100% rename from test/poetry_simple_math/pyproject.toml rename to test/test_packages/poetry_simple_math/pyproject.toml diff --git a/test/python_simple_math/requirements.txt b/test/test_packages/python_simple_math/requirements.txt similarity index 100% rename from test/python_simple_math/requirements.txt rename to test/test_packages/python_simple_math/requirements.txt diff --git a/test/test_packages/python_simple_math/setup.py b/test/test_packages/python_simple_math/setup.py new file mode 100644 index 0000000..6a04527 --- /dev/null +++ b/test/test_packages/python_simple_math/setup.py @@ -0,0 +1,18 @@ +# setup.py + +from setuptools import setup, find_packages + +setup( + name="simple_math", + version="0.1.0", + packages=find_packages(), + install_requires=[ + "requests==2.26.0", + "numpy==2.0.0" + ], + entry_points={ + 'console_scripts': [ + 'simple-math=simple_math.operations:add', + ], + }, +) diff --git a/test/test_packages/python_simple_math/simple_math/__init__.py b/test/test_packages/python_simple_math/simple_math/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_packages/python_simple_math/simple_math/operations.py b/test/test_packages/python_simple_math/simple_math/operations.py new file mode 100644 index 0000000..cd37a75 --- /dev/null +++ b/test/test_packages/python_simple_math/simple_math/operations.py @@ -0,0 +1,23 @@ +from __future__ import annotations +# simple_math/operations.py +from typing import Union + +Number = Union[int, float] + + +def add(a: Number, b: Number) -> Number: + return a + b + + +def subtract(a: Number, b: Number) -> Number: + return a - b + + +def multiply(a: Number, b: Number) -> Number: + return a * b + + +def divide(a: Number, b: Number) -> float: + if b == 0: + raise ValueError("Cannot divide by zero") + return a / b diff --git a/test/yarn_massive_math/.gitignore b/test/test_packages/yarn_massive_math/.gitignore similarity index 100% rename from test/yarn_massive_math/.gitignore rename to test/test_packages/yarn_massive_math/.gitignore diff --git a/test/yarn_massive_math/README.md b/test/test_packages/yarn_massive_math/README.md similarity index 100% rename from test/yarn_massive_math/README.md rename to test/test_packages/yarn_massive_math/README.md diff --git a/test/yarn_massive_math/package.json b/test/test_packages/yarn_massive_math/package.json similarity index 100% rename from test/yarn_massive_math/package.json rename to test/test_packages/yarn_massive_math/package.json diff --git a/test/yarn_massive_math/src/index.js b/test/test_packages/yarn_massive_math/src/index.js similarity index 100% rename from test/yarn_massive_math/src/index.js rename to test/test_packages/yarn_massive_math/src/index.js diff --git a/test/yarn_massive_math/test/simpleMath.test.js b/test/test_packages/yarn_massive_math/test/simpleMath.test.js similarity index 100% rename from test/yarn_massive_math/test/simpleMath.test.js rename to test/test_packages/yarn_massive_math/test/simpleMath.test.js diff --git a/test/yarn_massive_math/yarn.lock b/test/test_packages/yarn_massive_math/yarn.lock similarity index 100% rename from test/yarn_massive_math/yarn.lock rename to test/test_packages/yarn_massive_math/yarn.lock diff --git a/test/yarn_simple_math/.gitignore b/test/test_packages/yarn_simple_math/.gitignore similarity index 100% rename from test/yarn_simple_math/.gitignore rename to test/test_packages/yarn_simple_math/.gitignore diff --git a/test/yarn_simple_math/README.md b/test/test_packages/yarn_simple_math/README.md similarity index 100% rename from test/yarn_simple_math/README.md rename to test/test_packages/yarn_simple_math/README.md diff --git a/test/yarn_simple_math/package.json b/test/test_packages/yarn_simple_math/package.json similarity index 100% rename from test/yarn_simple_math/package.json rename to test/test_packages/yarn_simple_math/package.json diff --git a/test/yarn_simple_math/src/index.js b/test/test_packages/yarn_simple_math/src/index.js similarity index 100% rename from test/yarn_simple_math/src/index.js rename to test/test_packages/yarn_simple_math/src/index.js diff --git a/test/yarn_simple_math/test/simpleMath.test.js b/test/test_packages/yarn_simple_math/test/simpleMath.test.js similarity index 100% rename from test/yarn_simple_math/test/simpleMath.test.js rename to test/test_packages/yarn_simple_math/test/simpleMath.test.js diff --git a/test/yarn_simple_math/yarn.lock b/test/test_packages/yarn_simple_math/yarn.lock similarity index 100% rename from test/yarn_simple_math/yarn.lock rename to test/test_packages/yarn_simple_math/yarn.lock