Skip to content

Commit

Permalink
Adds summary stats about found violations
Browse files Browse the repository at this point in the history
Fixes: #2110
  • Loading branch information
ssbarnea committed Sep 29, 2022
1 parent 78efaa7 commit 7682853
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 32 deletions.
1 change: 1 addition & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ max-line-length = 100
extend-ignore =
E203,
E501,
C901, # complexity is also measured by pylint: too-many-branches
DAR104, # We use type annotations instead
DAR301, # https://github.com/terrencepreilly/darglint/issues/165
F401, # duplicate of pylint W0611 (unused-import)
Expand Down
9 changes: 5 additions & 4 deletions playbook.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
- hosts: localhost
- name: Example
hosts: localhost
tasks:
- shell:
cmd: |
if [ $FOO != false ]; then
- name: Play name
debug:
msg: "Hello {{ ansible_facts.env | list }}!"
113 changes: 86 additions & 27 deletions src/ansiblelint/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@

import logging
import os
from dataclasses import dataclass
from functools import lru_cache
from typing import TYPE_CHECKING, Any

from ansible_compat.runtime import Runtime
from rich.markup import escape
from rich.table import Table

from ansiblelint import formatters
from ansiblelint._mockings import _perform_mockings
from ansiblelint.color import console, console_stderr, render_yaml
from ansiblelint.config import PROFILES
from ansiblelint.config import options as default_options
from ansiblelint.constants import SUCCESS_RC, VIOLATIONS_FOUND_RC
from ansiblelint.constants import RULE_DOC_URL, SUCCESS_RC, VIOLATIONS_FOUND_RC
from ansiblelint.errors import MatchError
from ansiblelint.stats import SummarizedResults, TagStats

if TYPE_CHECKING:
from argparse import Namespace
Expand All @@ -28,21 +31,6 @@
_logger = logging.getLogger(__package__)


@dataclass
class SummarizedResults:
"""The statistics about an ansible-lint run."""

failures: int = 0
warnings: int = 0
fixed_failures: int = 0
fixed_warnings: int = 0

@property
def fixed(self) -> int:
"""Get total fixed count."""
return self.fixed_failures + self.fixed_warnings


class App:
"""App class represents an execution of the linter."""

Expand Down Expand Up @@ -101,27 +89,34 @@ def render_matches(self, matches: list[MatchError]) -> None:

def count_results(self, matches: list[MatchError]) -> SummarizedResults:
"""Count failures and warnings in matches."""
failures = 0
warnings = 0
fixed_failures = 0
fixed_warnings = 0
result = SummarizedResults()

for match in matches:
# tag can include a sub-rule id: `yaml[document-start]`
# rule.id is the generic rule id: `yaml`
# *rule.tags is the list of the rule's tags (categories): `style`
if match.tag not in result.tag_stats:
result.tag_stats[match.tag] = TagStats(
tag=match.tag, count=1, associated_tags=match.rule.tags
)
else:
result.tag_stats[match.tag].count += 1

if {match.tag, match.rule.id, *match.rule.tags}.isdisjoint(
self.options.warn_list
):
# not in warn_list
if match.fixed:
fixed_failures += 1
result.fixed_failures += 1
else:
failures += 1
result.failures += 1
else:
result.tag_stats[match.tag].warning = True
if match.fixed:
fixed_warnings += 1
result.fixed_warnings += 1
else:
warnings += 1
return SummarizedResults(failures, warnings, fixed_failures, fixed_warnings)
result.warnings += 1
return result

@staticmethod
def count_lintables(files: set[Lintable]) -> tuple[int, int]:
Expand Down Expand Up @@ -207,10 +202,32 @@ def report_outcome(self, result: LintResult, mark_as_success: bool = False) -> i
return VIOLATIONS_FOUND_RC

