diff --git a/PLAN_CODE_SMELLS.md b/PLAN_CODE_SMELLS.md deleted file mode 100644 index 1c17b667..00000000 --- a/PLAN_CODE_SMELLS.md +++ /dev/null @@ -1,47 +0,0 @@ -# Code Smells — Future Issues - -Identified during the Pydantic v2 migration (PR #306). These are **not** related to the migration itself and should be addressed in separate issues/PRs. - ---- - -## High Severity - -### ~~Scattered `sys.exit()` in location classes~~ — FIXED in PR #329 -- `LocationError` / `ArtifactDownloadError` / `ArtifactExtractionError` added to exception hierarchy -- Location classes now raise exceptions; only `command.py` calls `sys.exit()` - -### ~~Mutable singleton `TempDirectoryUtil`~~ — FIXED in PR #330 -- Replaced with instance-based `TempDirectoryManager` (context manager, DI, guaranteed cleanup) - ---- - -## Medium Severity - -### ~~Duplicated filter parsing logic~~ — FIXED in PR #332 -- Extracted `parse_filters()` into `common/filter_parser.py`; both generators reduced to 3-line calls; `# NOSONAR` removed - -### Monolithic `status.py` with manual table rendering (322 lines) -- **Files:** `commands/status/status.py` -- **Problem:** Still manually constructs Unicode box-drawing characters and ANSI color codes (`colorama`, `\033[38;5;208m`). -- **Fix:** Replace manual table rendering with **Rich** library. Eliminates ~120 lines of string manipulation. - -### ~~No unit tests for CLI entry point~~ — FIXED in PR #331 -- Added `tests/unit/reqstool/test_command.py` with 12 tests covering routing, deprecation warnings, error handling, argument parsing - ---- - -## Low Severity - -### ~~Repetitive CLI parser setup~~ — FIXED in PR #333 -- Replaced 52-line repetitive body of `_add_subparsers_source` with `_LOCATION_DEFS` config dict + 16-line loop - ---- - -## Resolved (no longer valid) - -### `@dataclass` in generators/validators — RESOLVED -- `combined_indexed_dataset_generator.py` and `indexed_dataset_filter_processor.py` no longer exist -- Remaining `@dataclass` use in `syntax_validator.py` is legitimate (schema registry item) - -### Expression language — empty subclasses — RESOLVED -- `requirements_el.py` and `svcs_el.py` no longer exist; generic transformer used directly diff --git a/pyproject.toml b/pyproject.toml index 230614de..bc1f5a41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,6 @@ classifiers = [ ] requires-python = ">=3.13" dependencies = [ - "colorama==0.4.6", "Jinja2==3.1.6", "jsonschema[format-nongpl]==4.26.0", "lark==1.3.1", @@ -45,8 +44,8 @@ dependencies = [ "pygit2==1.19.1", "referencing==0.37.0", "requests-file==3.0.1", + "rich>=13.0", "ruamel.yaml==0.19.1", - "tabulate==0.10.0", "reqstool-python-decorators==0.1.0", "packaging==26.0", "requests==2.32.5", diff --git a/src/reqstool/commands/status/status.py b/src/reqstool/commands/status/status.py index ac220f53..49105849 100644 --- a/src/reqstool/commands/status/status.py +++ b/src/reqstool/commands/status/status.py @@ -3,10 +3,14 @@ from __future__ import annotations import json +import re +import shutil -from colorama import Fore, Style +from rich.columns import Columns +from rich.console import Console +from rich.table import Table, box +from rich.text import Text from reqstool_python_decorators.decorators.decorators import Requirements -from tabulate import tabulate from reqstool.common.validator_error_holder import ValidationErrorHolder from reqstool.common.validators.semantic_validator import SemanticValidator @@ -17,6 +21,24 @@ from reqstool.storage.requirements_repository import RequirementsRepository +_ORANGE = "dark_orange" +_DIM = "dim" +_ANSI_ESCAPE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") + + +def _make_console() -> Console: + width = max(80, shutil.get_terminal_size((120, 24)).columns) + return Console(highlight=False, force_terminal=True, color_system="standard", width=width) + + +def _render(*renderables) -> str: + console = _make_console() + with console.capture() as cap: + for r in renderables: + console.print(r) + return cap.get() + + @Requirements("REQ_027") class StatusCommand: def __init__(self, location: LocationInterface, format: str = "console"): @@ -44,6 +66,32 @@ def __status_result(self) -> tuple[str, int]: ) +def _format_test_cell(stats: TestStats) -> Text: + """Format a TestStats into a Rich Text object with colored counts.""" + if stats.not_applicable: + return Text() + + def _slot(value: int, style: str) -> Text: + t = Text() + if value == 0: + t.append(" -", style=_DIM) + else: + t.append(f"{value:>2}", style=style) + return t + + cell = Text() + cell.append_text(_slot(stats.total, "default")) + cell.append(" ") + cell.append_text(_slot(stats.passed, "green")) + cell.append(" ") + cell.append_text(_slot(stats.failed, "red")) + cell.append(" ") + cell.append_text(_slot(stats.skipped, "yellow")) + cell.append(" ") + cell.append_text(_slot(stats.missing, _ORANGE)) + return cell + + def _build_table( req_id: str, urn: str, @@ -52,24 +100,23 @@ def _build_table( mvrs: TestStats, completed: bool, implementation: IMPLEMENTATION, -) -> list[str]: - row = [urn] - # add color to requirement if it's completed or not - req_id_color = f"{Fore.GREEN}" if completed else f"{Fore.RED}" - row.append(f"{req_id_color}{req_id}{Style.RESET_ALL}") - - # Perform check for implementations +) -> list: + id_style = "green" if completed else "red" + row = [ + Text(urn), + Text(req_id, style=id_style), + ] if implementation == IMPLEMENTATION.NOT_APPLICABLE: - row.extend(["N/A"]) + row.append(Text("N/A", style="dim")) else: - color = Fore.GREEN if impls > 0 else Fore.RED - row.extend([f"{color}{impls}{Style.RESET_ALL}"]) - _extend_row(tests, row, kind="automated") - _extend_row(mvrs, row, kind="manual") + impl_style = "green" if impls > 0 else "red" + row.append(Text(str(impls), style=impl_style)) + row.append(_format_test_cell(tests)) + row.append(_format_test_cell(mvrs)) return row -def _get_row_with_totals(stats_service: StatisticsService) -> list[str]: +def _get_row_with_totals(stats_service: StatisticsService) -> list: ts = stats_service.total_statistics total_automatic = ts.passed_automatic_tests + ts.failed_automatic_tests total_manual = ts.passed_manual_tests + ts.failed_manual_tests @@ -93,22 +140,44 @@ def _get_row_with_totals(stats_service: StatisticsService) -> list[str]: missing=ts.missing_manual_tests, ) return [ - "Total", - "", - str(total_implementations), + Text("Total"), + Text(""), + Text(str(total_implementations)), _format_test_cell(auto_stats), _format_test_cell(manual_stats), ] -# builds the status table def _status_table(stats_service: StatisticsService) -> str: - table_data = [] - headers = ["URN", "ID", "Implementation", "Automated Tests", "Manual Tests"] + ts = stats_service.total_statistics + + legend = Text("T = Total, ") + legend.append("P = Passed", style="green") + legend.append(", ") + legend.append("F = Failed", style="red") + legend.append(", ") + legend.append("S = Skipped", style="yellow") + legend.append(", ") + legend.append("M = Missing", style=_ORANGE) + + table = Table( + box=box.DOUBLE_EDGE, + show_header=True, + header_style="bold", + show_lines=True, + title=f"REQUIREMENTS: {ts.total_requirements}", + title_style="bold", + caption=legend, + ) + table.add_column("URN", justify="center") + table.add_column("ID", justify="left") + table.add_column("Implementation", justify="center") + table.add_column("Automated Tests", justify="center") + table.add_column("Manual Tests", justify="center") for req, stats in stats_service.requirement_statistics.items(): - table_data.append( - _build_table( + table.add_row( + *_build_table( req_id=req.id, urn=req.urn, impls=stats.implementations, @@ -119,41 +188,12 @@ def _status_table(stats_service: StatisticsService) -> str: ) ) - table_data.append(_get_row_with_totals(stats_service)) - - col_align = ["center"] * len(headers) if table_data else [] - table = tabulate(tablefmt="fancy_grid", tabular_data=table_data, headers=headers, colalign=col_align) - - lines = table.split("\n") - - # Find a line without ANSI codes to measure visible width - visible_table_width = 75 - for line in lines: - if "╞" in line or "╘" in line: - visible_table_width = len(line) - break - - ts = stats_service.total_statistics - header_req_data = ("\b" * len(str(ts.total_requirements))) + f"REQUIREMENTS: {str(ts.total_requirements)}" - inner_width = visible_table_width - 2 # subtract ╒ and ╕ - title = ( - "╒" + "═" * inner_width + "╕" + f"\n│{header_req_data.center(inner_width)}│" + "\n╘" + "═" * inner_width + "╛" - ) - - table_with_title = f"{title}\n{table}\n" - - legend_line = ( - f"T = Total, {Fore.GREEN}P = Passed{Style.RESET_ALL}, " - f"{Fore.RED}F = Failed{Style.RESET_ALL}, " - f"{Fore.YELLOW}S = Skipped{Style.RESET_ALL}, " - f"{_ORANGE}M = Missing{Style.RESET_ALL}" - ) + table.add_section() + table.add_row(*_get_row_with_totals(stats_service)) statistics = _summarize_statistics(ts) - status = table_with_title + legend_line + statistics - - return status + return _render(table) + statistics def _summarize_statistics(ts: TotalStats) -> str: @@ -162,104 +202,81 @@ def _summarize_statistics(ts: TotalStats) -> str: code_reqs = ts.total_requirements - nr_of_reqs_without_implementation code_completed = ts.completed_requirements - nr_of_completed_reqs_without_implementation - header_test_data = ("\b" * len(str(ts.total_tests))) + f"Total Tests: {str(ts.total_tests)}" - header_svcs_data = ("\b" * len(str(ts.total_svcs))) + f"Total SVCs: {str(ts.total_svcs)}" - CODE, NA, IMPLEMENTATIONS = __colorize_headers( - total=ts.total_requirements, - total_completed=ts.completed_requirements, - total_reqs_no_impl=nr_of_reqs_without_implementation, - completed_reqs_no_impl=nr_of_completed_reqs_without_implementation, + CODE, NA, IMPLEMENTATIONS = __colorize_headers() + + # Code group: 4 stats (total, implemented, verified, not verified) + code_table = Table(box=box.DOUBLE_EDGE, show_header=True, title=CODE, title_justify="center") + code_table.add_column("Total", justify="center") + code_table.add_column("Implemented", justify="center") + code_table.add_column("Verified", justify="center") + code_table.add_column("Not Verified", justify="center") + code_table.add_row( + str(code_reqs) + __numbers_as_percentage(numerator=code_reqs, denominator=code_reqs), + str(ts.with_implementation) + __numbers_as_percentage(numerator=ts.with_implementation, denominator=code_reqs), + str(code_completed) + __numbers_as_percentage(numerator=code_completed, denominator=code_reqs), + str(ts.total_requirements - (nr_of_reqs_without_implementation + code_completed)) + + __numbers_as_percentage( + numerator=(ts.total_requirements - (nr_of_reqs_without_implementation + code_completed)), + denominator=code_reqs, + ), ) - implementation_data = [ - [ - str(code_reqs) + __numbers_as_percentage(numerator=code_reqs, denominator=code_reqs), - str(ts.with_implementation) - + __numbers_as_percentage(numerator=ts.with_implementation, denominator=code_reqs), - str(code_completed) + __numbers_as_percentage(numerator=code_completed, denominator=code_reqs), - str(ts.total_requirements - (nr_of_reqs_without_implementation + code_completed)) - + __numbers_as_percentage( - numerator=(ts.total_requirements - (nr_of_reqs_without_implementation + code_completed)), - denominator=code_reqs, - ), - str(nr_of_reqs_without_implementation) - + __numbers_as_percentage( - numerator=nr_of_reqs_without_implementation, - denominator=nr_of_reqs_without_implementation, - ), - str(nr_of_completed_reqs_without_implementation) - + __numbers_as_percentage( - numerator=nr_of_completed_reqs_without_implementation, - denominator=nr_of_reqs_without_implementation, - ), - str(nr_of_reqs_without_implementation - nr_of_completed_reqs_without_implementation) - + __numbers_as_percentage( - numerator=(nr_of_reqs_without_implementation - nr_of_completed_reqs_without_implementation), - denominator=nr_of_reqs_without_implementation, - ), - ] - ] - - table_svc_data = [ - [ - str(ts.passed_tests) + __numbers_as_percentage(numerator=ts.passed_tests, denominator=ts.total_tests), - str(ts.failed_tests) + __numbers_as_percentage(numerator=ts.failed_tests, denominator=ts.total_tests), - str(ts.skipped_tests) + __numbers_as_percentage(numerator=ts.skipped_tests, denominator=ts.total_tests), - str(ts.missing_automated_tests) - + __numbers_as_percentage(numerator=ts.missing_automated_tests, denominator=ts.total_svcs), - str(ts.missing_manual_tests) - + __numbers_as_percentage(numerator=ts.missing_manual_tests, denominator=ts.total_svcs), - ] - ] - - implementation_headers = ["Total", "Implemented", "Verified", "Not Verified", "Total", "Verified", "Not Verified"] - - svc_headers = [ - "Passed tests", - "Failed tests", - "Skipped tests", - "SVCs missing tests", - "SVCs missing MVRs", - ] - - svc_table = tabulate( - tablefmt="fancy_grid", - tabular_data=table_svc_data, - headers=svc_headers, - colalign=["center"] * len(table_svc_data[0]), + # N/A group: 3 stats (total, verified, not verified) + na_table = Table(box=box.DOUBLE_EDGE, show_header=True, title=NA, title_justify="center") + na_table.add_column("Total", justify="center") + na_table.add_column("Verified", justify="center") + na_table.add_column("Not Verified", justify="center") + na_table.add_row( + str(nr_of_reqs_without_implementation) + + __numbers_as_percentage( + numerator=nr_of_reqs_without_implementation, + denominator=nr_of_reqs_without_implementation, + ), + str(nr_of_completed_reqs_without_implementation) + + __numbers_as_percentage( + numerator=nr_of_completed_reqs_without_implementation, + denominator=nr_of_reqs_without_implementation, + ), + str(nr_of_reqs_without_implementation - nr_of_completed_reqs_without_implementation) + + __numbers_as_percentage( + numerator=(nr_of_reqs_without_implementation - nr_of_completed_reqs_without_implementation), + denominator=nr_of_reqs_without_implementation, + ), ) - implementation_table = tabulate( - tablefmt="fancy_grid", - tabular_data=implementation_data, - headers=implementation_headers, - colalign=["center"] * len(implementation_data[0]), + tests_table = Table( + box=box.DOUBLE_EDGE, show_header=True, title=f"Total Tests: {ts.total_tests}", title_style="white" ) - - total_tests_svcs_header = ( - "╒═══════════════════════════════════════════════════╤════════════════════════════════════════════╕" - f"\n│ {header_test_data} │" - f" {header_svcs_data} │" - "\n╘═══════════════════════════════════════════════════╧════════════════════════════════════════════╛" - ) - - test_header = ( - "╒═══════════════════════════════════════════════════════════╤═══════════════════════════════════════════╕" - f"\n| {CODE} │ {NA} │" - "\n╘═══════════════════════════════════════════════════════════╧═══════════════════════════════════════════╛" + tests_table.add_column("Passed tests", justify="center") + tests_table.add_column("Failed tests", justify="center") + tests_table.add_column("Skipped tests", justify="center") + tests_table.add_row( + str(ts.passed_tests) + __numbers_as_percentage(numerator=ts.passed_tests, denominator=ts.total_tests), + str(ts.failed_tests) + __numbers_as_percentage(numerator=ts.failed_tests, denominator=ts.total_tests), + str(ts.skipped_tests) + __numbers_as_percentage(numerator=ts.skipped_tests, denominator=ts.total_tests), ) - impl_header = ( - "╒═══════════════════════════════════════════════════════════════════════════════════════════════════════╕" - f"\n| {IMPLEMENTATIONS} │" - "\n╘═══════════════════════════════════════════════════════════════════════════════════════════════════════╛" + svcs_table = Table(box=box.DOUBLE_EDGE, show_header=True, title=f"Total SVCs: {ts.total_svcs}", title_style="white") + svcs_table.add_column("SVCs missing tests", justify="center") + svcs_table.add_column("SVCs missing MVRs", justify="center") + svcs_table.add_row( + str(ts.missing_automated_tests) + + __numbers_as_percentage(numerator=ts.missing_automated_tests, denominator=ts.total_svcs), + str(ts.missing_manual_tests) + + __numbers_as_percentage(numerator=ts.missing_manual_tests, denominator=ts.total_svcs), ) - table_with_title = ( - f"\n{impl_header}\n{test_header}\n" f"{implementation_table}\n{total_tests_svcs_header}\n{svc_table}" + cols_rendered = _render(Columns([code_table, na_table])) + cols_width = max( + (len(_ANSI_ESCAPE.sub("", line)) for line in cols_rendered.split("\n") if line.strip()), + default=80, ) + impl_console = Console(highlight=False, force_terminal=True, color_system="standard", width=cols_width) + with impl_console.capture() as cap: + impl_console.print(IMPLEMENTATIONS, justify="center") + impl_header = cap.get() - return table_with_title + return "\n" + impl_header + cols_rendered + _render(Columns([tests_table, svcs_table])) def __numbers_as_percentage(numerator: int, denominator: int) -> str: @@ -270,53 +287,9 @@ def __numbers_as_percentage(numerator: int, denominator: int) -> str: return percentage_as_string -def __colorize_headers( - total: int, total_completed: int, total_reqs_no_impl: int, completed_reqs_no_impl: int -) -> tuple[str, str, str]: - total_code = total - total_reqs_no_impl - total_code_completed = total_code == (total_completed - completed_reqs_no_impl) - total_no_impl_completed = total_reqs_no_impl - completed_reqs_no_impl == 0 - - CODE = f"{Fore.GREEN}{'Code'}{Style.RESET_ALL}" if total_code_completed else f"{Fore.RED}{'Code'}{Style.RESET_ALL}" - NA = f"{Fore.GREEN}{'N/A'}{Style.RESET_ALL}" if total_no_impl_completed else f"{Fore.RED}{'N/A'}{Style.RESET_ALL}" - IMPLEMENTATIONS = ( - f"{Fore.GREEN}{'IMPLEMENTATIONS'}{Style.RESET_ALL}" - if total == total_completed - else f"{Fore.RED}{'IMPLEMENTATIONS'}{Style.RESET_ALL}" +def __colorize_headers() -> tuple[Text, Text, Text]: + return ( + Text("In Code", style="white"), + Text("Not in Code", style="white"), + Text("IMPLEMENTATIONS", style="bold white"), ) - - return CODE, NA, IMPLEMENTATIONS - - -_ORANGE = "\033[38;5;208m" -_DIM = Style.DIM - - -def _format_test_cell(stats: TestStats) -> str: - """Format a TestStats into a single fixed-width string with colored counts.""" - if stats.not_applicable: - return "" - - slots = [ - (stats.total, ""), - (stats.passed, Fore.GREEN), - (stats.failed, Fore.RED), - (stats.skipped, Fore.YELLOW), - (stats.missing, _ORANGE), - ] - - parts = [] - for value, color in slots: - if value == 0: - parts.append(f"{_DIM} -{Style.RESET_ALL}") - else: - text = f"{value:>2}" - if color: - text = f"{color}{text}{Style.RESET_ALL}" - parts.append(text) - - return " ".join(parts) - - -def _extend_row(result: TestStats, row: list[str], kind: str) -> None: - row.append(_format_test_cell(result)) diff --git a/src/reqstool/common/validators/semantic_validator.py b/src/reqstool/common/validators/semantic_validator.py index 54eb63f3..ef2df6e3 100644 --- a/src/reqstool/common/validators/semantic_validator.py +++ b/src/reqstool/common/validators/semantic_validator.py @@ -4,9 +4,10 @@ import re from typing import List -from colorama import Fore, Style +from rich.console import Console +from rich.table import Table, box +from rich.text import Text from reqstool_python_decorators.decorators.decorators import Requirements -from tabulate import tabulate from reqstool.common.models.urn_id import UrnId from reqstool.common.utils import Utils @@ -55,21 +56,27 @@ def validate_post_parsing(self, combined_raw_dataset: CombinedRawDataset) -> Lis def _log_all_errors(self): errors = self._validation_error_holder.get_errors() - validation_result = "" - table_data = [] if len(errors) > 0: - validation_result = f"{Fore.RED}FAIL{Style.RESET_ALL}" + result_text = Text("FAIL", style="red") + error_table = Table(box=box.HEAVY_HEAD, show_header=False) + error_table.add_column("Message") for error in errors: - table_data.append([re.sub(r"\s+", " ", error.msg.strip("\n"))]) + error_table.add_row(re.sub(r"\s+", " ", error.msg.strip("\n"))) else: - validation_result = f"{Fore.GREEN}PASS{Style.RESET_ALL}" + result_text = Text("PASS", style="green") + error_table = None - title = f"\n\nVALIDATION: {validation_result}" - table = tabulate(tablefmt="fancy_grid", tabular_data=table_data) - table_with_title = f"{title}\n{table}\n" + title = Text("\n\nVALIDATION: ") + title.append_text(result_text) - logging.info(table_with_title) + console = Console(highlight=False, force_terminal=True, color_system="standard") + with console.capture() as cap: + console.print(title) + if error_table is not None: + console.print(error_table) + + logging.info(cap.get()) @Requirements("REQ_022") def _validate_no_duplicate_requirement_ids(self, data: RequirementData) -> bool: diff --git a/tests/unit/reqstool/commands/status/test_status_presentation.py b/tests/unit/reqstool/commands/status/test_status_presentation.py index 2ee3444f..29d9ba6d 100644 --- a/tests/unit/reqstool/commands/status/test_status_presentation.py +++ b/tests/unit/reqstool/commands/status/test_status_presentation.py @@ -1,33 +1,42 @@ # Copyright © LFV -from colorama import Fore, Style +from rich.console import Console -from reqstool.commands.status.status import _build_table, _extend_row, _format_test_cell, _ORANGE, _summarize_statistics +from reqstool.commands.status.status import _build_table, _format_test_cell, _summarize_statistics from reqstool.models.requirements import IMPLEMENTATION from reqstool.services.statistics_service import TestStats, TotalStats +def _render(renderable) -> str: + """Render a Rich renderable to a string with ANSI codes.""" + console = Console(highlight=False, force_terminal=True, color_system="standard") + with console.capture() as cap: + console.print(renderable, end="") + return cap.get() + + # --------------------------------------------------------------------------- # _format_test_cell # --------------------------------------------------------------------------- def test_format_test_cell_not_applicable(): - assert _format_test_cell(TestStats(not_applicable=True)) == "" + result = _format_test_cell(TestStats(not_applicable=True)) + assert result.plain == "" def test_format_test_cell_all_zeros(): result = _format_test_cell(TestStats(total=0, passed=0, failed=0, skipped=0, missing=0)) # All slots are dim dashes - assert Style.DIM in result - plain = result.replace(Style.DIM, "").replace(Style.RESET_ALL, "").replace(" ", "").replace("-", "") + rendered = _render(result) + assert "\033[2m" in rendered # dim ANSI code + plain = result.plain.replace(" ", "").replace("-", "") assert plain == "" def test_format_test_cell_mixed_values(): result = _format_test_cell(TestStats(total=3, passed=2, failed=1, skipped=0, missing=0)) - # Strip ANSI to check content - plain = result.replace(Fore.GREEN, "").replace(Fore.RED, "").replace(Fore.YELLOW, "").replace(Style.RESET_ALL, "") + plain = result.plain assert " 3" in plain assert " 2" in plain assert " 1" in plain @@ -35,53 +44,18 @@ def test_format_test_cell_mixed_values(): def test_format_test_cell_colors(): result = _format_test_cell(TestStats(total=1, passed=1, failed=2, skipped=3, missing=4)) - assert Fore.GREEN in result # passed - assert Fore.RED in result # failed - assert Fore.YELLOW in result # skipped - assert _ORANGE in result # missing + rendered = _render(result) + assert "\033[32m" in rendered # passed is green + assert "\033[31m" in rendered # failed is red + assert "\033[33m" in rendered # skipped is yellow + assert " 4" in result.plain # missing value is present def test_format_test_cell_zero_slots_are_blank(): result = _format_test_cell(TestStats(total=5, passed=0, failed=0, skipped=0, missing=3)) - plain = ( - result.replace(Fore.GREEN, "") - .replace(Fore.RED, "") - .replace(Fore.YELLOW, "") - .replace(_ORANGE, "") - .replace(Style.RESET_ALL, "") - ) - assert " 5" in plain - assert " 3" in plain - - -# --------------------------------------------------------------------------- -# _extend_row -# --------------------------------------------------------------------------- - - -def test_extend_row_not_applicable(): - """not_applicable=True appends single empty cell.""" - row = [] - _extend_row(TestStats(not_applicable=True), row, kind="automated") - assert len(row) == 1 - assert row[0] == "" - - -def test_extend_row_appends_single_cell(): - """_extend_row appends exactly one cell.""" - row = [] - _extend_row(TestStats(total=5, passed=3, failed=2), row, kind="automated") - assert len(row) == 1 - - -def test_extend_row_cell_contains_values(): - """The single cell contains the test counts.""" - row = [] - _extend_row(TestStats(total=5, passed=3, failed=2), row, kind="automated") - plain = row[0].replace(Fore.GREEN, "").replace(Fore.RED, "").replace(Fore.YELLOW, "").replace(Style.RESET_ALL, "") + plain = result.plain assert " 5" in plain assert " 3" in plain - assert " 2" in plain # --------------------------------------------------------------------------- @@ -100,8 +74,8 @@ def test_build_table_completed_req_is_green(): completed=True, implementation=IMPLEMENTATION.IN_CODE, ) - assert Fore.GREEN in row[1] - assert "REQ_001" in row[1] + assert row[1].style == "green" + assert "REQ_001" in row[1].plain def test_build_table_incomplete_req_is_red(): @@ -115,7 +89,7 @@ def test_build_table_incomplete_req_is_red(): completed=False, implementation=IMPLEMENTATION.IN_CODE, ) - assert Fore.RED in row[1] + assert row[1].style == "red" def test_build_table_not_applicable_shows_na(): @@ -129,7 +103,7 @@ def test_build_table_not_applicable_shows_na(): completed=True, implementation=IMPLEMENTATION.NOT_APPLICABLE, ) - assert row[2] == "N/A" + assert row[2].plain == "N/A" def test_build_table_in_code_with_impls_shows_count(): @@ -143,8 +117,8 @@ def test_build_table_in_code_with_impls_shows_count(): completed=True, implementation=IMPLEMENTATION.IN_CODE, ) - assert "2" in row[2] - assert Fore.GREEN in row[2] + assert "2" in row[2].plain + assert row[2].style == "green" def test_build_table_in_code_no_impls_shows_zero(): @@ -158,8 +132,8 @@ def test_build_table_in_code_no_impls_shows_zero(): completed=False, implementation=IMPLEMENTATION.IN_CODE, ) - assert "0" in row[2] - assert Fore.RED in row[2] + assert "0" in row[2].plain + assert row[2].style == "red" def test_build_table_urn_is_first_column(): @@ -173,7 +147,7 @@ def test_build_table_urn_is_first_column(): completed=True, implementation=IMPLEMENTATION.IN_CODE, ) - assert row[0] == "ms-001" + assert row[0].plain == "ms-001" def test_build_table_returns_5_columns(): @@ -202,8 +176,8 @@ def test_summarize_statistics_zero_counts_no_crash(): assert "IMPLEMENTATIONS" in result -def test_summarize_statistics_all_complete_has_green_header(): - """All requirements complete: IMPLEMENTATIONS header is green.""" +def test_summarize_statistics_all_complete_has_white_header(): + """IMPLEMENTATIONS header is always white regardless of completion.""" result = _summarize_statistics( TotalStats( total_requirements=2, @@ -214,11 +188,12 @@ def test_summarize_statistics_all_complete_has_green_header(): total_svcs=2, ) ) - assert Fore.GREEN in result + assert "\033[37m" in result # white ANSI code + assert "IMPLEMENTATIONS" in result -def test_summarize_statistics_incomplete_has_red_header(): - """Incomplete requirements: at least one header is red.""" +def test_summarize_statistics_incomplete_has_white_header(): + """IMPLEMENTATIONS header is always white regardless of completion.""" result = _summarize_statistics( TotalStats( total_requirements=3, @@ -231,7 +206,8 @@ def test_summarize_statistics_incomplete_has_red_header(): total_svcs=3, ) ) - assert Fore.RED in result + assert "\033[37m" in result # white ANSI code + assert "IMPLEMENTATIONS" in result def test_summarize_statistics_contains_percentage_string():