Public Access
420 lines
14 KiB
Python
420 lines
14 KiB
Python
"""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
|