"""
Report
------
Functions used to aggregate and produce the final RST reports seen on the CLI.
"""
import logging
from collections import Counter
from datetime import datetime
from operator import attrgetter
from pathlib import Path
from typing import Dict, List, NamedTuple, Tuple, Union
from mutatest import run
from mutatest.run import MutantReport
from mutatest.run import MutantTrialResult
LOGGER = logging.getLogger(__name__)
[docs]class ReportedMutants(NamedTuple):
"""Container for reported mutants to pair status with the list of mutants."""
status: str
mutants: List[MutantReport]
[docs]class DisplayResults(NamedTuple):
"""Results to display on the CLI with coloring."""
summary: str
survived: str
timedout: str
detected: str
[docs]def get_reported_results(trial_results: List[MutantTrialResult], status: str) -> ReportedMutants:
"""Utility function to create filtered lists of mutants based on status.
Args:
trial_results: list of mutant trial results
status: the status to filter by
Returns:
The reported mutants as a ``ReportedMutants`` container.
"""
mutants = [t.mutant for t in trial_results if t.status == status]
return ReportedMutants(status, mutants)
[docs]def get_status_summary(trial_results: List[MutantTrialResult]) -> Dict[str, Union[str, int]]:
"""Create a status summary dictionary for later formatting.
Args:
trial_results: list of mutant trials
Returns:
Dictionary with keys for formatting in the report
"""
status: Dict[str, Union[str, int]] = dict(Counter([t.status for t in trial_results]))
status["TOTAL RUNS"] = len(trial_results)
status["RUN DATETIME"] = str(datetime.now())
return status
[docs]def analyze_mutant_trials(trial_results: List[MutantTrialResult]) -> Tuple[str, DisplayResults]:
"""Create the analysis text report string for the trials.
Additionally, return a DisplayResults NamedTuple that includes terminal coloring for the
output on the terminal.
It will look like:
.. code-block::
Overall mutation trial summary:
===============================
DETECTED: x
TIMEOUT: w
SURVIVED: y
...
Breakdown by section:
=====================
Section title
-------------
source_file.py: (l: 1, c: 10) - mutation from op.Original to op.Mutated
source_file.py: (l: 3, c: 10) - mutation from op.Original to op.Mutated
Args:
trial_results: list of ``MutantTrial`` results
Returns:
Tuple: (text report, ``DisplayResults``)
"""
status = get_status_summary(trial_results)
detected = get_reported_results(trial_results, "DETECTED")
timeouts = get_reported_results(trial_results, "TIMEOUT")
survived = get_reported_results(trial_results, "SURVIVED")
errors = get_reported_results(trial_results, "ERROR")
unknowns = get_reported_results(trial_results, "UNKNOWN")
report_sections = []
# build the summary section
summary_header = "Overall mutation trial summary"
report_sections.append("\n".join([summary_header, "=" * len(summary_header)]))
for s, n in status.items():
report_sections.append(f" - {s}: {n}")
# prepare display of summary results, no color applied
display_summary = "\n".join(report_sections)
display_survived, display_timedout, display_detected = "", "", ""
# build the breakout sections for each type
section_header = "Mutations by result status"
report_sections.append("\n".join(["\n", section_header, "=" * len(section_header)]))
for rpt_results in [survived, timeouts, detected, errors, unknowns]:
if rpt_results.mutants:
section = build_report_section(rpt_results.status, rpt_results.mutants)
report_sections.append(section)
if rpt_results.status == "SURVIVED":
display_survived = run.colorize_output(section, "red")
if rpt_results.status == "TIMEOUT":
display_timedout = run.colorize_output(section, "yellow")
if rpt_results.status == "DETECTED":
display_detected = run.colorize_output(section, "green")
return (
"\n".join(report_sections),
DisplayResults(
summary=display_summary,
detected=display_detected,
timedout=display_timedout,
survived=display_survived,
),
)
[docs]def build_report_section(title: str, mutants: List[MutantReport]) -> str:
"""Build a readable mutation report section from the list of mutants.
It will look like:
.. code-block::
Title
-----
source_file.py: (l: 1, c: 10) - mutation from op.Original to op.Mutated
source_file.py: (l: 3, c: 10) - mutation from op.Original to op.Mutated
Args:
title: title for the section.
mutants: list of mutants for the formatted lines.
Returns:
The report section as a formatted string.
"""
fmt_list = []
fmt_template = (
" - {src_file}: (l: {lineno}, c: {col_offset}) - mutation from {op_type} to {mutation}"
)
# in place sort by source file, then location line then column
mutant_sort_keys = attrgetter("src_file.stem", "src_idx.lineno", "src_idx.col_offset")
mutants.sort(key=mutant_sort_keys)
for mutant in mutants:
summary = {
"src_file": str(mutant.src_file),
"lineno": str(mutant.src_idx.lineno),
"col_offset": str(mutant.src_idx.col_offset),
"op_type": str(mutant.src_idx.op_type),
"mutation": str(mutant.mutation),
}
fmt_list.append(fmt_template.format_map(summary))
report = "\n".join(["\n", title, "-" * len(title)] + [s for s in fmt_list])
return report
[docs]def write_report(report: str, location: Path) -> None:
"""Write the report to a file.
If the location does not exist with folders they are created.
Args:
report: the string report to write
location: path location to the file
Returns:
None, writes output to location
"""
if not location.parent.exists():
LOGGER.info("Creating directory tree for: %s", location.parent.resolve())
location.parent.mkdir(parents=True, exist_ok=True)
with open(location, "w", encoding="utf-8") as output_loc:
LOGGER.info("Writing output report to: %s", location.resolve())
output_loc.write(report)