@staticmethod
def report_summary(
def report_summary( # pylint: disable=too-many-branches,too-many-locals
summary: SummarizedResults, changed_files_count: int, files_count: int
) -> None:
"""Report match and file counts."""
# sort the stats by profiles
idx = 0
rule_order = {}

for profile, profile_config in PROFILES.items():
for rule in profile_config["rules"]:
# print(profile, rule)
rule_order[rule] = (idx, profile)
idx += 1
_logger.debug("Determined rule-profile order: %s", rule_order)
failed_profiles = set()
for tag, tag_stats in summary.tag_stats.items():
if tag in rule_order:
tag_stats.order, tag_stats.profile = rule_order.get(tag, (idx, ""))
elif "[" in tag:
tag_stats.order, tag_stats.profile = rule_order.get(
tag.split("[")[0], (idx, "")
)
if tag_stats.profile:
failed_profiles.add(tag_stats.profile)
summary.sort()

if changed_files_count:
console_stderr.print(f"Modified {changed_files_count} files.")

Expand All @@ -220,6 +237,48 @@ def report_summary(
msg += f", and fixed {summary.fixed} issue(s)"
msg += f" on {files_count} files."

# determine which profile passed
summary.passed_profile = ""
passed_profile_count = 0
for profile in PROFILES.keys():
if profile in failed_profiles:
break
if profile != summary.passed_profile:
summary.passed_profile = profile
passed_profile_count += 1

if summary.tag_stats:
table = Table(
title="Rule Violation Summary",
collapse_padding=True,
box=None,
show_lines=False,
)
table.add_column("count", justify="right")
table.add_column("tag")
table.add_column("profile")
table.add_column("rule associated tags")
for tag, stats in summary.tag_stats.items():
table.add_row(
str(stats.count),
f"[link={RULE_DOC_URL}{ tag.split('[')[0] }]{escape(tag)}[/link]",
stats.profile,
f"{', '.join(stats.associated_tags)}{' (warning)' if stats.warning else ''}",
style="yellow" if stats.warning else "red",
)
# rate stars for the top 5 profiles (min would not get
rating = 5 - (len(PROFILES.keys()) - passed_profile_count)
if 0 < rating < 6:
stars = f"Rated as {rating}/5 stars."
else:
stars = "No rating."

console_stderr.print(table)
console_stderr.print()

if summary.passed_profile:
msg += f" Code passed [white bold]{summary.passed_profile}[/] profile. {stars}"

console_stderr.print(msg)


Expand Down
2 changes: 1 addition & 1 deletion src/ansiblelint/data/profiles.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ basic:
partial-become:
playbook-extension:
role-name:
schema: # can cover lots of rules, but not really be able to give best error messages
name:
var-naming:
yaml:
Expand All @@ -54,7 +55,6 @@ moderate:
name[casing]:
no-shorthand: # schema-related
url: https://github.com/ansible/ansible-lint/issues/2117
schema: # can cover lots of rules, but not really be able to give best error messages
spell-var-name:
url: https://github.com/ansible/ansible-lint/issues/2168
safety:
Expand Down
36 changes: 36 additions & 0 deletions src/ansiblelint/stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Module hosting functionality about reporting."""
from __future__ import annotations

from dataclasses import dataclass, field


@dataclass(order=True)
class TagStats:
"""Tag statistics."""

order: int = 0 # to be computed based on rule's profile
tag: str = "" # rule effective id (can be multiple tags per rule id)
count: int = 0 # total number of occurrences
warning: bool = False # set true if listed in warn_list
profile: str = ""
associated_tags: list[str] = field(default_factory=list)


class SummarizedResults:
"""The statistics about an ansible-lint run."""

failures: int = 0
warnings: int = 0
fixed_failures: int = 0
fixed_warnings: int = 0
tag_stats: dict[str, TagStats] = {}
passed_profile: str = ""

@property
def fixed(self) -> int:
"""Get total fixed count."""
return self.fixed_failures + self.fixed_warnings

def sort(self) -> None:
"""Sort tag stats by tag name."""
self.tag_stats = dict(sorted(self.tag_stats.items(), key=lambda t: t[1]))

0 comments on commit 7682853

Please sign in to comment.