Files
devx/tests/unit/test_ci_check_workflow_artifact_deps.py
T
emil 3d4b4940ff
Post-merge / detect-and-configure (push) Successful in 16s
Post-merge / release-and-maintain (push) Successful in 1m2s
DEVX-153: feat: sync missing features from v0.49.x line to master
Co-authored-by: emil User <emil.simeonov@tutanota.com>
2026-08-09 01:09:20 +00:00

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