Skip to content

Commit 784faf1

Browse files
RSMetaCheck action fails on detection #96
1 parent 678d5e6 commit 784faf1

4 files changed

Lines changed: 223 additions & 2 deletions

File tree

src/rsmetacheck/cli.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import argparse
2+
import json
23
import os
4+
import sys
35
from pathlib import Path
46

5-
from rsmetacheck.config import load_analysis_config
7+
from rsmetacheck.config import AnalysisConfig, load_analysis_config
68
from rsmetacheck.run_analyzer import run_analysis
79
from rsmetacheck.run_somef import (
810
ensure_somef_configured,
@@ -11,6 +13,30 @@
1113
)
1214

1315

16+
def _exit_on_findings(analysis_output: str, analysis_config: AnalysisConfig) -> None:
17+
try:
18+
with open(analysis_output) as f:
19+
data = json.load(f)
20+
except Exception as e:
21+
print(f"Warning: Could not read analysis output to evaluate exit code: {e}")
22+
return
23+
24+
summary = data.get("summary", {})
25+
pitfalls = summary.get("total_pitfalls_detected", 0)
26+
warnings = summary.get("total_warnings_detected", 0)
27+
28+
should_fail = False
29+
if analysis_config.fail_on_pitfalls and pitfalls > 0:
30+
print(f"CI gate: {pitfalls} pitfall(s) detected (fail_on_pitfalls=true).")
31+
should_fail = True
32+
if analysis_config.fail_on_warnings and warnings > 0:
33+
print(f"CI gate: {warnings} warning(s) detected (fail_on_warnings=true).")
34+
should_fail = True
35+
36+
if should_fail:
37+
sys.exit(1)
38+
39+
1440
def cli():
1541
parser = argparse.ArgumentParser(
1642
description="Detect metadata pitfalls in software repositories using SoMEF."
@@ -118,6 +144,8 @@ def cli():
118144
analysis_config=analysis_config,
119145
)
120146

147+
_exit_on_findings(args.analysis_output, analysis_config)
148+
121149
else:
122150
ensure_somef_configured()
123151

@@ -178,6 +206,8 @@ def cli():
178206
analysis_config=analysis_config,
179207
)
180208

209+
_exit_on_findings(args.analysis_output, analysis_config)
210+
181211

182212
if __name__ == "__main__":
183213
cli()

src/rsmetacheck/config.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ class AnalysisConfig:
1515
ignored_checks: Set[str] = field(default_factory=set)
1616
exclude_files: list[str] = field(default_factory=list)
1717
check_parameters: Dict[str, Dict[str, Any]] = field(default_factory=dict)
18+
fail_on_pitfalls: bool = True
19+
fail_on_warnings: bool = True
1820
profile: Optional[str] = None
1921
source_path: Optional[Path] = None
2022

