"""Unit tests for devx.ci.check_workflow_artifact_deps.""" from __future__ import annotations import textwrap from pathlib import Path from click.testing import CliRunner from devx.ci.check_workflow_artifact_deps import ( _check_workflow, _extract_artifact_info, _is_artifact_action, main, ) class TestIsArtifactAction: def test_upload_action_gitea(self): assert _is_artifact_action("christopherhx/gitea-upload-artifact@v4", ("upload-artifact",)) def test_upload_action_github(self): assert _is_artifact_action("actions/upload-artifact@v4", ("upload-artifact",)) def test_download_action(self): assert _is_artifact_action("christopherhx/gitea-download-artifact@v4", ("download-artifact",)) def test_non_artifact_action(self): assert not _is_artifact_action("actions/checkout@v4", ("upload-artifact",)) def test_empty_string(self): assert not _is_artifact_action("", ("upload-artifact",)) def test_case_insensitive(self): assert _is_artifact_action("Actions/Upload-Artifact@v4", ("upload-artifact",)) class TestExtractArtifactInfo: def test_uploads_and_downloads(self): import yaml workflow_yaml = textwrap.dedent(""" jobs: producer: steps: - name: Upload config uses: christopherhx/gitea-upload-artifact@v4 with: name: config-${{ github.run_id }} consumer: needs: [producer] steps: - name: Download config uses: christopherhx/gitea-download-artifact@v4 with: name: config-${{ github.run_id }} """).strip() wf = yaml.safe_load(workflow_yaml) uploads, downloads = _extract_artifact_info(wf) assert uploads == {"config-${{ github.run_id }}": ["producer"]} assert downloads == [("consumer", "config-${{ github.run_id }}", "Download config")] def test_no_artifacts(self): import yaml workflow_yaml = textwrap.dedent(""" jobs: build: steps: - name: Checkout uses: actions/checkout@v4 """).strip() wf = yaml.safe_load(workflow_yaml) uploads, downloads = _extract_artifact_info(wf) assert uploads == {} assert downloads == [] def test_multiple_uploaders_same_artifact(self): import yaml workflow_yaml = textwrap.dedent(""" jobs: producer-a: steps: - uses: christopherhx/gitea-upload-artifact@v4 with: name: shared producer-b: steps: - uses: christopherhx/gitea-upload-artifact@v4 with: name: shared """).strip() wf = yaml.safe_load(workflow_yaml) uploads, downloads = _extract_artifact_info(wf) assert uploads == {"shared": ["producer-a", "producer-b"]} def test_step_without_name(self): import yaml workflow_yaml = textwrap.dedent(""" jobs: producer: steps: - uses: christopherhx/gitea-upload-artifact@v4 with: name: data consumer: needs: [producer] steps: - uses: christopherhx/gitea-download-artifact@v4 with: name: data """).strip() wf = yaml.safe_load(workflow_yaml) uploads, downloads = _extract_artifact_info(wf) assert downloads == [("consumer", "data", "")] def test_upload_without_name_skipped(self): import yaml workflow_yaml = textwrap.dedent(""" jobs: producer: steps: - uses: christopherhx/gitea-upload-artifact@v4 with: path: ./dist """).strip() wf = yaml.safe_load(workflow_yaml) uploads, downloads = _extract_artifact_info(wf) assert uploads == {} class TestCheckWorkflow: def test_valid_dependency(self, tmp_path: Path): workflow_yaml = textwrap.dedent(""" jobs: producer: steps: - name: Upload config uses: christopherhx/gitea-upload-artifact@v4 with: name: config consumer: needs: [producer] steps: - name: Download config uses: christopherhx/gitea-download-artifact@v4 with: name: config """).strip() f = tmp_path / "test.yml" f.write_text(workflow_yaml) assert _check_workflow(f) == [] def test_missing_dependency(self, tmp_path: Path): workflow_yaml = textwrap.dedent(""" jobs: producer: steps: - name: Upload config uses: christopherhx/gitea-upload-artifact@v4 with: name: config consumer: needs: [other-job] steps: - name: Download config uses: christopherhx/gitea-download-artifact@v4 with: name: config """).strip() f = tmp_path / "test.yml" f.write_text(workflow_yaml) errors = _check_workflow(f) assert len(errors) == 1 assert "consumer" in errors[0] assert "producer" in errors[0] def test_no_needs_at_all(self, tmp_path: Path): workflow_yaml = textwrap.dedent(""" jobs: producer: steps: - uses: christopherhx/gitea-upload-artifact@v4 with: name: data consumer: steps: - uses: christopherhx/gitea-download-artifact@v4 with: name: data """).strip() f = tmp_path / "test.yml" f.write_text(workflow_yaml) errors = _check_workflow(f) assert len(errors) == 1 assert "consumer" in errors[0] def test_artifact_not_uploaded_in_workflow(self, tmp_path: Path): workflow_yaml = textwrap.dedent(""" jobs: consumer: steps: - name: Download external uses: christopherhx/gitea-download-artifact@v4 with: name: external-artifact """).strip() f = tmp_path / "test.yml" f.write_text(workflow_yaml) assert _check_workflow(f) == [] def test_multiple_uploaders_one_in_needs(self, tmp_path: Path): workflow_yaml = textwrap.dedent(""" jobs: producer-a: steps: - uses: christopherhx/gitea-upload-artifact@v4 with: name: shared producer-b: steps: - uses: christopherhx/gitea-upload-artifact@v4 with: name: shared consumer: needs: [producer-a, other] steps: - uses: christopherhx/gitea-download-artifact@v4 with: name: shared """).strip() f = tmp_path / "test.yml" f.write_text(workflow_yaml) assert _check_workflow(f) == [] def test_string_needs(self, tmp_path: Path): workflow_yaml = textwrap.dedent(""" jobs: producer: steps: - uses: christopherhx/gitea-upload-artifact@v4 with: name: data consumer: needs: producer steps: - uses: christopherhx/gitea-download-artifact@v4 with: name: data """).strip() f = tmp_path / "test.yml" f.write_text(workflow_yaml) assert _check_workflow(f) == [] def test_needs_null(self, tmp_path: Path): workflow_yaml = textwrap.dedent(""" jobs: producer: steps: - uses: christopherhx/gitea-upload-artifact@v4 with: name: data consumer: needs: null steps: - uses: christopherhx/gitea-download-artifact@v4 with: name: data """).strip() f = tmp_path / "test.yml" f.write_text(workflow_yaml) errors = _check_workflow(f) assert len(errors) == 1 assert "consumer" in errors[0] def test_invalid_yaml(self, tmp_path: Path): f = tmp_path / "test.yml" f.write_text("jobs: [invalid yaml: {") errors = _check_workflow(f) assert len(errors) == 1 assert "cannot parse YAML" in errors[0] def test_not_a_dict(self, tmp_path: Path): f = tmp_path / "test.yml" f.write_text("just a string") errors = _check_workflow(f) assert len(errors) == 1 assert "not a valid workflow" in errors[0] def test_no_jobs(self, tmp_path: Path): f = tmp_path / "test.yml" f.write_text("name: empty\non: push\n") assert _check_workflow(f) == [] def test_continue_on_error_guard(self, tmp_path: Path): workflow_yaml = textwrap.dedent(""" jobs: producer: steps: - name: Upload config uses: christopherhx/gitea-upload-artifact@v4 with: name: config consumer: needs: [other-job] steps: - name: Download config continue-on-error: true uses: christopherhx/gitea-download-artifact@v4 with: name: config """).strip() f = tmp_path / "test.yml" f.write_text(workflow_yaml) assert _check_workflow(f) == [] def test_continue_on_error_false_still_errors(self, tmp_path: Path): workflow_yaml = textwrap.dedent(""" jobs: producer: steps: - name: Upload config uses: christopherhx/gitea-upload-artifact@v4 with: name: config consumer: needs: [other-job] steps: - name: Download config continue-on-error: false uses: christopherhx/gitea-download-artifact@v4 with: name: config """).strip() f = tmp_path / "test.yml" f.write_text(workflow_yaml) errors = _check_workflow(f) assert len(errors) == 1 assert "consumer" in errors[0] def test_job_with_no_steps(self, tmp_path: Path): workflow_yaml = textwrap.dedent(""" jobs: empty: runs-on: docker """).strip() f = tmp_path / "test.yml" f.write_text(workflow_yaml) assert _check_workflow(f) == [] class TestMain: def test_passes_when_valid(self, tmp_path: Path): workflow_yaml = textwrap.dedent(""" jobs: producer: steps: - uses: christopherhx/gitea-upload-artifact@v4 with: name: data consumer: needs: [producer] steps: - uses: christopherhx/gitea-download-artifact@v4 with: name: data """).strip() f = tmp_path / "test.yml" f.write_text(workflow_yaml) runner = CliRunner() result = runner.invoke(main, ["--workflows-dir", str(tmp_path)]) assert result.exit_code == 0 assert "OK" in result.output def test_fails_when_missing_dep(self, tmp_path: Path): workflow_yaml = textwrap.dedent(""" jobs: producer: steps: - uses: christopherhx/gitea-upload-artifact@v4 with: name: data consumer: steps: - uses: christopherhx/gitea-download-artifact@v4 with: name: data """).strip() f = tmp_path / "test.yml" f.write_text(workflow_yaml) runner = CliRunner() result = runner.invoke(main, ["--workflows-dir", str(tmp_path)]) assert result.exit_code == 1 assert "FAIL" in result.output assert "consumer" in result.output def test_specific_workflow_file(self, tmp_path: Path): workflow_yaml = textwrap.dedent(""" jobs: producer: steps: - uses: christopherhx/gitea-upload-artifact@v4 with: name: data consumer: needs: [producer] steps: - uses: christopherhx/gitea-download-artifact@v4 with: name: data """).strip() f = tmp_path / "test.yml" f.write_text(workflow_yaml) runner = CliRunner() result = runner.invoke(main, ["--workflow", str(f)]) assert result.exit_code == 0