diff --git a/.github/workflows/foss_cli_tests.yml b/.github/workflows/foss_cli_tests.yml index f58964b..509ad23 100644 --- a/.github/workflows/foss_cli_tests.yml +++ b/.github/workflows/foss_cli_tests.yml @@ -28,7 +28,7 @@ jobs: - name: Install host dependencies run: | apt-get -qq update - apt-get install -qq gcc git nmap xz-utils + apt-get install -qq bzip2 gcc git nmap xz-utils rm -rf /var/lib/apt/lists/* - name: Install Python dependencies run: | @@ -36,7 +36,7 @@ jobs: poetry install --with=dev - name: Install files in shared volume run: | - tar xJf tests/files/base-files_11.tar.xz -C /tmp + tar xvf tests/files/base-files_10.3-debian10-test.tar.bz2 -C /tmp - name: Check services run: nmap fossology -p 80 - name: Run tests diff --git a/.github/workflows/fossologytests.yml b/.github/workflows/fossologytests.yml index 1495ab7..9bb8942 100644 --- a/.github/workflows/fossologytests.yml +++ b/.github/workflows/fossologytests.yml @@ -28,7 +28,7 @@ jobs: - name: Install host dependencies run: | apt-get -qq update - apt-get install -qq gcc git nmap xz-utils + apt-get install -qq bzip2 curl gcc git nmap xz-utils rm -rf /var/lib/apt/lists/* - name: Install Python dependencies run: | @@ -36,12 +36,25 @@ jobs: poetry install --with=dev - name: Install files in shared volume run: | - tar xJf tests/files/base-files_11.tar.xz -C /tmp + tar xvf tests/files/base-files_10.3-debian10-test.tar.bz2 -C /tmp - name: Check services - run: nmap fossology -p 80 + run: | + nmap fossology -p 80 + for i in $(seq 1 60); do + if curl --fail --silent --show-error http://fossology/repo/api/v1/info > /dev/null; then + echo "Fossology API is up" + break + fi + if [ "$i" -eq 60 ]; then + echo "Fossology API did not become ready in time" >&2 + exit 1 + fi + echo "Waiting for Fossology API... attempt $i/60" + sleep 5 + done - name: Run tests run: | - poetry run coverage run --source=fossology -m pytest + poetry run coverage run --source=fossology -m pytest --ignore-glob="tests/test_foss_cli*.py" poetry run coverage report -m - name: upload codecoverage results only if the PR originates from the upstream repository if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} diff --git a/README.rst b/README.rst index f7fb2f4..04ce3be 100644 --- a/README.rst +++ b/README.rst @@ -148,7 +148,7 @@ Fossology Python also offers a command line interface to simplify interactions w .. code:: bash - $ foss_cli -vv upload_file tests/files/zlib_1.2.11.dfsg-0ubuntu2.debian.tar.xz \ + $ foss_cli -vv upload_file tests/files/base-files_10.3-debian10-test.tar.bz2 \ --folder_name FossFolder --access_level public @@ -200,6 +200,25 @@ Develop To avoid running the whole testsuite during development of a new branch with changing only touching the code related to the CLI, name your branch ``feat/cli-{something}`` and only the ``test_foss_cli_*`` will run in the pull request context. +**Testing Requirements for New Code** + + When contributing new features or API extensions, ensure comprehensive test coverage using these guidelines: + + - **Unit Tests with Responses Framework**: Use the `responses `_ library + to mock HTTP requests and provide unit test coverage for all code paths. This allows fast, isolated testing without + requiring a running Fossology instance. See existing tests in ``tests/`` directory for examples of the + ``@responses.activate`` decorator pattern. + + - **Integration Tests**: Provide at least one generic integration test that runs against an actual Fossology instance. + These tests validate real API behavior and can be executed: + + - **Locally**: Run against a Fossology Docker container (see the Test section below for setup instructions) + - **In CI/CD**: Tests automatically run via GitHub Actions in the `API Tests job + `_ + + - **Coverage Requirements**: All new code paths must be covered by tests. Pull requests without adequate test + coverage will not be accepted. Use ``poetry run coverage`` to verify coverage locally. + **AI-Assisted Contributions** AI coding agents are active in this repository. If you are an AI agent or using AI-assisted tooling, @@ -275,7 +294,8 @@ The testsuite available in this project expects a running Fossology instance und .. code:: shell docker pull fossology/fossology - tar xJf tests/files/base-files_11.tar.xz -C /tmp + tar xvf tests/files/base-files_10.3-debian10-test.tar.bz2 -C /tmp + chmod a+r /tmp/base-files-10.3 docker run --mount src="/tmp",dst=/tmp,type=bind --name fossology -p 80:80 fossology/fossology - Start the complete test suite or a specific test case (and generate coverage report): diff --git a/docs-source/conf.py b/docs-source/conf.py index 4899add..653c5f1 100644 --- a/docs-source/conf.py +++ b/docs-source/conf.py @@ -22,7 +22,7 @@ copyright = "2021, Siemens AG" # The full version, including major/minor/patch tags -release = "3.5.0" +release = "3.6.0" # -- General configuration --------------------------------------------------- diff --git a/docs-source/sample_workflow.rst b/docs-source/sample_workflow.rst index 3c0fc34..dc4d197 100644 --- a/docs-source/sample_workflow.rst +++ b/docs-source/sample_workflow.rst @@ -111,10 +111,10 @@ We first get an example file from our github repository test environment and the upload it to the server. ->>> filename = "my_base-files_11.tar.xz" +>>> filename = "my_base-files_10.3-debian10-test.tar.bz2" >>> path_to_upload_file = pathlib.Path.cwd() / filename >>> if not path_to_upload_file.exists(): -... url = "https://github.com/fossology/fossology-python/blob/master/tests/files/base-files_11.tar.xz" +... url = "https://github.com/fossology/fossology-python/blob/master/tests/files/base-files_10.3-debian10-test.tar.bz2" ... r = requests.get(url) ... with open(path_to_upload_file, "wb") as fp: ... len = fp.write(r.content) diff --git a/pyproject.toml b/pyproject.toml index 47f1434..fd220ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ version_toml = [ "pyproject.toml:tool.poetry.version", ] version_variables = [ - "docs-source/conf.py:version", + "docs-source/conf.py:release", ] [tool.poetry.dependencies] diff --git a/tests/conftest.py b/tests/conftest.py index d042dd7..94e3cdf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -185,11 +185,6 @@ def foss_v2(foss_server: str, foss_token: str, foss_agents: Agents): foss.close() -@pytest.fixture(scope="session") -def test_file_path() -> str: - return "tests/files/base-files_11.tar.xz" - - @pytest.fixture(scope="session") def upload_folder(foss: fossology.Fossology) -> Generator: name = "UploadFolderTest" @@ -227,7 +222,7 @@ def upload( time.sleep(5) -@pytest.fixture(scope="function") +@pytest.fixture(scope="session") def upload_v2( foss_v2: fossology.Fossology, test_file_path: str, @@ -248,23 +243,11 @@ def upload_v2( @pytest.fixture(scope="session") def upload_with_jobs( - foss: fossology.Fossology, test_file_path: str, foss_schedule_agents: dict -) -> Generator: - upload = foss.upload_file( - foss.rootFolder, - file=test_file_path, - description="Test upload_with_jobs via fossology-python lib", - access_level=AccessLevel.PUBLIC, - wait_time=5, - ) - if upload: - jobs_lookup(foss, upload) - foss.schedule_jobs(foss.rootFolder, upload, foss_schedule_agents) - jobs_lookup(foss, upload) - yield upload - foss.delete_upload(upload) - time.sleep(5) - + foss: fossology.Fossology, upload: Upload, foss_schedule_agents: dict +): + foss.schedule_jobs(foss.rootFolder, upload, foss_schedule_agents) + jobs_lookup(foss, upload) + return upload @pytest.fixture(scope="session") def created_foss_user(foss: fossology.Fossology, foss_user: dict) -> Generator: @@ -275,15 +258,9 @@ def created_foss_user(foss: fossology.Fossology, foss_user: dict) -> Generator: foss.delete_user(user) -# foss_cli specific @pytest.fixture(scope="session") -def click_test_file_path() -> str: - return "tests/files" - - -@pytest.fixture(scope="session") -def click_test_file() -> str: - return "zlib_1.2.11.dfsg-0ubuntu2.debian.tar.xz" +def test_file_path() -> str: + return "tests/files/base-files_10.3-debian10-test.tar.bz2" @pytest.fixture(scope="session") diff --git a/tests/files/base-files_11.tar.xz b/tests/files/base-files_11.tar.xz deleted file mode 100644 index 1fd74ba..0000000 Binary files a/tests/files/base-files_11.tar.xz and /dev/null differ diff --git a/tests/files/zlib_1.2.11.dfsg-0ubuntu2.debian.tar.xz b/tests/files/zlib_1.2.11.dfsg-0ubuntu2.debian.tar.xz deleted file mode 100644 index 4234c2a..0000000 Binary files a/tests/files/zlib_1.2.11.dfsg-0ubuntu2.debian.tar.xz and /dev/null differ diff --git a/tests/test_foss_cli_flow_cmds.py b/tests/test_foss_cli_flow_cmds.py index ac1d6dd..62c5daf 100644 --- a/tests/test_foss_cli_flow_cmds.py +++ b/tests/test_foss_cli_flow_cmds.py @@ -10,10 +10,10 @@ from fossology import foss_cli -def test_upload_file(runner, click_test_file_path, click_test_file, click_test_dict): +def test_upload_file(runner, test_file_path, click_test_dict): """Test the CLI.""" d = click_test_dict - q_path = PurePath(click_test_file_path, click_test_file) + q_path = PurePath(test_file_path) result = runner.invoke( foss_cli.cli, [ @@ -30,7 +30,7 @@ def test_upload_file(runner, click_test_file_path, click_test_file, click_test_d assert result.exit_code == 0 assert d["VERBOSE"] == 2 assert d["DEBUG"] - assert d["UPLOAD"].uploadname == click_test_file + assert d["UPLOAD"].uploadname == q_path.name assert "Summary of upload" in result.output time.sleep(2) result = runner.invoke( @@ -38,7 +38,7 @@ def test_upload_file(runner, click_test_file_path, click_test_file, click_test_d [ "-vv", "delete_upload", - click_test_file, + q_path.name, ], obj=d, catch_exceptions=False, diff --git a/tests/test_foss_cli_start_workflow.py b/tests/test_foss_cli_start_workflow.py index 2a14445..e356b5f 100644 --- a/tests/test_foss_cli_start_workflow.py +++ b/tests/test_foss_cli_start_workflow.py @@ -9,10 +9,10 @@ def test_start_workflow_calling_with_wrong_report_format_exits_with_1( - runner, click_test_file_path, click_test_file, click_test_dict + runner, test_file_path, click_test_dict ): d = click_test_dict - q_path = PurePath(click_test_file_path, click_test_file) + q_path = PurePath(test_file_path) result = runner.invoke( foss_cli.cli, ["-vv", "start_workflow", str(q_path), "--report_format", "imp"], @@ -24,10 +24,10 @@ def test_start_workflow_calling_with_wrong_report_format_exits_with_1( def test_start_workflow_calling_with_wrong_access_level_exits_with_1( - runner, click_test_file_path, click_test_file, click_test_dict + runner, test_file_path, click_test_dict ): d = click_test_dict - q_path = PurePath(click_test_file_path, click_test_file) + q_path = PurePath(test_file_path) result = runner.invoke( foss_cli.cli, ["-vv", "start_workflow", str(q_path), "--access_level", "imp"], @@ -39,10 +39,10 @@ def test_start_workflow_calling_with_wrong_access_level_exits_with_1( def test_start_workflow_a_dry_run_without_reuse_newest_upload_always_exits_with_1( - runner, click_test_file_path, click_test_file, click_test_dict + runner, test_file_path, click_test_dict ): d = click_test_dict - q_path = PurePath(click_test_file_path, click_test_file) + q_path = PurePath(test_file_path) result = runner.invoke( foss_cli.cli, [ @@ -63,13 +63,13 @@ def test_start_workflow_a_dry_run_without_reuse_newest_upload_always_exits_with_ def test_start_workflow_reuse_newest_job( - runner, click_test_file_path, click_test_file, click_test_dict + runner, test_file_path, click_test_dict ): d = click_test_dict # first upload is the initial one # - it uploads # - it triggers a job on the upload - q_path = PurePath(click_test_file_path, click_test_file) + q_path = PurePath(test_file_path) result = runner.invoke( foss_cli.cli, [ @@ -108,8 +108,8 @@ def test_start_workflow_reuse_newest_job( obj=d, catch_exceptions=False, ) - assert f"Can reuse upload for {click_test_file}" in result.output - assert f"Can reuse old job on Upload {click_test_file}" in result.output + assert f"Can reuse upload for {q_path.name}" in result.output + assert f"Can reuse old job on Upload {q_path.name}" in result.output assert "Generated report" in result.output assert "Report downloaded" in result.output assert "Report written to file: " in result.output @@ -117,7 +117,7 @@ def test_start_workflow_reuse_newest_job( @pytest.fixture(scope="module", autouse=True) -def cleanup_module(runner, click_test_file, click_test_dict): +def cleanup_module(runner, test_file_path, click_test_dict): # Setup code (if any) yield # Teardown code: this runs after all tests in the module @@ -126,7 +126,7 @@ def cleanup_module(runner, click_test_file, click_test_dict): [ "-vv", "delete_upload", - click_test_file, + PurePath(test_file_path).name, ], obj=click_test_dict, catch_exceptions=False, diff --git a/tests/test_jobs.py b/tests/test_jobs.py index aeb7b32..dda8544 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -16,7 +16,7 @@ def test_unpack_jobs(foss: Fossology, upload: Upload): jobs, _ = foss.list_jobs(upload=upload) - assert len(jobs) == 1 + assert len(jobs) == 3 def test_nogroup_jobs(foss: Fossology, upload: Upload, foss_schedule_agents: Dict): diff --git a/tests/test_upload_from.py b/tests/test_upload_from.py index d643f85..118785e 100644 --- a/tests/test_upload_from.py +++ b/tests/test_upload_from.py @@ -112,8 +112,8 @@ def test_upload_from_url_v2(foss_v2: Fossology): def test_upload_from_server(foss: Fossology): server = { - "path": "/tmp/base-files-11", - "name": "base-files-11", + "path": "/tmp/base-files-10.3", + "name": "base-files-10.3", } server_upload = foss.upload_file( foss.rootFolder, @@ -132,8 +132,8 @@ def test_upload_from_server(foss: Fossology): @pytest.mark.xfail def test_upload_from_server_v2(foss_v2: Fossology): server = { - "path": "/tmp/base-files-11", - "name": "base-files-11", + "path": "/tmp/base-files_10.3-debian10-test", + "name": "base-files_10.3-debian10-test", } server_upload = foss_v2.upload_file( foss_v2.rootFolder, diff --git a/tests/test_upload_licenses_copyrights.py b/tests/test_upload_licenses_copyrights.py index 448349d..9c889e5 100644 --- a/tests/test_upload_licenses_copyrights.py +++ b/tests/test_upload_licenses_copyrights.py @@ -23,12 +23,12 @@ def test_upload_licenses_with_containers(foss: Fossology, upload_with_jobs: Uplo def test_upload_licenses_agent_ojo(foss: Fossology, upload_with_jobs: Upload): licenses = foss.upload_licenses(upload_with_jobs, agent="ojo") - assert len(licenses) == 9 # type: ignore + assert len(licenses) == 10 # type: ignore def test_upload_licenses_agent_monk(foss: Fossology, upload_with_jobs: Upload): licenses = foss.upload_licenses(upload_with_jobs, agent="monk") - assert len(licenses) == 22 # type: ignore + assert len(licenses) == 23 # type: ignore def test_upload_licenses_and_copyrights(foss: Fossology, upload_with_jobs: Upload): diff --git a/tests/test_uploads.py b/tests/test_uploads.py index 2c741c6..79b245c 100644 --- a/tests/test_uploads.py +++ b/tests/test_uploads.py @@ -52,8 +52,8 @@ def test_upload_assignee_fields_are_not_wrapped_in_tuples(): def test_upload_sha1(upload: Upload): - assert upload.uploadname == "base-files_11.tar.xz" - assert upload.hash.sha1 == "D4D663FC2877084362FB2297337BE05684869B00" + assert upload.uploadname == "base-files_10.3-debian10-test.tar.bz2" + assert upload.hash.sha1 == "7A36310AB05CD8401A1CC16B934D998ED491EB12" assert str(upload) == ( f"Upload '{upload.uploadname}' ({upload.id}, {upload.hash.size}B, {upload.hash.sha1}) " f"in folder {upload.foldername} ({upload.folderid})" @@ -65,8 +65,8 @@ def test_upload_sha1(upload: Upload): def test_upload(upload: Upload): - assert upload.uploadname == "base-files_11.tar.xz" - assert upload.hash.sha1 == "D4D663FC2877084362FB2297337BE05684869B00" + assert upload.uploadname == "base-files_10.3-debian10-test.tar.bz2" + assert upload.hash.sha1 == "7A36310AB05CD8401A1CC16B934D998ED491EB12" assert str(upload) == ( f"Upload '{upload.uploadname}' ({upload.id}, {upload.hash.size}B, {upload.hash.sha1}) " f"in folder {upload.foldername} ({upload.folderid})" @@ -90,9 +90,9 @@ def test_upload_for_group(foss: Fossology, test_file_path: str): wait_time=5, ) if upload: - assert upload.uploadname == "base-files_11.tar.xz" + assert upload.uploadname == "base-files_10.3-debian10-test.tar.bz2" uploads = foss.list_uploads(group="upload_access") - assert uploads[0][0].uploadname == "base-files_11.tar.xz" + assert uploads[0][0].uploadname == "base-files_10.3-debian10-test.tar.bz2" foss.delete_upload(upload) foss.delete_group(group_access.id) @@ -111,16 +111,16 @@ def test_upload_for_group_v2(foss_v2: Fossology, test_file_path: str): wait_time=5, ) if upload: - assert upload.uploadname == "base-files_11.tar.xz" + assert upload.uploadname == "base-files_10.3-debian10-test.tar.bz2" uploads = foss_v2.list_uploads(group="upload_access") - assert uploads[0][0].uploadname == "base-files_11.tar.xz" + assert uploads[0][0].uploadname == "base-files_10.3-debian10-test.tar.bz2" foss_v2.delete_upload(upload) foss_v2.delete_group(group_access.id) @pytest.mark.xfail def test_upload_v2(upload_v2: Upload): - assert upload_v2.uploadname == "base-files_11.tar.xz" + assert upload_v2.uploadname == "base-files_10.3-debian10-test.tar.bz2" assert upload_v2.hash.sha1 == "D4D663FC2877084362FB2297337BE05684869B00" assert str(upload_v2) == ( f"Upload '{upload_v2.uploadname}' ({upload_v2.id}, {upload_v2.hash.size}B, {upload_v2.hash.sha1}) " @@ -392,7 +392,7 @@ def test_download_upload(foss: Fossology, upload: Upload): filetype = mimetypes.guess_type(download_path / upload_filename) upload_stat = os.stat(download_path / upload_filename) assert upload_stat.st_size > 0 - assert filetype == ("application/x-tar", "xz") + assert filetype == ("application/x-tar", "bzip2") Path(download_path / upload_filename).unlink()