@@ -129,6 +131,9 @@ def load_analysis_config(
129131
base_exclude_files = _normalize_exclude_files(raw.get("exclude_files", []))
130132
base_parameters = _normalize_parameters(raw.get("parameters", {}))
131133

134+
fail_on_pitfalls = raw.get("fail_on_pitfalls", True)
135+
fail_on_warnings = raw.get("fail_on_warnings", True)
136+
132137
profile_ignore: Set[str] = set()
133138
profile_exclude_files: list[str] = []
134139
profile_parameters: Dict[str, Dict[str, Any]] = {}
@@ -146,12 +151,19 @@ def load_analysis_config(
146151
profile_exclude_files = _normalize_exclude_files(selected.get("exclude_files", []))
147152
profile_parameters = _normalize_parameters(selected.get("parameters", {}))
148153

154+
if "fail_on_pitfalls" in selected:
155+
fail_on_pitfalls = selected["fail_on_pitfalls"]
156+
if "fail_on_warnings" in selected:
157+
fail_on_warnings = selected["fail_on_warnings"]
158+
149159
merged_parameters = _merge_parameters(base_parameters, profile_parameters)
150160

151161
return AnalysisConfig(
152162
ignored_checks=base_ignore | profile_ignore,
153163
exclude_files=base_exclude_files + profile_exclude_files,
154164
check_parameters=merged_parameters,
165+
fail_on_pitfalls=fail_on_pitfalls,
166+
fail_on_warnings=fail_on_warnings,
155167
profile=selected_profile,
156168
source_path=resolved_path,
157169
)

tests/test_cli.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
"""Unit tests verifying CLI argument handling and dispatch."""
22

33
import importlib
4+
import json
45
from unittest.mock import MagicMock
56

7+
import pytest
8+
69
from rsmetacheck.config import AnalysisConfig
710

811
cli_module = importlib.import_module("rsmetacheck.cli")
@@ -539,3 +542,125 @@ def test_cli_config_load_error_stops_execution(monkeypatch, tmp_path, capsys):
539542
captured = capsys.readouterr()
540543
assert "Error loading config" in captured.out
541544
run_analysis_mock.assert_not_called()
545+
546+
547+
def test_exit_on_findings_pitfalls_cause_exit(tmp_path, capsys):
548+
"""Exit code 1 when pitfalls detected and fail_on_pitfalls=True."""
549+
analysis_file = tmp_path / "analysis.json"
550+
analysis_file.write_text(json.dumps({
551+
"summary": {
552+
"total_pitfalls_detected": 3,
553+
"total_warnings_detected": 0,
554+
}
555+
}))
556+
config = AnalysisConfig(fail_on_pitfalls=True, fail_on_warnings=False)
557+
558+
with pytest.raises(SystemExit) as exc_info:
559+
cli_module._exit_on_findings(str(analysis_file), config)
560+
561+
assert exc_info.value.code == 1
562+
captured = capsys.readouterr()
563+
assert "CI gate: 3 pitfall(s) detected" in captured.out
564+
565+
566+
def test_exit_on_findings_warnings_cause_exit(tmp_path, capsys):
567+
"""Exit code 1 when warnings detected and fail_on_warnings=True."""
568+
analysis_file = tmp_path / "analysis.json"
569+
analysis_file.write_text(json.dumps({
570+
"summary": {
571+
"total_pitfalls_detected": 0,
572+
"total_warnings_detected": 2,
573+
}
574+
}))
575+
config = AnalysisConfig(fail_on_pitfalls=False, fail_on_warnings=True)
576+
577+
with pytest.raises(SystemExit) as exc_info:
578+
cli_module._exit_on_findings(str(analysis_file), config)
579+
580+
assert exc_info.value.code == 1
581+
captured = capsys.readouterr()
582+
assert "CI gate: 2 warning(s) detected" in captured.out
583+
584+
585+
def test_exit_on_findings_both_cause_exit(tmp_path, capsys):
586+
"""Exit code 1 when both pitfalls and warnings detected with both flags True."""
587+
analysis_file = tmp_path / "analysis.json"
588+
analysis_file.write_text(json.dumps({
589+
"summary": {
590+
"total_pitfalls_detected": 1,
591+
"total_warnings_detected": 1,
592+
}
593+
}))
594+
config = AnalysisConfig(fail_on_pitfalls=True, fail_on_warnings=True)
595+
596+
with pytest.raises(SystemExit) as exc_info:
597+
cli_module._exit_on_findings(str(analysis_file), config)
598+
599+
assert exc_info.value.code == 1
600+
captured = capsys.readouterr()
601+
assert "CI gate: 1 pitfall(s) detected" in captured.out
602+
assert "CI gate: 1 warning(s) detected" in captured.out
603+
604+
605+
def test_exit_on_findings_no_exit_when_flags_false(tmp_path):
606+
"""No exit when fail flags are False even with findings."""
607+
analysis_file = tmp_path / "analysis.json"
608+
analysis_file.write_text(json.dumps({
609+
"summary": {
610+
"total_pitfalls_detected": 5,
611+
"total_warnings_detected": 5,
612+
}
613+
}))
614+
config = AnalysisConfig(fail_on_pitfalls=False, fail_on_warnings=False)
615+
616+
# Should not raise SystemExit
617+
cli_module._exit_on_findings(str(analysis_file), config)
618+
619+
620+
def test_exit_on_findings_no_exit_with_zero_findings(tmp_path):
621+
"""No exit when zero pitfalls and warnings even with flags True."""
622+
analysis_file = tmp_path / "analysis.json"
623+
analysis_file.write_text(json.dumps({
624+
"summary": {
625+
"total_pitfalls_detected": 0,
626+
"total_warnings_detected": 0,
627+
}
628+
}))
629+
config = AnalysisConfig(fail_on_pitfalls=True, fail_on_warnings=True)
630+
631+
# Should not raise SystemExit
632+
cli_module._exit_on_findings(str(analysis_file), config)
633+
634+
635+
def test_exit_on_findings_missing_file_prints_warning(tmp_path, capsys):
636+
"""Missing analysis file should print warning and not exit."""
637+
config = AnalysisConfig(fail_on_pitfalls=True, fail_on_warnings=True)
638+
639+
cli_module._exit_on_findings(str(tmp_path / "nonexistent.json"), config)
640+
641+
captured = capsys.readouterr()
642+
assert "Warning: Could not read analysis output" in captured.out
643+
644+
645+
def test_exit_on_findings_malformed_json_prints_warning(tmp_path, capsys):
646+
"""Malformed analysis file should print warning and not exit."""
647+
analysis_file = tmp_path / "analysis.json"
648+
analysis_file.write_text("not valid json")
649+
650+
config = AnalysisConfig(fail_on_pitfalls=True, fail_on_warnings=True)
651+
652+
cli_module._exit_on_findings(str(analysis_file), config)
653+
654+
captured = capsys.readouterr()
655+
assert "Warning: Could not read analysis output" in captured.out
656+
657+
658+
def test_exit_on_findings_missing_summary_fields_treated_as_zero(tmp_path):
659+
"""Missing summary fields should be treated as 0 (no exit)."""
660+
analysis_file = tmp_path / "analysis.json"
661+
analysis_file.write_text(json.dumps({"summary": {}}))
662+
663+
config = AnalysisConfig(fail_on_pitfalls=True, fail_on_warnings=True)
664+
665+
# Should not raise SystemExit
666+
cli_module._exit_on_findings(str(analysis_file), config)

tests/test_config.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from rsmetacheck.config import load_analysis_config
1+
from rsmetacheck.config import AnalysisConfig, load_analysis_config
22

33

44
def test_load_analysis_config_auto_detects_default_file(tmp_path):
@@ -59,3 +59,57 @@ def test_load_analysis_config_missing_profile_raises(tmp_path):
5959
assert False, "Expected ValueError"
6060
except ValueError:
6161
pass
62+
63+
64+
def test_analysis_config_fail_flags_default_to_true():
65+
"""fail_on_pitfalls and fail_on_warnings should default to True."""
66+
config = AnalysisConfig()
67+
assert config.fail_on_pitfalls is True
68+
assert config.fail_on_warnings is True
69+
70+
71+
def test_load_analysis_config_reads_fail_flags_from_toml(tmp_path):
72+
"""fail_on_pitfalls and fail_on_warnings should be read from TOML root."""
73+
config_file = tmp_path / ".rsmetacheck.toml"
74+
config_file.write_text(
75+
"""
76+
fail_on_pitfalls = false
77+
fail_on_warnings = false
78+
""".strip()
79+
)
80+
81+
config = load_analysis_config(cwd=tmp_path)
82+
83+
assert config.fail_on_pitfalls is False
84+
assert config.fail_on_warnings is False
85+
86+
87+
def test_load_analysis_config_fail_flags_default_when_absent(tmp_path):
88+
"""When TOML has no fail flags, they should default to True."""
89+
config_file = tmp_path / ".rsmetacheck.toml"
90+
config_file.write_text('ignore = ["P001"]\n')
91+
92+
config = load_analysis_config(cwd=tmp_path)
93+
94+
assert config.fail_on_pitfalls is True
95+
assert config.fail_on_warnings is True
96+
97+
98+
def test_load_analysis_config_profile_overrides_fail_flags(tmp_path):
99+
"""Profile-level fail flags should override base values."""
100+
config_file = tmp_path / ".rsmetacheck.toml"
101+
config_file.write_text(
102+
"""
103+
fail_on_pitfalls = true
104+
fail_on_warnings = true
105+
106+
[profiles.permissive]
107+
fail_on_pitfalls = false
108+
fail_on_warnings = false
109+
""".strip()
110+
)
111+
112+
config = load_analysis_config(cwd=tmp_path, profile="permissive")
113+
114+
assert config.fail_on_pitfalls is False
115+
assert config.fail_on_warnings is False

0 commit comments

Comments
 (0)