Public Access
224 lines
6.2 KiB
Python
224 lines
6.2 KiB
Python
"""Unit tests for devx.ci.validate_spec."""
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from click.testing import CliRunner
|
|
|
|
from devx.ci.validate_spec import (
|
|
AC_CHECKED_RE,
|
|
AC_UNCHECKED_RE,
|
|
REQ_ID_RE,
|
|
cli,
|
|
find_spec_file,
|
|
validate_spec_content,
|
|
)
|
|
|
|
VALID_SPEC = """\
|
|
# OBL-INFRA-531: Fix sso-bridge role for pip install
|
|
|
|
## Problem
|
|
The sso-bridge role uses scripts.sso_bridge.listener but the pip
|
|
package uses sso_bridge.listener.
|
|
|
|
## Approach
|
|
REQ-1: Update molecule verify.yml to use sso_bridge.listener
|
|
REQ-2: Add infra repo clone task to sso_bridge role
|
|
|
|
## Test Plan
|
|
- Run molecule test for sso_bridge role
|
|
- Verify pip package is installed correctly
|
|
|
|
## Deploy Plan
|
|
- Merge PR
|
|
- Auto-deploy to staging
|
|
|
|
## Rollback Plan
|
|
- Revert PR
|
|
- Re-deploy previous version
|
|
|
|
## Acceptance Criteria
|
|
- [x] Molecule test passes with sso_bridge.listener
|
|
- [x] Infra repo is cloned by sso_bridge role
|
|
"""
|
|
|
|
|
|
SPEC_MISSING_SECTION = """\
|
|
# OBL-INFRA-531: Fix sso-bridge
|
|
|
|
## Problem
|
|
Something is broken.
|
|
|
|
## Approach
|
|
REQ-1: Fix it
|
|
|
|
## Test Plan
|
|
Run tests
|
|
"""
|
|
|
|
|
|
SPEC_UNCHECKED_AC = """\
|
|
# OBL-INFRA-531: Fix sso-bridge
|
|
|
|
## Problem
|
|
Broken.
|
|
|
|
## Approach
|
|
REQ-1: Fix it
|
|
|
|
## Test Plan
|
|
Run tests
|
|
|
|
## Deploy Plan
|
|
Deploy
|
|
|
|
## Rollback Plan
|
|
Revert
|
|
|
|
## Acceptance Criteria
|
|
- [x] Fixed
|
|
- [ ] Verified in staging
|
|
"""
|
|
|
|
|
|
SPEC_NO_REQ_IDS = """\
|
|
# OBL-INFRA-531: Fix sso-bridge
|
|
|
|
## Problem
|
|
Broken.
|
|
|
|
## Approach
|
|
Fix it.
|
|
|
|
## Test Plan
|
|
Run tests
|
|
|
|
## Deploy Plan
|
|
Deploy
|
|
|
|
## Rollback Plan
|
|
Revert
|
|
|
|
## Acceptance Criteria
|
|
- [x] Fixed
|
|
"""
|
|
|
|
|
|
class TestFindSpecFile:
|
|
def test_finds_exact_match(self, tmp_path: Path) -> None:
|
|
specs_dir = tmp_path / "specs"
|
|
specs_dir.mkdir()
|
|
(specs_dir / "OBL-INFRA-531.md").write_text("content")
|
|
result = find_spec_file("OBL-INFRA-531", str(specs_dir))
|
|
assert result is not None
|
|
assert result.name == "OBL-INFRA-531.md"
|
|
|
|
def test_finds_case_insensitive(self, tmp_path: Path) -> None:
|
|
specs_dir = tmp_path / "specs"
|
|
specs_dir.mkdir()
|
|
(specs_dir / "obl-infra-531.md").write_text("content")
|
|
result = find_spec_file("OBL-INFRA-531", str(specs_dir))
|
|
assert result is not None
|
|
|
|
def test_returns_none_when_not_found(self, tmp_path: Path) -> None:
|
|
specs_dir = tmp_path / "specs"
|
|
specs_dir.mkdir()
|
|
result = find_spec_file("OBL-INFRA-999", str(specs_dir))
|
|
assert result is None
|
|
|
|
def test_returns_none_when_dir_missing(self, tmp_path: Path) -> None:
|
|
result = find_spec_file("OBL-INFRA-531", str(tmp_path / "nonexistent"))
|
|
assert result is None
|
|
|
|
|
|
class TestValidateSpecContent:
|
|
def test_valid_spec_passes(self) -> None:
|
|
errors = validate_spec_content(VALID_SPEC)
|
|
assert errors == []
|
|
|
|
def test_missing_sections(self) -> None:
|
|
errors = validate_spec_content(SPEC_MISSING_SECTION)
|
|
assert len(errors) >= 3 # Missing Deploy Plan, Rollback Plan, Acceptance Criteria
|
|
assert any("Deploy Plan" in e for e in errors)
|
|
assert any("Rollback Plan" in e for e in errors)
|
|
assert any("Acceptance Criteria" in e for e in errors)
|
|
|
|
def test_unchecked_ac_fails(self) -> None:
|
|
errors = validate_spec_content(SPEC_UNCHECKED_AC)
|
|
assert len(errors) == 1
|
|
assert "unchecked" in errors[0].lower()
|
|
|
|
def test_no_req_ids_fails(self) -> None:
|
|
errors = validate_spec_content(SPEC_NO_REQ_IDS)
|
|
assert any("REQ-ID" in e for e in errors)
|
|
|
|
def test_empty_content_fails(self) -> None:
|
|
errors = validate_spec_content("")
|
|
assert len(errors) >= 2 # Missing sections + no REQ-IDs
|
|
|
|
|
|
class TestRegexPatterns:
|
|
def test_req_id_re_matches(self) -> None:
|
|
assert REQ_ID_RE.search("REQ-1: Do something")
|
|
assert REQ_ID_RE.search("REQ-42: Another thing")
|
|
assert not REQ_ID_RE.search("REQ: no number")
|
|
|
|
def test_ac_checked_re_matches(self) -> None:
|
|
assert AC_CHECKED_RE.search("- [x] Done")
|
|
assert AC_CHECKED_RE.search(" - [x] Indented")
|
|
assert not AC_CHECKED_RE.search("- [ ] Not done")
|
|
|
|
def test_ac_unchecked_re_matches(self) -> None:
|
|
assert AC_UNCHECKED_RE.search("- [ ] Not done")
|
|
assert AC_UNCHECKED_RE.search(" - [ ] Indented")
|
|
assert not AC_UNCHECKED_RE.search("- [x] Done")
|
|
|
|
|
|
class TestCli:
|
|
def test_fails_without_task_id(self) -> None:
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["--branch", "no-task-id"])
|
|
assert result.exit_code != 0
|
|
|
|
def test_allow_missing_succeeds_without_task_id(self) -> None:
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["--branch", "no-task-id", "--allow-missing"])
|
|
assert result.exit_code == 0
|
|
assert "WARNING" in result.output
|
|
|
|
def test_fails_when_spec_not_found(self, tmp_path: Path) -> None:
|
|
runner = CliRunner()
|
|
with patch("devx.ci.validate_spec.extract_task_id", return_value="OBL-INFRA-999"):
|
|
result = runner.invoke(
|
|
cli,
|
|
["--branch", "OBL-INFRA-999-test", "--specs-dir", str(tmp_path / "specs")],
|
|
)
|
|
assert result.exit_code != 0
|
|
assert "No spec file found" in result.output
|
|
|
|
def test_passes_with_valid_spec(self, tmp_path: Path) -> None:
|
|
specs_dir = tmp_path / "specs"
|
|
specs_dir.mkdir()
|
|
(specs_dir / "OBL-INFRA-531.md").write_text(VALID_SPEC)
|
|
runner = CliRunner()
|
|
with patch("devx.ci.validate_spec.extract_task_id", return_value="OBL-INFRA-531"):
|
|
result = runner.invoke(
|
|
cli,
|
|
["--branch", "OBL-INFRA-531-fix-foo", "--specs-dir", str(specs_dir)],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "Spec validated" in result.output
|
|
|
|
def test_fails_with_unchecked_ac(self, tmp_path: Path) -> None:
|
|
specs_dir = tmp_path / "specs"
|
|
specs_dir.mkdir()
|
|
(specs_dir / "OBL-INFRA-531.md").write_text(SPEC_UNCHECKED_AC)
|
|
runner = CliRunner()
|
|
with patch("devx.ci.validate_spec.extract_task_id", return_value="OBL-INFRA-531"):
|
|
result = runner.invoke(
|
|
cli,
|
|
["--branch", "OBL-INFRA-531-fix-foo", "--specs-dir", str(specs_dir)],
|
|
)
|
|
assert result.exit_code != 0
|
|
assert "unchecked" in result.output.lower()
|