Merge pull request #7 from AIOSAI/feat/rich-cli-formatting
feat(seedgo): add Rich formatting to CLI output
This commit is contained in:
+3
-1
@@ -25,7 +25,9 @@ classifiers = [
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
||||
]
|
||||
dependencies = []
|
||||
dependencies = [
|
||||
"rich>=13.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/AIOSAI/AIPass"
|
||||
|
||||
+19
-37
@@ -32,6 +32,13 @@ from pathlib import Path
|
||||
|
||||
from .config import ConfigError, create_default_config, find_project_root
|
||||
from .discovery import discover_plugins
|
||||
from .display import (
|
||||
print_error,
|
||||
print_header,
|
||||
print_init_success,
|
||||
print_plugin_table,
|
||||
print_warning,
|
||||
)
|
||||
from .reporter import report_results
|
||||
from .runner import run_checks
|
||||
|
||||
@@ -159,17 +166,10 @@ def _cmd_init(args: argparse.Namespace) -> None:
|
||||
|
||||
try:
|
||||
config_path = create_default_config(project_root, profile=profile)
|
||||
print("Seed Go initialized.")
|
||||
print(f" Config: {config_path}")
|
||||
print(f" Plugins: {str(Path(config_path).parent / 'plugins')}")
|
||||
if profile:
|
||||
print(f" Profile: {profile}")
|
||||
print("")
|
||||
print("Next steps:")
|
||||
print(" 1. Add plugins to .seedgo/plugins/")
|
||||
print(" 2. Run: seedgo check")
|
||||
plugins_path = str(Path(config_path).parent / "plugins")
|
||||
print_init_success(config_path, plugins_path, profile=profile)
|
||||
except ConfigError as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
print_error(str(exc), suggestion="Check directory permissions and try again.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -220,13 +220,14 @@ def _cmd_check(args: argparse.Namespace) -> None:
|
||||
overall["passed"] = overall["overall_score"] >= args.threshold and error_count == 0
|
||||
|
||||
output = report_results(results, overall, format=args.format)
|
||||
print(output)
|
||||
if output:
|
||||
print(output)
|
||||
|
||||
except ConfigError as exc:
|
||||
print(f"Config error: {exc}", file=sys.stderr)
|
||||
print_error(f"Config error: {exc}")
|
||||
sys.exit(1)
|
||||
except Exception as exc:
|
||||
print(f"Unexpected error: {exc}", file=sys.stderr)
|
||||
print_error(f"Unexpected error: {exc}")
|
||||
sys.exit(1)
|
||||
|
||||
sys.exit(0 if overall.get("passed", True) else 1)
|
||||
@@ -235,7 +236,7 @@ def _cmd_check(args: argparse.Namespace) -> None:
|
||||
def _cmd_list(_args: argparse.Namespace) -> None:
|
||||
"""Handle `seedgo list`.
|
||||
|
||||
Discovers all plugins and prints a formatted table showing:
|
||||
Discovers all plugins and prints a Rich table showing:
|
||||
name, description, source, file types.
|
||||
|
||||
Exits 0 always (listing is informational).
|
||||
@@ -247,30 +248,11 @@ def _cmd_list(_args: argparse.Namespace) -> None:
|
||||
plugins = discover_plugins(project_root)
|
||||
|
||||
if not plugins:
|
||||
print("No plugins found.")
|
||||
print("")
|
||||
print("Add plugins to .seedgo/plugins/ or install plugin packages.")
|
||||
print_warning("No plugins found.")
|
||||
print_header("SEEDGO — Plugins", "Add plugins to .seedgo/plugins/ or install plugin packages.")
|
||||
sys.exit(0)
|
||||
|
||||
print(f"Found {len(plugins)} plugin(s):\n")
|
||||
|
||||
col_name = max(len(p["name"]) for p in plugins) + 2
|
||||
col_source = max(len(p["source"]) for p in plugins) + 2
|
||||
|
||||
header = (
|
||||
f" {'NAME':<{col_name}}{'SOURCE':<{col_source}}{'FILE TYPES':<20} DESCRIPTION"
|
||||
)
|
||||
print(header)
|
||||
print(" " + "-" * (len(header) - 2))
|
||||
|
||||
for plugin in plugins:
|
||||
module = plugin["module"]
|
||||
name = plugin["name"]
|
||||
source = plugin["source"]
|
||||
description = getattr(module, "PLUGIN_DESCRIPTION", "")
|
||||
file_types = getattr(module, "FILE_TYPES", ["*"])
|
||||
types_str = ", ".join(file_types)
|
||||
|
||||
print(f" {name:<{col_name}}{source:<{col_source}}{types_str:<20} {description}")
|
||||
print_header("SEEDGO — Plugins", f"{len(plugins)} plugin(s) discovered")
|
||||
print_plugin_table(plugins)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
"""Rich-based display utilities for seedgo CLI output.
|
||||
|
||||
Provides consistent, styled terminal output using the Rich library.
|
||||
All seedgo CLI commands use these functions for human-facing output.
|
||||
|
||||
Functions:
|
||||
console — Shared Rich Console instance.
|
||||
print_header — Branded section header with optional subtitle.
|
||||
print_plugin — Plugin check result line (name + verdict + score).
|
||||
print_check_item — Individual check item with severity marker.
|
||||
print_summary — Overall score and verdict.
|
||||
print_counts — Check count summary (passed/failed/warnings).
|
||||
print_plugin_table — Table of discovered plugins (for `seedgo list`).
|
||||
print_init — Init success message.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
from rich import box
|
||||
|
||||
from .models import Severity
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def print_header(title: str, subtitle: str | None = None) -> None:
|
||||
"""Print a branded seedgo header panel."""
|
||||
content = f"[bold cyan]{title}[/bold cyan]"
|
||||
if subtitle:
|
||||
content += f"\n[dim]{subtitle}[/dim]"
|
||||
console.print(Panel(content, border_style="cyan", box=box.ROUNDED, expand=False, padding=(0, 2)))
|
||||
console.print()
|
||||
|
||||
|
||||
def print_plugin(name: str, file_path: str, passed: bool, score: int) -> None:
|
||||
"""Print a plugin result header line."""
|
||||
if passed:
|
||||
verdict = "[green]PASS[/green]"
|
||||
icon = "[green]✓[/green]"
|
||||
else:
|
||||
verdict = "[red]FAIL[/red]"
|
||||
icon = "[red]✗[/red]"
|
||||
|
||||
# Truncate long paths for readability
|
||||
path_display = file_path
|
||||
if len(path_display) > 40:
|
||||
path_display = "..." + path_display[-37:]
|
||||
|
||||
dots = "[dim]" + "·" * max(1, 48 - len(name) - len(path_display)) + "[/dim]"
|
||||
console.print(f" {icon} [bold]{name}[/bold] [dim]{path_display}[/dim] {dots} {verdict} [dim]({score}/100)[/dim]")
|
||||
|
||||
|
||||
def print_check_item(name: str, passed: bool, message: str, severity: Severity,
|
||||
line: int | None = None, fix_hint: str | None = None) -> None:
|
||||
"""Print a single check item with appropriate severity marker."""
|
||||
if passed:
|
||||
marker = "[green]✓[/green]"
|
||||
name_style = "dim"
|
||||
elif severity == Severity.ERROR:
|
||||
marker = "[red]✗[/red]"
|
||||
name_style = "red"
|
||||
elif severity == Severity.WARNING:
|
||||
marker = "[yellow]⚠[/yellow]"
|
||||
name_style = "yellow"
|
||||
else:
|
||||
marker = "[cyan]ℹ[/cyan]"
|
||||
name_style = "cyan"
|
||||
|
||||
line_ref = f" [dim]\\[line {line}][/dim]" if line is not None else ""
|
||||
console.print(f" {marker} [{name_style}]{name}[/{name_style}]: {message}{line_ref}")
|
||||
|
||||
if fix_hint and not passed:
|
||||
console.print(f" [dim]hint: {fix_hint}[/dim]")
|
||||
|
||||
|
||||
def print_summary(score: int, passed: bool, threshold: int) -> None:
|
||||
"""Print the overall score and verdict."""
|
||||
if passed:
|
||||
verdict = "[bold green]PASS[/bold green]"
|
||||
else:
|
||||
verdict = "[bold red]FAIL[/bold red]"
|
||||
|
||||
console.print(f" [bold]Overall:[/bold] {score}/100 — {verdict} [dim](threshold: {threshold})[/dim]")
|
||||
|
||||
|
||||
def print_counts(plugins_passed: int, plugins_failed: int,
|
||||
error_count: int = 0, warning_count: int = 0) -> None:
|
||||
"""Print the check count summary line."""
|
||||
total = plugins_passed + plugins_failed
|
||||
parts = [f"{total} check(s) ran", f"{plugins_passed} passed", f"{plugins_failed} failed"]
|
||||
if error_count:
|
||||
parts.append(f"[red]{error_count} error(s)[/red]")
|
||||
if warning_count:
|
||||
parts.append(f"[yellow]{warning_count} warning(s)[/yellow]")
|
||||
|
||||
console.print(" " + ", ".join(parts))
|
||||
|
||||
|
||||
def print_separator() -> None:
|
||||
"""Print a horizontal separator."""
|
||||
console.print()
|
||||
console.print("[dim]" + "─" * 56 + "[/dim]")
|
||||
console.print()
|
||||
|
||||
|
||||
def print_plugin_table(plugins: list[dict]) -> None:
|
||||
"""Print a Rich table of discovered plugins."""
|
||||
table = Table(
|
||||
show_header=True,
|
||||
header_style="bold cyan",
|
||||
border_style="dim",
|
||||
box=box.ROUNDED,
|
||||
expand=False,
|
||||
)
|
||||
table.add_column("Plugin", style="bold")
|
||||
table.add_column("Source", style="dim")
|
||||
table.add_column("File Types")
|
||||
table.add_column("Description", style="dim")
|
||||
|
||||
for plugin in plugins:
|
||||
module = plugin["module"]
|
||||
name = plugin["name"]
|
||||
source = plugin["source"]
|
||||
description = getattr(module, "PLUGIN_DESCRIPTION", "")
|
||||
file_types = getattr(module, "FILE_TYPES", ["*"])
|
||||
types_str = ", ".join(file_types)
|
||||
table.add_row(name, source, types_str, description)
|
||||
|
||||
console.print(table)
|
||||
|
||||
|
||||
def print_init_success(config_path: str, plugins_path: str, profile: str | None = None) -> None:
|
||||
"""Print init success message."""
|
||||
console.print()
|
||||
console.print("[green]✓[/green] [bold]Seed Go initialized[/bold]")
|
||||
console.print()
|
||||
console.print(f" Config: [dim]{config_path}[/dim]")
|
||||
console.print(f" Plugins: [dim]{plugins_path}[/dim]")
|
||||
if profile:
|
||||
console.print(f" Profile: [cyan]{profile}[/cyan]")
|
||||
console.print()
|
||||
console.print(" [bold]Next steps:[/bold]")
|
||||
console.print(" 1. Add plugins to .seedgo/plugins/")
|
||||
console.print(" 2. Run: [cyan]seedgo check[/cyan]")
|
||||
console.print()
|
||||
|
||||
|
||||
def print_no_results() -> None:
|
||||
"""Print message when no checks ran."""
|
||||
console.print("[dim]No checks ran.[/dim]")
|
||||
|
||||
|
||||
def print_error(message: str, suggestion: str | None = None) -> None:
|
||||
"""Print an error message."""
|
||||
console.print(f"[red]✗[/red] [red bold]{message}[/red bold]")
|
||||
if suggestion:
|
||||
console.print(f" [yellow]→ {suggestion}[/yellow]")
|
||||
|
||||
|
||||
def print_warning(message: str) -> None:
|
||||
"""Print a warning message."""
|
||||
console.print(f"[yellow]⚠[/yellow] [yellow]{message}[/yellow]")
|
||||
+20
-40
@@ -48,40 +48,20 @@ def get_introspective() -> str:
|
||||
|
||||
Called when user types `drone @seedgo` with no arguments.
|
||||
Shows connected plugins and available commands at a glance.
|
||||
"""
|
||||
# Discover plugins by calling seedgo list and parsing output
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "seedgo", "list"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
plugin_output = result.stdout if result.stdout else ""
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
plugin_output = ""
|
||||
|
||||
# Count plugins from the output
|
||||
# seedgo list outputs lines like " plugin-name source file_types description"
|
||||
plugin_lines = []
|
||||
in_table = False
|
||||
for line in plugin_output.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("---"):
|
||||
in_table = True
|
||||
continue
|
||||
if in_table and stripped:
|
||||
parts = stripped.split()
|
||||
if len(parts) >= 2:
|
||||
name = parts[0]
|
||||
# Find description — everything after the file types column
|
||||
# The format is: NAME SOURCE FILE_TYPES DESCRIPTION
|
||||
desc = ""
|
||||
if len(parts) >= 4:
|
||||
# Rejoin from 3rd column onwards as description
|
||||
# Actually the columns are fixed-width, let's just grab the name
|
||||
desc = " ".join(parts[3:]) if len(parts) > 3 else ""
|
||||
plugin_lines.append((name, desc))
|
||||
Uses direct import of seedgo's discovery module instead of parsing
|
||||
CLI output, since the adapter lives in the same package.
|
||||
"""
|
||||
# Import discovery directly — no subprocess parsing needed
|
||||
try:
|
||||
from seedgo.discovery import discover_plugins
|
||||
except ImportError:
|
||||
return f"SEEDGO — Code Standards Framework (v{DRONE_MODULE['version']})\n\nDiscovered Plugins: 0\n"
|
||||
|
||||
try:
|
||||
plugins = discover_plugins(None)
|
||||
except Exception:
|
||||
plugins = []
|
||||
|
||||
lines = []
|
||||
lines.append(f"SEEDGO — Code Standards Framework (v{DRONE_MODULE['version']})")
|
||||
@@ -89,16 +69,16 @@ def get_introspective() -> str:
|
||||
lines.append("Auto-discovered plugin orchestration")
|
||||
lines.append("")
|
||||
|
||||
if plugin_lines:
|
||||
lines.append(f"Discovered Plugins: {len(plugin_lines)}")
|
||||
if plugins:
|
||||
lines.append(f"Discovered Plugins: {len(plugins)}")
|
||||
lines.append("")
|
||||
# Find max name length for alignment
|
||||
max_name = max(len(name) for name, _ in plugin_lines) if plugin_lines else 20
|
||||
for name, desc in plugin_lines:
|
||||
max_name = max(len(p["name"]) for p in plugins)
|
||||
for p in plugins:
|
||||
desc = getattr(p["module"], "PLUGIN_DESCRIPTION", "")
|
||||
if desc:
|
||||
lines.append(f" * {name:<{max_name}} {desc}")
|
||||
lines.append(f" * {p['name']:<{max_name}} {desc}")
|
||||
else:
|
||||
lines.append(f" * {name}")
|
||||
lines.append(f" * {p['name']}")
|
||||
else:
|
||||
lines.append("Discovered Plugins: 0")
|
||||
|
||||
|
||||
+56
-111
@@ -3,10 +3,9 @@ Seed Go Result Reporter
|
||||
|
||||
Formats check results for display in three output modes:
|
||||
|
||||
"human" — Colored terminal output using raw ANSI escape codes.
|
||||
"human" — Rich-formatted terminal output using the display module.
|
||||
Shows plugin names, pass/fail verdicts, scores, and per-check
|
||||
items with severity markers. Summary line at the bottom.
|
||||
No external dependencies (no Rich, no colorama).
|
||||
|
||||
"json" — Machine-readable JSON dict. Includes all result data and the
|
||||
overall summary. Safe for piping to other tools.
|
||||
@@ -20,19 +19,15 @@ All formats are produced by a single entry point: report_results().
|
||||
import dataclasses
|
||||
import json
|
||||
from .models import CheckResult, Severity
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ANSI color codes (raw escape sequences — zero dependencies)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_RESET = "\033[0m"
|
||||
_BOLD = "\033[1m"
|
||||
_RED = "\033[31m"
|
||||
_GREEN = "\033[32m"
|
||||
_YELLOW = "\033[33m"
|
||||
_CYAN = "\033[36m"
|
||||
_DIM = "\033[2m"
|
||||
from .display import (
|
||||
print_header,
|
||||
print_plugin,
|
||||
print_check_item,
|
||||
print_summary,
|
||||
print_counts,
|
||||
print_separator,
|
||||
print_no_results,
|
||||
)
|
||||
|
||||
|
||||
def report_results(
|
||||
@@ -51,18 +46,21 @@ def report_results(
|
||||
threshold, plugins_passed, plugins_failed, error_count,
|
||||
warning_count, info_count.
|
||||
format: Output format. One of:
|
||||
"human" — Colored terminal output (default).
|
||||
"human" — Rich terminal output (default). Prints directly,
|
||||
returns empty string.
|
||||
"json" — JSON-encoded string, machine-readable.
|
||||
"github" — GitHub Actions annotation lines.
|
||||
|
||||
Returns:
|
||||
Formatted string ready to print or write to stdout.
|
||||
For "human" format, prints directly and returns empty string.
|
||||
|
||||
Raises:
|
||||
ValueError: If format is not one of the three supported values.
|
||||
"""
|
||||
if format == "human":
|
||||
return _format_human(results, overall)
|
||||
_format_human(results, overall)
|
||||
return ""
|
||||
elif format == "json":
|
||||
return _format_json(results, overall)
|
||||
elif format == "github":
|
||||
@@ -76,112 +74,59 @@ def report_results(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _format_human(results: list[CheckResult], overall: dict) -> str:
|
||||
"""Produce colored terminal output for human consumption.
|
||||
def _format_human(results: list[CheckResult], overall: dict) -> None:
|
||||
"""Print Rich-formatted terminal output for human consumption.
|
||||
|
||||
Prints directly to the Rich console via the display module.
|
||||
|
||||
Layout:
|
||||
plugin-name .............. PASS (100/100)
|
||||
another-plugin ........... FAIL (60/100)
|
||||
✗ check-name: message [line N]
|
||||
hint: fix_hint text
|
||||
✓ passing-check: message
|
||||
╭─ SEEDGO ─────────────────────────────╮
|
||||
│ Code Standards Check │
|
||||
│ 5 plugins · 3 files · threshold: 75 │
|
||||
╰───────────────────────────────────────╯
|
||||
|
||||
✓ plugin-name file.py ········· PASS (100/100)
|
||||
✓ check-name: message
|
||||
✗ another-plugin file.py ······ FAIL (60/100)
|
||||
✗ check-name: message [line N]
|
||||
hint: fix_hint text
|
||||
|
||||
─────────────────────────────────────────
|
||||
Overall: 80/100 — PASS (threshold: 75)
|
||||
2 checks ran, 1 passed, 1 failed
|
||||
"""
|
||||
lines: list[str] = []
|
||||
|
||||
if not results:
|
||||
lines.append(f"{_DIM}No checks ran.{_RESET}")
|
||||
lines.append(_summary_line(overall))
|
||||
return "\n".join(lines)
|
||||
|
||||
# Group results by plugin name for a cleaner display
|
||||
for result in results:
|
||||
lines.append(_plugin_header_line(result))
|
||||
for item in result.checks:
|
||||
lines.extend(_check_item_lines(item))
|
||||
|
||||
lines.append("")
|
||||
lines.append(_summary_line(overall))
|
||||
lines.append(_counts_line(overall))
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _plugin_header_line(result: CheckResult) -> str:
|
||||
"""Format the plugin name + verdict + score header line."""
|
||||
name = result.plugin
|
||||
score = result.score
|
||||
file_label = f" [{result.file_path}]" if result.file_path else ""
|
||||
|
||||
dots = "." * max(1, 50 - len(name) - len(file_label))
|
||||
|
||||
if result.passed:
|
||||
verdict = f"{_GREEN}PASS{_RESET}"
|
||||
else:
|
||||
verdict = f"{_RED}FAIL{_RESET}"
|
||||
|
||||
score_str = f"({score}/100)"
|
||||
return f" {_BOLD}{name}{_RESET}{file_label} {_DIM}{dots}{_RESET} {verdict} {_DIM}{score_str}{_RESET}"
|
||||
|
||||
|
||||
def _check_item_lines(item) -> list[str]:
|
||||
"""Format a single CheckItem into one or two display lines."""
|
||||
lines: list[str] = []
|
||||
|
||||
if item.passed:
|
||||
marker = f"{_GREEN}✓{_RESET}"
|
||||
color = _DIM
|
||||
elif item.severity == Severity.ERROR:
|
||||
marker = f"{_RED}✗{_RESET}"
|
||||
color = _RED
|
||||
elif item.severity == Severity.WARNING:
|
||||
marker = f"{_YELLOW}⚠{_RESET}"
|
||||
color = _YELLOW
|
||||
else:
|
||||
marker = f"{_CYAN}ℹ{_RESET}"
|
||||
color = _CYAN
|
||||
|
||||
line_ref = f" [line {item.line}]" if item.line is not None else ""
|
||||
main = f" {marker} {color}{item.name}{_RESET}: {item.message}{line_ref}"
|
||||
lines.append(main)
|
||||
|
||||
if item.fix_hint and not item.passed:
|
||||
lines.append(f" {_DIM}hint: {item.fix_hint}{_RESET}")
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def _summary_line(overall: dict) -> str:
|
||||
"""Format the overall score summary line."""
|
||||
score = overall.get("overall_score", 100)
|
||||
passed = overall.get("passed", True)
|
||||
threshold = overall.get("threshold", 75)
|
||||
|
||||
if passed:
|
||||
verdict = f"{_GREEN}{_BOLD}PASS{_RESET}"
|
||||
else:
|
||||
verdict = f"{_RED}{_BOLD}FAIL{_RESET}"
|
||||
|
||||
return f" {_BOLD}Overall:{_RESET} {score}/100 — {verdict} {_DIM}(threshold: {threshold}){_RESET}"
|
||||
|
||||
|
||||
def _counts_line(overall: dict) -> str:
|
||||
"""Format the plugin count summary line."""
|
||||
passed = overall.get("passed", True)
|
||||
p_passed = overall.get("plugins_passed", 0)
|
||||
p_failed = overall.get("plugins_failed", 0)
|
||||
total = p_passed + p_failed
|
||||
errors = overall.get("error_count", 0)
|
||||
warnings = overall.get("warning_count", 0)
|
||||
|
||||
parts = [f"{total} check(s) ran", f"{p_passed} passed", f"{p_failed} failed"]
|
||||
if errors:
|
||||
parts.append(f"{_RED}{errors} error(s){_RESET}")
|
||||
if warnings:
|
||||
parts.append(f"{_YELLOW}{warnings} warning(s){_RESET}")
|
||||
# Header
|
||||
total_plugins = p_passed + p_failed
|
||||
subtitle = f"{total_plugins} plugin(s) · threshold: {threshold}"
|
||||
print_header("SEEDGO — Code Standards Check", subtitle)
|
||||
|
||||
return " " + ", ".join(parts)
|
||||
if not results:
|
||||
print_no_results()
|
||||
print_separator()
|
||||
print_summary(score, passed, threshold)
|
||||
return
|
||||
|
||||
for result in results:
|
||||
print_plugin(result.plugin, result.file_path, result.passed, result.score)
|
||||
for item in result.checks:
|
||||
print_check_item(
|
||||
item.name, item.passed, item.message, item.severity,
|
||||
line=item.line, fix_hint=item.fix_hint,
|
||||
)
|
||||
|
||||
print_separator()
|
||||
print_summary(score, passed, threshold)
|
||||
print_counts(
|
||||
p_passed, p_failed,
|
||||
error_count=overall.get("error_count", 0),
|
||||
warning_count=overall.get("warning_count", 0),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
+25
-18
@@ -548,6 +548,8 @@ def check(file_path, config=None):
|
||||
|
||||
|
||||
class TestReportHuman:
|
||||
"""Human format prints directly via Rich console. Tests capture stdout."""
|
||||
|
||||
def _make_result(self, passed=True, score=100, checks=None):
|
||||
return CheckResult(
|
||||
plugin="test-plugin",
|
||||
@@ -569,34 +571,39 @@ class TestReportHuman:
|
||||
"info_count": 0,
|
||||
}
|
||||
|
||||
def test_output_contains_plugin_name(self):
|
||||
def _report_captured(self, results, overall, capsys):
|
||||
"""Call report_results for human format and return captured output."""
|
||||
report_results(results, overall, format="human")
|
||||
return capsys.readouterr().out
|
||||
|
||||
def test_output_contains_plugin_name(self, capsys):
|
||||
result = self._make_result()
|
||||
output = report_results([result], self._overall())
|
||||
output = self._report_captured([result], self._overall(), capsys)
|
||||
assert "test-plugin" in output
|
||||
|
||||
def test_passing_result_contains_pass(self):
|
||||
def test_passing_result_contains_pass(self, capsys):
|
||||
result = self._make_result(passed=True, score=100)
|
||||
output = report_results([result], self._overall(score=100, passed=True))
|
||||
output = self._report_captured([result], self._overall(score=100, passed=True), capsys)
|
||||
assert "PASS" in output
|
||||
|
||||
def test_failing_result_contains_fail(self):
|
||||
def test_failing_result_contains_fail(self, capsys):
|
||||
checks = [CheckItem(name="c", passed=False, message="fail", severity=Severity.ERROR)]
|
||||
result = self._make_result(passed=False, score=0, checks=checks)
|
||||
output = report_results([result], self._overall(score=0, passed=False))
|
||||
output = self._report_captured([result], self._overall(score=0, passed=False), capsys)
|
||||
assert "FAIL" in output
|
||||
|
||||
def test_score_shown_in_output(self):
|
||||
def test_score_shown_in_output(self, capsys):
|
||||
result = self._make_result(score=75)
|
||||
output = report_results([result], self._overall(score=75))
|
||||
output = self._report_captured([result], self._overall(score=75), capsys)
|
||||
assert "75" in output
|
||||
|
||||
def test_check_item_message_shown(self):
|
||||
def test_check_item_message_shown(self, capsys):
|
||||
checks = [CheckItem(name="bare-except", passed=False, message="Found bare except", severity=Severity.ERROR)]
|
||||
result = self._make_result(passed=False, checks=checks)
|
||||
output = report_results([result], self._overall(passed=False))
|
||||
output = self._report_captured([result], self._overall(passed=False), capsys)
|
||||
assert "Found bare except" in output
|
||||
|
||||
def test_fix_hint_shown_for_failing_check(self):
|
||||
def test_fix_hint_shown_for_failing_check(self, capsys):
|
||||
checks = [
|
||||
CheckItem(
|
||||
name="c",
|
||||
@@ -607,22 +614,22 @@ class TestReportHuman:
|
||||
)
|
||||
]
|
||||
result = self._make_result(passed=False, checks=checks)
|
||||
output = report_results([result], self._overall(passed=False))
|
||||
output = self._report_captured([result], self._overall(passed=False), capsys)
|
||||
assert "Use except Exception:" in output
|
||||
|
||||
def test_no_results_shows_no_checks_ran(self):
|
||||
output = report_results([], self._overall())
|
||||
def test_no_results_shows_no_checks_ran(self, capsys):
|
||||
output = self._report_captured([], self._overall(), capsys)
|
||||
assert "No checks ran" in output
|
||||
|
||||
def test_threshold_shown_in_summary(self):
|
||||
def test_threshold_shown_in_summary(self, capsys):
|
||||
result = self._make_result()
|
||||
output = report_results([result], self._overall(threshold=80))
|
||||
output = self._report_captured([result], self._overall(threshold=80), capsys)
|
||||
assert "80" in output
|
||||
|
||||
def test_line_number_shown_when_present(self):
|
||||
def test_line_number_shown_when_present(self, capsys):
|
||||
checks = [CheckItem(name="c", passed=False, message="fail", severity=Severity.ERROR, line=42)]
|
||||
result = self._make_result(passed=False, checks=checks)
|
||||
output = report_results([result], self._overall(passed=False))
|
||||
output = self._report_captured([result], self._overall(passed=False), capsys)
|
||||
assert "42" in output
|
||||
|
||||
def test_invalid_format_raises_value_error(self):
|
||||
|
||||
Reference in New Issue
Block a user