Compare commits

...
3 Commits
Author SHA1 Message Date
devx-ci-bot a14d838564 release: v0.10.2 [skip ci] 2026-06-24 20:56:35 +00:00
emil 7dcb9c03c0 DEVX-42: fix: badge generation respects pyproject.toml testpaths, shows stdout in warnings
Post-merge / detect-type (push) Successful in 13s
Post-merge / validate-commit-msg (push) Successful in 6s
Post-merge / configure-repo (push) Successful in 9s
Post-merge / release (push) Successful in 45s
Post-merge / vikunja (push) Successful in 11s
Post-merge / sync-wiki (push) Successful in 51s
Post-merge / badges (push) Successful in 1m5s
2026-06-24 20:55:35 +00:00
gitea-actions-bot 6f2b110c17 chore: update badge URLs to commit 55e25b06 [skip ci] 2026-06-24 20:34:59 +00:00
6 changed files with 90 additions and 19 deletions
+6
View File
@@ -2,6 +2,12 @@
All notable changes to this project will be documented in this file.
## [0.10.2] - 2026-06-24
### Bug Fixes
- Badge generation respects pyproject.toml testpaths, shows stdout in warnings
## [0.10.1] - 2026-06-24
### Bug Fixes
+6 -6
View File
@@ -16,12 +16,12 @@ quality badges.
[![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/16bc9c200c1f0a7b28ff6114f2aa58af551fd155/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/16bc9c200c1f0a7b28ff6114f2aa58af551fd155/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/16bc9c200c1f0a7b28ff6114f2aa58af551fd155/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/16bc9c200c1f0a7b28ff6114f2aa58af551fd155/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/16bc9c200c1f0a7b28ff6114f2aa58af551fd155/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/16bc9c200c1f0a7b28ff6114f2aa58af551fd155/python.svg)](https://www.python.org/downloads/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/55e25b065ddcd66194f8897c4d5b12515f15fa28/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/55e25b065ddcd66194f8897c4d5b12515f15fa28/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/55e25b065ddcd66194f8897c4d5b12515f15fa28/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/55e25b065ddcd66194f8897c4d5b12515f15fa28/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/55e25b065ddcd66194f8897c4d5b12515f15fa28/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/55e25b065ddcd66194f8897c4d5b12515f15fa28/python.svg)](https://www.python.org/downloads/)
## Why devx?
+6 -6
View File
@@ -12,12 +12,12 @@ project to be reusable across all oblachno-oss repositories.
[![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/16bc9c200c1f0a7b28ff6114f2aa58af551fd155/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/16bc9c200c1f0a7b28ff6114f2aa58af551fd155/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/16bc9c200c1f0a7b28ff6114f2aa58af551fd155/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/16bc9c200c1f0a7b28ff6114f2aa58af551fd155/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/16bc9c200c1f0a7b28ff6114f2aa58af551fd155/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/16bc9c200c1f0a7b28ff6114f2aa58af551fd155/python.svg)](https://www.python.org/downloads/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/55e25b065ddcd66194f8897c4d5b12515f15fa28/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/55e25b065ddcd66194f8897c4d5b12515f15fa28/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/55e25b065ddcd66194f8897c4d5b12515f15fa28/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/55e25b065ddcd66194f8897c4d5b12515f15fa28/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/55e25b065ddcd66194f8897c4d5b12515f15fa28/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/55e25b065ddcd66194f8897c4d5b12515f15fa28/python.svg)](https://www.python.org/downloads/)
## Overview
+1 -1
View File
@@ -1,3 +1,3 @@
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
__version__ = "0.10.1"
__version__ = "0.10.2"
+36 -4
View File
@@ -239,6 +239,36 @@ def doc_coverage_color(pct: int) -> str:
return "orange"
def detect_testpaths(repo_root: Path) -> list[str]:
"""Detect test paths from pyproject.toml or filesystem.
Parses ``testpaths`` in ``[tool.pytest.ini_options]`` from
pyproject.toml. Falls back to ``["tests"]`` if the tests/
directory exists. Returns an empty list if no test paths
are found (pytest will use its own defaults).
"""
pyproject = repo_root / "pyproject.toml"
if pyproject.exists():
content = pyproject.read_text()
# Match: testpaths = ["dir1", "dir2"]
match = re.search(r"testpaths\s*=\s*\[([^\]]+)\]", content)
if match:
paths = re.findall(r'["\']([^"\']+)["\']', match.group(1))
resolved = []
for p in paths:
p = p.strip()
if (repo_root / p).exists():
resolved.append(p)
if resolved:
return resolved
# Fallback: tests/ directory
tests_dir = repo_root / "tests"
if tests_dir.is_dir():
return ["tests"]
return []
def collect_coverage_and_tests(repo_root: Path) -> tuple[dict[str, str | int], dict[str, str | int]]:
"""Run pytest-cov and collect coverage + test count badges.
@@ -251,8 +281,8 @@ def collect_coverage_and_tests(repo_root: Path) -> tuple[dict[str, str | int], d
click.echo(" WARNING: No coverage target detected (no src/ package, no --cov in pyproject.toml)")
return make_badge("coverage", "unknown", "lightgrey"), make_badge("tests", "unknown", "lightgrey")
tests_dir = repo_root / "tests"
testpaths: list[str] = [str(tests_dir)] if tests_dir.is_dir() else []
testpaths = detect_testpaths(repo_root)
click.echo(f" Test paths: {testpaths or '(pytest defaults)'}")
cmd = [
sys.executable,
@@ -273,7 +303,8 @@ def collect_coverage_and_tests(repo_root: Path) -> tuple[dict[str, str | int], d
cov_badge = make_badge("coverage", f"{coverage:.0f}%", coverage_color(coverage))
else:
click.echo(f" WARNING: Could not extract coverage from pytest output (rc={rc})")
click.echo(f" pytest stderr: {stderr.strip()[:200]}")
click.echo(f" pytest stdout (last 300 chars): {stdout.strip()[-300:]}")
click.echo(f" pytest stderr (last 300 chars): {stderr.strip()[-300:]}")
cov_badge = make_badge("coverage", "unknown", "red")
test_count = extract_test_count(combined)
@@ -281,7 +312,8 @@ def collect_coverage_and_tests(repo_root: Path) -> tuple[dict[str, str | int], d
tests_badge = make_badge("tests", f"{test_count} passing", "brightgreen" if rc == 0 else "red")
else:
click.echo(f" WARNING: Could not extract test count from pytest output (rc={rc})")
click.echo(f" pytest stderr: {stderr.strip()[:200]}")
click.echo(f" pytest stdout (last 300 chars): {stdout.strip()[-300:]}")
click.echo(f" pytest stderr (last 300 chars): {stderr.strip()[-300:]}")
tests_badge = make_badge("tests", "unknown", "red")
return cov_badge, tests_badge
+35 -2
View File
@@ -14,6 +14,7 @@ from devx.tools.generate_badges import (
coverage_color,
detect_coverage_target,
detect_package_name,
detect_testpaths,
doc_coverage_color,
extract_coverage,
extract_doc_coverage,
@@ -97,6 +98,32 @@ class TestDetectCoverageTarget:
assert detect_coverage_target(tmp_path) is None
class TestDetectTestpaths:
def test_parses_from_pyproject(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
(tmp_path / "scripts" / "tests").mkdir(parents=True)
(tmp_path / "tests" / "unit").mkdir(parents=True)
(tmp_path / "pyproject.toml").write_text(
'[tool.pytest.ini_options]\ntestpaths = ["scripts/tests", "tests/unit"]\n'
)
assert detect_testpaths(tmp_path) == ["scripts/tests", "tests/unit"]
def test_filters_nonexistent_paths(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
(tmp_path / "tests").mkdir()
(tmp_path / "pyproject.toml").write_text('[tool.pytest.ini_options]\ntestpaths = ["tests", "nonexistent"]\n')
assert detect_testpaths(tmp_path) == ["tests"]
def test_falls_back_to_tests_dir(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
(tmp_path / "tests").mkdir()
assert detect_testpaths(tmp_path) == ["tests"]
def test_returns_empty_when_no_tests_dir(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
assert detect_testpaths(tmp_path) == []
def test_returns_empty_when_pyproject_has_no_testpaths(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
(tmp_path / "pyproject.toml").write_text("[tool.pytest.ini_options]\naddopts = '-ra'\n")
assert detect_testpaths(tmp_path) == []
class TestRunCommand:
@patch("devx.tools.generate_badges.subprocess.run")
def test_returns_returncode_stdout_stderr(self, mock_run: MagicMock) -> None:
@@ -256,16 +283,22 @@ class TestReadVersion:
class TestCollectCoverageAndTests:
@patch("devx.tools.generate_badges.run_command")
@patch("devx.tools.generate_badges.detect_testpaths", return_value=["tests"])
@patch("devx.tools.generate_badges.detect_coverage_target", return_value="src/devx")
def test_extracts_coverage_and_tests(self, mock_target: MagicMock, mock_run: MagicMock, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
def test_extracts_coverage_and_tests(
self, mock_target: MagicMock, mock_testpaths: MagicMock, mock_run: MagicMock, tmp_path: Path
) -> None: # type: ignore[no-untyped-def]
mock_run.return_value = (0, "1018 passed in 4.23s\nTOTAL 3546 0 100%", "")
cov, tests = collect_coverage_and_tests(tmp_path)
assert cov["message"] == "100%"
assert tests["message"] == "1018 passing"
@patch("devx.tools.generate_badges.run_command")
@patch("devx.tools.generate_badges.detect_testpaths", return_value=["tests"])
@patch("devx.tools.generate_badges.detect_coverage_target", return_value="src/devx")
def test_returns_unknown_when_no_match(self, mock_target: MagicMock, mock_run: MagicMock, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
def test_returns_unknown_when_no_match(
self, mock_target: MagicMock, mock_testpaths: MagicMock, mock_run: MagicMock, tmp_path: Path
) -> None: # type: ignore[no-untyped-def]
mock_run.return_value = (1, "garbled output", "some error")
cov, tests = collect_coverage_and_tests(tmp_path)
assert cov["message"] == "unknown"