robotframework-quality-scanner 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- robotframework_quality_scanner/__init__.py +5 -0
- robotframework_quality_scanner/analyzers/__init__.py +1 -0
- robotframework_quality_scanner/analyzers/dependency_analyzer.py +54 -0
- robotframework_quality_scanner/analyzers/duplication_analyzer.py +43 -0
- robotframework_quality_scanner/analyzers/performance_analyzer.py +56 -0
- robotframework_quality_scanner/analyzers/test_data_analyzer.py +70 -0
- robotframework_quality_scanner/api/__init__.py +92 -0
- robotframework_quality_scanner/scanner.py +173 -0
- robotframework_quality_scanner/utils/__init__.py +56 -0
- robotframework_quality_scanner/utils/autofix.py +51 -0
- robotframework_quality_scanner/utils/history.py +91 -0
- robotframework_quality_scanner-0.3.0.dist-info/METADATA +542 -0
- robotframework_quality_scanner-0.3.0.dist-info/RECORD +15 -0
- robotframework_quality_scanner-0.3.0.dist-info/WHEEL +5 -0
- robotframework_quality_scanner-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Analisadores especializados
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from ..models.issue import Issue
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DependencyAnalyzer:
|
|
6
|
+
"""Valida imports e dependências."""
|
|
7
|
+
|
|
8
|
+
def analyze_file(self, filepath, content):
|
|
9
|
+
issues = []
|
|
10
|
+
lines = content.split('\n')
|
|
11
|
+
|
|
12
|
+
for i, line in enumerate(lines, 1):
|
|
13
|
+
# Library não é String
|
|
14
|
+
if 'Library' in line and not any(x in line for x in ['Builtin', 'String', 'Collections']):
|
|
15
|
+
if 'Library http://' in line or 'Library C:\\' in line:
|
|
16
|
+
issues.append(Issue(
|
|
17
|
+
rule_id="DEP001",
|
|
18
|
+
category="DEPENDENCY",
|
|
19
|
+
severity="HIGH",
|
|
20
|
+
description="Library com URL ou path absoluto.",
|
|
21
|
+
file=filepath,
|
|
22
|
+
line=i,
|
|
23
|
+
recommendation="Use imports relativos ou instale como package."
|
|
24
|
+
))
|
|
25
|
+
|
|
26
|
+
# Resource não encontrado (heurística: arquivo com extensão rara)
|
|
27
|
+
if 'Resource' in line:
|
|
28
|
+
res_match = re.search(r'Resource\s+(.+)', line)
|
|
29
|
+
if res_match:
|
|
30
|
+
res_file = res_match.group(1).strip()
|
|
31
|
+
if res_file.endswith(('.py', '.txt', '.json')):
|
|
32
|
+
issues.append(Issue(
|
|
33
|
+
rule_id="DEP002",
|
|
34
|
+
category="DEPENDENCY",
|
|
35
|
+
severity="MEDIUM",
|
|
36
|
+
description="Resource com extensão não-robot.",
|
|
37
|
+
file=filepath,
|
|
38
|
+
line=i,
|
|
39
|
+
recommendation="Use arquivos .robot ou .resource."
|
|
40
|
+
))
|
|
41
|
+
|
|
42
|
+
# Import organizado (Libraries antes de Resources)
|
|
43
|
+
if i > 1 and 'Resource' in lines[i-2] and 'Library' in line:
|
|
44
|
+
issues.append(Issue(
|
|
45
|
+
rule_id="DEP003",
|
|
46
|
+
category="DEPENDENCY",
|
|
47
|
+
severity="LOW",
|
|
48
|
+
description="Library após Resource detectado.",
|
|
49
|
+
file=filepath,
|
|
50
|
+
line=i,
|
|
51
|
+
recommendation="Organize: Libraries primeiro, depois Resources."
|
|
52
|
+
))
|
|
53
|
+
|
|
54
|
+
return issues
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from difflib import SequenceMatcher
|
|
2
|
+
from ..models.issue import Issue
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DuplicationAnalyzer:
|
|
6
|
+
"""Detecta código duplicado em testes."""
|
|
7
|
+
|
|
8
|
+
def analyze_file(self, filepath, content):
|
|
9
|
+
issues = []
|
|
10
|
+
lines = [l.strip() for l in content.split('\n') if l.strip()]
|
|
11
|
+
|
|
12
|
+
# Detectar linhas repetidas
|
|
13
|
+
seen = {}
|
|
14
|
+
for i, line in enumerate(lines):
|
|
15
|
+
if line in seen and not line.startswith('***'):
|
|
16
|
+
issues.append(Issue(
|
|
17
|
+
rule_id="DUP001",
|
|
18
|
+
category="DUPLICATION",
|
|
19
|
+
severity="MEDIUM",
|
|
20
|
+
description=f"Linha duplicada (apareceu em linha {seen[line] + 1}).",
|
|
21
|
+
file=filepath,
|
|
22
|
+
line=i + 1,
|
|
23
|
+
recommendation="Consolide em uma keyword reutilizável."
|
|
24
|
+
))
|
|
25
|
+
seen[line] = i
|
|
26
|
+
|
|
27
|
+
# Detectar testes com alta similaridade
|
|
28
|
+
test_lines = [l for l in lines if 'Test Cases' in l or l.startswith('***')]
|
|
29
|
+
for i in range(len(test_lines)):
|
|
30
|
+
for j in range(i + 1, len(test_lines)):
|
|
31
|
+
ratio = SequenceMatcher(None, test_lines[i], test_lines[j]).ratio()
|
|
32
|
+
if 0.8 <= ratio < 1.0:
|
|
33
|
+
issues.append(Issue(
|
|
34
|
+
rule_id="DUP002",
|
|
35
|
+
category="DUPLICATION",
|
|
36
|
+
severity="LOW",
|
|
37
|
+
description=f"Testes similares detectados ({int(ratio*100)}% match).",
|
|
38
|
+
file=filepath,
|
|
39
|
+
line=i + 1,
|
|
40
|
+
recommendation="Use [Template] ou data-driven tests."
|
|
41
|
+
))
|
|
42
|
+
|
|
43
|
+
return issues
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from ..models.issue import Issue
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class PerformanceAnalyzer:
|
|
6
|
+
"""Detecta problemas de performance em testes Robot Framework."""
|
|
7
|
+
|
|
8
|
+
def analyze_file(self, filepath, content):
|
|
9
|
+
issues = []
|
|
10
|
+
lines = content.split('\n')
|
|
11
|
+
|
|
12
|
+
for i, line in enumerate(lines, 1):
|
|
13
|
+
# Deep nesting (>4 níveis)
|
|
14
|
+
indent = len(line) - len(line.lstrip())
|
|
15
|
+
if indent > 16: # 4+ níveis de indentação
|
|
16
|
+
issues.append(Issue(
|
|
17
|
+
rule_id="PERF001",
|
|
18
|
+
category="PERFORMANCE",
|
|
19
|
+
severity="MEDIUM",
|
|
20
|
+
description="Deep nesting detectado (>4 níveis).",
|
|
21
|
+
file=filepath,
|
|
22
|
+
line=i,
|
|
23
|
+
recommendation="Refatore keywords para reduzir nesting."
|
|
24
|
+
))
|
|
25
|
+
|
|
26
|
+
# Sleep excessivo
|
|
27
|
+
if 'Sleep' in line:
|
|
28
|
+
sleep_match = re.search(r'Sleep\s+(\d+)([smh]?)', line, re.IGNORECASE)
|
|
29
|
+
if sleep_match:
|
|
30
|
+
val = int(sleep_match.group(1))
|
|
31
|
+
unit = sleep_match.group(2) or 's'
|
|
32
|
+
total_sec = val * (1 if unit == 's' else 60 if unit == 'm' else 3600)
|
|
33
|
+
if total_sec > 5:
|
|
34
|
+
issues.append(Issue(
|
|
35
|
+
rule_id="PERF002",
|
|
36
|
+
category="PERFORMANCE",
|
|
37
|
+
severity="HIGH",
|
|
38
|
+
description=f"Sleep muito longo detectado ({val}{unit}).",
|
|
39
|
+
file=filepath,
|
|
40
|
+
line=i,
|
|
41
|
+
recommendation="Use waits explícitos ao invés de Sleep."
|
|
42
|
+
))
|
|
43
|
+
|
|
44
|
+
# Linha muito longa (>120 chars)
|
|
45
|
+
if len(line) > 120:
|
|
46
|
+
issues.append(Issue(
|
|
47
|
+
rule_id="PERF003",
|
|
48
|
+
category="PERFORMANCE",
|
|
49
|
+
severity="LOW",
|
|
50
|
+
description="Linha muito longa detectada (>120 caracteres).",
|
|
51
|
+
file=filepath,
|
|
52
|
+
line=i,
|
|
53
|
+
recommendation="Quebre a linha ou refatore a lógica."
|
|
54
|
+
))
|
|
55
|
+
|
|
56
|
+
return issues
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from ..models.issue import Issue
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TestDataAnalyzer:
|
|
6
|
+
"""Valida separação entre dados e lógica."""
|
|
7
|
+
|
|
8
|
+
def analyze_file(self, filepath, content):
|
|
9
|
+
issues = []
|
|
10
|
+
lines = content.split('\n')
|
|
11
|
+
|
|
12
|
+
for i, line in enumerate(lines, 1):
|
|
13
|
+
# Dados hardcoded
|
|
14
|
+
email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
|
|
15
|
+
url_pattern = r'https?://[^\s]+'
|
|
16
|
+
phone_pattern = r'\(\d{2}\)\s?9?\d{4}-\d{4}'
|
|
17
|
+
|
|
18
|
+
if re.search(email_pattern, line):
|
|
19
|
+
issues.append(Issue(
|
|
20
|
+
rule_id="DATA001",
|
|
21
|
+
category="TEST_DATA",
|
|
22
|
+
severity="MEDIUM",
|
|
23
|
+
description="Email hardcoded detectado.",
|
|
24
|
+
file=filepath,
|
|
25
|
+
line=i,
|
|
26
|
+
recommendation="Extraia para variável ou arquivo de dados."
|
|
27
|
+
))
|
|
28
|
+
|
|
29
|
+
if re.search(url_pattern, line) and 'http' not in line.split('#')[0]:
|
|
30
|
+
continue # Skip comentários
|
|
31
|
+
|
|
32
|
+
if re.search(url_pattern, line):
|
|
33
|
+
issues.append(Issue(
|
|
34
|
+
rule_id="DATA002",
|
|
35
|
+
category="TEST_DATA",
|
|
36
|
+
severity="MEDIUM",
|
|
37
|
+
description="URL hardcoded detectada.",
|
|
38
|
+
file=filepath,
|
|
39
|
+
line=i,
|
|
40
|
+
recommendation="Use variável de ambiente ou arquivo de configuração."
|
|
41
|
+
))
|
|
42
|
+
|
|
43
|
+
if re.search(phone_pattern, line):
|
|
44
|
+
issues.append(Issue(
|
|
45
|
+
rule_id="DATA003",
|
|
46
|
+
category="TEST_DATA",
|
|
47
|
+
severity="MEDIUM",
|
|
48
|
+
description="Telefone hardcoded detectado.",
|
|
49
|
+
file=filepath,
|
|
50
|
+
line=i,
|
|
51
|
+
recommendation="Extraia para dados de teste."
|
|
52
|
+
))
|
|
53
|
+
|
|
54
|
+
# Data-driven pattern sugestão
|
|
55
|
+
if 'Test Cases' in line:
|
|
56
|
+
# Heurística: 3+ testes similares → sugerir [Template]
|
|
57
|
+
test_count = sum(1 for l in lines if l.strip() and not l.startswith(' ') and l not in ['*** Test Cases ***', '*** Keywords ***', '*** Settings ***'])
|
|
58
|
+
if test_count >= 3:
|
|
59
|
+
issues.append(Issue(
|
|
60
|
+
rule_id="DATA004",
|
|
61
|
+
category="TEST_DATA",
|
|
62
|
+
severity="LOW",
|
|
63
|
+
description="Oportunidade de data-driven tests detectada.",
|
|
64
|
+
file=filepath,
|
|
65
|
+
line=i,
|
|
66
|
+
recommendation="Considere usar [Template] com multiple test cases."
|
|
67
|
+
))
|
|
68
|
+
break
|
|
69
|
+
|
|
70
|
+
return issues
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""API REST para integração com ferramentas externas."""
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AnalysisAPI:
|
|
7
|
+
"""API simples para exposição de análises."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, scanner):
|
|
10
|
+
self.scanner = scanner
|
|
11
|
+
|
|
12
|
+
def analyze_file_api(self, filepath):
|
|
13
|
+
"""Analisa um arquivo e retorna JSON."""
|
|
14
|
+
if not Path(filepath).exists():
|
|
15
|
+
return {"error": f"Arquivo não encontrado: {filepath}"}, 404
|
|
16
|
+
|
|
17
|
+
issues = self.scanner.scan_file(filepath)
|
|
18
|
+
return {
|
|
19
|
+
"file": filepath,
|
|
20
|
+
"issue_count": len(issues),
|
|
21
|
+
"issues": [i.to_dict() for i in issues]
|
|
22
|
+
}, 200
|
|
23
|
+
|
|
24
|
+
def analyze_directory_api(self, dirpath):
|
|
25
|
+
"""Analisa diretório e retorna JSON."""
|
|
26
|
+
if not Path(dirpath).exists():
|
|
27
|
+
return {"error": f"Diretório não encontrado: {dirpath}"}, 404
|
|
28
|
+
|
|
29
|
+
issues = self.scanner.scan(dirpath)
|
|
30
|
+
|
|
31
|
+
# Agrupar por arquivo
|
|
32
|
+
by_file = {}
|
|
33
|
+
for issue in issues:
|
|
34
|
+
if issue.file not in by_file:
|
|
35
|
+
by_file[issue.file] = []
|
|
36
|
+
by_file[issue.file].append(issue.to_dict())
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
"directory": dirpath,
|
|
40
|
+
"total_issues": len(issues),
|
|
41
|
+
"files_with_issues": len(by_file),
|
|
42
|
+
"issues_by_file": by_file
|
|
43
|
+
}, 200
|
|
44
|
+
|
|
45
|
+
def generate_report(self, dirpath, report_format="json"):
|
|
46
|
+
"""Gera relatório em diferentes formatos."""
|
|
47
|
+
if report_format == "json":
|
|
48
|
+
data, code = self.analyze_directory_api(dirpath)
|
|
49
|
+
return {
|
|
50
|
+
"format": "json",
|
|
51
|
+
"data": data,
|
|
52
|
+
"code": code
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
elif report_format == "txt":
|
|
56
|
+
issues = self.scanner.scan(dirpath)
|
|
57
|
+
report = "QUALITY REPORT\n" + "=" * 50 + "\n\n"
|
|
58
|
+
for issue in issues:
|
|
59
|
+
report += f"[{issue.severity}] {issue.rule_id}\n"
|
|
60
|
+
report += f" File: {issue.file}:{issue.line}\n"
|
|
61
|
+
report += f" {issue.description}\n"
|
|
62
|
+
report += f" Recommendation: {issue.recommendation}\n\n"
|
|
63
|
+
return {"format": "txt", "content": report}
|
|
64
|
+
|
|
65
|
+
elif report_format == "html":
|
|
66
|
+
issues = self.scanner.scan(dirpath)
|
|
67
|
+
html = """<html><head><title>Quality Report</title>
|
|
68
|
+
<style>body{font-family:Arial;margin:20px}
|
|
69
|
+
.issue{margin:10px;padding:10px;border:1px solid #ccc}
|
|
70
|
+
.HIGH{background:#ffcccc}.MEDIUM{background:#ffffcc}.LOW{background:#ccffcc}
|
|
71
|
+
</style></head><body><h1>Quality Report</h1>"""
|
|
72
|
+
|
|
73
|
+
for issue in issues:
|
|
74
|
+
html += f'<div class="issue {issue.severity}">'
|
|
75
|
+
html += f'<strong>[{issue.severity}] {issue.rule_id}</strong><br>'
|
|
76
|
+
html += f'{issue.file}:{issue.line}<br>'
|
|
77
|
+
html += f'{issue.description}<br>'
|
|
78
|
+
html += f'<em>→ {issue.recommendation}</em>'
|
|
79
|
+
html += '</div>'
|
|
80
|
+
|
|
81
|
+
html += "</body></html>"
|
|
82
|
+
return {"format": "html", "content": html}
|
|
83
|
+
|
|
84
|
+
return {"error": "Unknown format"}, 400
|
|
85
|
+
|
|
86
|
+
def get_summary(self):
|
|
87
|
+
"""Retorna sumário do cache/histórico."""
|
|
88
|
+
cache_stats = self.scanner.cache.get_stats()
|
|
89
|
+
return {
|
|
90
|
+
"cache": cache_stats,
|
|
91
|
+
"version": "0.1.0"
|
|
92
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
from .models.issue import Issue
|
|
4
|
+
from .analyzers.performance_analyzer import PerformanceAnalyzer
|
|
5
|
+
from .analyzers.duplication_analyzer import DuplicationAnalyzer
|
|
6
|
+
from .analyzers.dependency_analyzer import DependencyAnalyzer
|
|
7
|
+
from .analyzers.test_data_analyzer import TestDataAnalyzer
|
|
8
|
+
from .utils import AnalysisCache
|
|
9
|
+
from .utils.history import AnalysisHistory
|
|
10
|
+
from .reporters.executive_report import ExecutiveReport
|
|
11
|
+
from .reporters.coverage_report import CoverageReport
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class QualityScanner:
|
|
15
|
+
"""Scanner de qualidade Robot Framework com múltiplos analisadores.
|
|
16
|
+
|
|
17
|
+
Detecta:
|
|
18
|
+
- Anti-patterns web (Sleep, XPath, URLs)
|
|
19
|
+
- Performance (deep nesting, timeouts)
|
|
20
|
+
- Código duplicado
|
|
21
|
+
- Problemas de dependência
|
|
22
|
+
- Dados hardcoded
|
|
23
|
+
- Gera relatórios executivos e de cobertura
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self.cache = AnalysisCache()
|
|
28
|
+
self.history = AnalysisHistory()
|
|
29
|
+
self.perf_analyzer = PerformanceAnalyzer()
|
|
30
|
+
self.dup_analyzer = DuplicationAnalyzer()
|
|
31
|
+
self.dep_analyzer = DependencyAnalyzer()
|
|
32
|
+
self.data_analyzer = TestDataAnalyzer()
|
|
33
|
+
self.scan_time = 0
|
|
34
|
+
self.files_analyzed = []
|
|
35
|
+
self.reports = {}
|
|
36
|
+
|
|
37
|
+
def scan_file(self, path, use_cache=True):
|
|
38
|
+
"""Escaneia um arquivo individual."""
|
|
39
|
+
# Verificar cache
|
|
40
|
+
if use_cache:
|
|
41
|
+
cached = self.cache.get(path)
|
|
42
|
+
if cached is not None:
|
|
43
|
+
return cached
|
|
44
|
+
|
|
45
|
+
issues = []
|
|
46
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
47
|
+
content = f.read()
|
|
48
|
+
|
|
49
|
+
basename = os.path.basename(path)
|
|
50
|
+
|
|
51
|
+
# Armazenar para cobertura
|
|
52
|
+
self.files_analyzed.append((basename, content))
|
|
53
|
+
|
|
54
|
+
# Web/Basic rules
|
|
55
|
+
for i, line in enumerate(content.split('\n'), 1):
|
|
56
|
+
if "Sleep" in line:
|
|
57
|
+
issues.append(Issue(
|
|
58
|
+
rule_id="WEB001",
|
|
59
|
+
category="WEB",
|
|
60
|
+
severity="HIGH",
|
|
61
|
+
description="Uso de Sleep detectado.",
|
|
62
|
+
file=basename,
|
|
63
|
+
line=i,
|
|
64
|
+
recommendation="Use waits explícitos.",
|
|
65
|
+
reference="https://robotframework.org/SeleniumLibrary/"
|
|
66
|
+
))
|
|
67
|
+
if "/html/" in line or (line.strip().startswith("/") and "xpath" in line.lower()):
|
|
68
|
+
issues.append(Issue(
|
|
69
|
+
rule_id="WEB002",
|
|
70
|
+
category="WEB",
|
|
71
|
+
severity="MEDIUM",
|
|
72
|
+
description="XPath absoluto detectado.",
|
|
73
|
+
file=basename,
|
|
74
|
+
line=i,
|
|
75
|
+
recommendation="Use localizadores mais resilientes."
|
|
76
|
+
))
|
|
77
|
+
if "http://" in line:
|
|
78
|
+
issues.append(Issue(
|
|
79
|
+
rule_id="WEB003",
|
|
80
|
+
category="WEB",
|
|
81
|
+
severity="MEDIUM",
|
|
82
|
+
description="URL hardcoded.",
|
|
83
|
+
file=basename,
|
|
84
|
+
line=i,
|
|
85
|
+
recommendation="Extrair para variável."
|
|
86
|
+
))
|
|
87
|
+
|
|
88
|
+
# Analisadores especializados
|
|
89
|
+
issues.extend(self.perf_analyzer.analyze_file(basename, content))
|
|
90
|
+
issues.extend(self.dup_analyzer.analyze_file(basename, content))
|
|
91
|
+
issues.extend(self.dep_analyzer.analyze_file(basename, content))
|
|
92
|
+
issues.extend(self.data_analyzer.analyze_file(basename, content))
|
|
93
|
+
|
|
94
|
+
# Cache + Histórico
|
|
95
|
+
self.cache.set(path, issues)
|
|
96
|
+
self.history.record_analysis(path, issues)
|
|
97
|
+
|
|
98
|
+
return issues
|
|
99
|
+
|
|
100
|
+
def scan(self, path, use_cache=True, generate_reports=True):
|
|
101
|
+
"""Escaneia arquivo ou diretório recursivamente e gera relatórios."""
|
|
102
|
+
start_time = time.time()
|
|
103
|
+
results = []
|
|
104
|
+
self.files_analyzed = []
|
|
105
|
+
|
|
106
|
+
if os.path.isfile(path):
|
|
107
|
+
if path.endswith(('.robot', '.resource')):
|
|
108
|
+
results.extend(self.scan_file(path, use_cache))
|
|
109
|
+
else:
|
|
110
|
+
for root, _, files in os.walk(path):
|
|
111
|
+
for f in files:
|
|
112
|
+
if f.endswith(('.robot', '.resource')):
|
|
113
|
+
full = os.path.join(root, f)
|
|
114
|
+
results.extend(self.scan_file(full, use_cache))
|
|
115
|
+
|
|
116
|
+
self.scan_time = time.time() - start_time
|
|
117
|
+
|
|
118
|
+
# Gerar relatórios
|
|
119
|
+
if generate_reports:
|
|
120
|
+
self.reports = {
|
|
121
|
+
'executive': ExecutiveReport(results, self.scan_time),
|
|
122
|
+
'coverage': CoverageReport(self.files_analyzed)
|
|
123
|
+
}
|
|
124
|
+
return results, self.reports
|
|
125
|
+
|
|
126
|
+
return results
|
|
127
|
+
|
|
128
|
+
def generate_executive_report(self, issues, format='text'):
|
|
129
|
+
"""Gera relatório executivo em diferentes formatos."""
|
|
130
|
+
report = ExecutiveReport(issues, self.scan_time)
|
|
131
|
+
|
|
132
|
+
if format == 'text':
|
|
133
|
+
return report.to_text()
|
|
134
|
+
elif format == 'json':
|
|
135
|
+
return report.to_json()
|
|
136
|
+
elif format == 'html':
|
|
137
|
+
return report.to_html()
|
|
138
|
+
else:
|
|
139
|
+
return report.to_dict()
|
|
140
|
+
|
|
141
|
+
def generate_coverage_report(self, format='text'):
|
|
142
|
+
"""Gera relatório de cobertura."""
|
|
143
|
+
report = CoverageReport(self.files_analyzed)
|
|
144
|
+
|
|
145
|
+
if format == 'text':
|
|
146
|
+
return report.to_text()
|
|
147
|
+
elif format == 'html':
|
|
148
|
+
return report.to_html()
|
|
149
|
+
else:
|
|
150
|
+
return report.to_dict()
|
|
151
|
+
|
|
152
|
+
def save_reports(self, output_dir='./quality-reports'):
|
|
153
|
+
"""Salva relatórios em arquivo."""
|
|
154
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
155
|
+
|
|
156
|
+
# Salvar relatório executivo
|
|
157
|
+
with open(os.path.join(output_dir, 'executive_report.html'), 'w') as f:
|
|
158
|
+
f.write(self.reports['executive'].to_html())
|
|
159
|
+
|
|
160
|
+
with open(os.path.join(output_dir, 'executive_report.txt'), 'w') as f:
|
|
161
|
+
f.write(self.reports['executive'].to_text())
|
|
162
|
+
|
|
163
|
+
with open(os.path.join(output_dir, 'executive_report.json'), 'w') as f:
|
|
164
|
+
f.write(self.reports['executive'].to_json())
|
|
165
|
+
|
|
166
|
+
# Salvar relatório de cobertura
|
|
167
|
+
with open(os.path.join(output_dir, 'coverage_report.html'), 'w') as f:
|
|
168
|
+
f.write(self.reports['coverage'].to_html())
|
|
169
|
+
|
|
170
|
+
with open(os.path.join(output_dir, 'coverage_report.txt'), 'w') as f:
|
|
171
|
+
f.write(self.reports['coverage'].to_text())
|
|
172
|
+
|
|
173
|
+
return output_dir
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import os
|
|
3
|
+
import pickle
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AnalysisCache:
|
|
8
|
+
"""Cache de análises com invalidação por hash."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, cache_dir=".cache"):
|
|
11
|
+
self.cache_dir = Path(cache_dir)
|
|
12
|
+
self.cache_dir.mkdir(exist_ok=True)
|
|
13
|
+
|
|
14
|
+
def _get_file_hash(self, filepath):
|
|
15
|
+
"""Calcula SHA256 do arquivo."""
|
|
16
|
+
sha = hashlib.sha256()
|
|
17
|
+
with open(filepath, 'rb') as f:
|
|
18
|
+
sha.update(f.read())
|
|
19
|
+
return sha.hexdigest()
|
|
20
|
+
|
|
21
|
+
def _get_cache_key(self, filepath):
|
|
22
|
+
"""Gera chave de cache baseada em arquivo + hash."""
|
|
23
|
+
file_hash = self._get_file_hash(filepath)
|
|
24
|
+
return f"{Path(filepath).stem}_{file_hash}.cache"
|
|
25
|
+
|
|
26
|
+
def get(self, filepath):
|
|
27
|
+
"""Retorna resultado em cache se válido."""
|
|
28
|
+
cache_key = self._get_cache_key(filepath)
|
|
29
|
+
cache_file = self.cache_dir / cache_key
|
|
30
|
+
|
|
31
|
+
if cache_file.exists():
|
|
32
|
+
with open(cache_file, 'rb') as f:
|
|
33
|
+
return pickle.load(f)
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
def set(self, filepath, data):
|
|
37
|
+
"""Armazena resultado em cache."""
|
|
38
|
+
cache_key = self._get_cache_key(filepath)
|
|
39
|
+
cache_file = self.cache_dir / cache_key
|
|
40
|
+
|
|
41
|
+
with open(cache_file, 'wb') as f:
|
|
42
|
+
pickle.dump(data, f)
|
|
43
|
+
|
|
44
|
+
def clear(self):
|
|
45
|
+
"""Limpa todo o cache."""
|
|
46
|
+
import shutil
|
|
47
|
+
shutil.rmtree(self.cache_dir, ignore_errors=True)
|
|
48
|
+
self.cache_dir.mkdir()
|
|
49
|
+
|
|
50
|
+
def get_stats(self):
|
|
51
|
+
"""Retorna estatísticas do cache."""
|
|
52
|
+
files = list(self.cache_dir.glob("*.cache"))
|
|
53
|
+
return {
|
|
54
|
+
"cache_size": sum(f.stat().st_size for f in files),
|
|
55
|
+
"file_count": len(files)
|
|
56
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
class AutoFixer:
|
|
2
|
+
"""Auto-corrigi problemas comuns em arquivos Robot."""
|
|
3
|
+
|
|
4
|
+
@staticmethod
|
|
5
|
+
def fix_file(filepath):
|
|
6
|
+
"""Aplica todas as correções disponíveis."""
|
|
7
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
8
|
+
content = f.read()
|
|
9
|
+
|
|
10
|
+
original = content
|
|
11
|
+
fixes_applied = []
|
|
12
|
+
|
|
13
|
+
# Fix 1: Remove trailing whitespace
|
|
14
|
+
lines = content.split('\n')
|
|
15
|
+
lines = [l.rstrip() for l in lines]
|
|
16
|
+
content = '\n'.join(lines)
|
|
17
|
+
if content != original:
|
|
18
|
+
fixes_applied.append("trailing_whitespace_removed")
|
|
19
|
+
original = content
|
|
20
|
+
|
|
21
|
+
# Fix 2: Normaliza indentação (tabs → 4 spaces)
|
|
22
|
+
content = content.replace('\t', ' ')
|
|
23
|
+
if content != original:
|
|
24
|
+
fixes_applied.append("indentation_normalized")
|
|
25
|
+
original = content
|
|
26
|
+
|
|
27
|
+
# Fix 3: Adiciona [Documentation] vazio em keywords sem doc
|
|
28
|
+
if '*** Keywords ***' in content:
|
|
29
|
+
kw_section = content.split('*** Keywords ***')[1]
|
|
30
|
+
if 'Log' in kw_section and '[Documentation]' not in kw_section:
|
|
31
|
+
content = content.replace('Log', '[Documentation]\nLog', 1)
|
|
32
|
+
fixes_applied.append("documentation_added")
|
|
33
|
+
|
|
34
|
+
# Fix 4: Capitaliza keywords comuns
|
|
35
|
+
keywords_to_capitalize = [
|
|
36
|
+
'log', 'sleep', 'open browser', 'close browser',
|
|
37
|
+
'click element', 'input text', 'wait until'
|
|
38
|
+
]
|
|
39
|
+
for kw in keywords_to_capitalize:
|
|
40
|
+
if kw.lower() in content.lower():
|
|
41
|
+
import re
|
|
42
|
+
pattern = re.compile(re.escape(kw), re.IGNORECASE)
|
|
43
|
+
content = pattern.sub(kw.title(), content)
|
|
44
|
+
fixes_applied.append(f"capitalized_{kw}")
|
|
45
|
+
|
|
46
|
+
# Salva mudanças
|
|
47
|
+
if fixes_applied:
|
|
48
|
+
with open(filepath, 'w', encoding='utf-8') as f:
|
|
49
|
+
f.write(content)
|
|
50
|
+
|
|
51
|
+
return fixes_applied
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AnalysisHistory:
|
|
7
|
+
"""Rastreia histórico de análises com tendências."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, history_dir=".analysis_history"):
|
|
10
|
+
self.history_dir = Path(history_dir)
|
|
11
|
+
self.history_dir.mkdir(exist_ok=True)
|
|
12
|
+
|
|
13
|
+
def _get_history_file(self, filepath):
|
|
14
|
+
"""Arquivo JSON de histórico para um arquivo Robot."""
|
|
15
|
+
return self.history_dir / f"{Path(filepath).stem}_history.json"
|
|
16
|
+
|
|
17
|
+
def record_analysis(self, filepath, issues, analysis_type="all"):
|
|
18
|
+
"""Registra resultado de análise."""
|
|
19
|
+
history_file = self._get_history_file(filepath)
|
|
20
|
+
|
|
21
|
+
# Carregar histórico existente
|
|
22
|
+
if history_file.exists():
|
|
23
|
+
with open(history_file) as f:
|
|
24
|
+
history = json.load(f)
|
|
25
|
+
else:
|
|
26
|
+
history = {"file": filepath, "runs": []}
|
|
27
|
+
|
|
28
|
+
# Adicionar novo resultado
|
|
29
|
+
run = {
|
|
30
|
+
"timestamp": datetime.now().isoformat(),
|
|
31
|
+
"analysis_type": analysis_type,
|
|
32
|
+
"issue_count": len(issues),
|
|
33
|
+
"issues_by_severity": {
|
|
34
|
+
"CRITICAL": sum(1 for i in issues if i.severity == "CRITICAL"),
|
|
35
|
+
"HIGH": sum(1 for i in issues if i.severity == "HIGH"),
|
|
36
|
+
"MEDIUM": sum(1 for i in issues if i.severity == "MEDIUM"),
|
|
37
|
+
"LOW": sum(1 for i in issues if i.severity == "LOW"),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Manter apenas últimas 100 análises
|
|
42
|
+
history["runs"].append(run)
|
|
43
|
+
history["runs"] = history["runs"][-100:]
|
|
44
|
+
|
|
45
|
+
# Salvar
|
|
46
|
+
with open(history_file, 'w') as f:
|
|
47
|
+
json.dump(history, f, indent=2)
|
|
48
|
+
|
|
49
|
+
def get_history(self, filepath):
|
|
50
|
+
"""Retorna histórico completo."""
|
|
51
|
+
history_file = self._get_history_file(filepath)
|
|
52
|
+
if history_file.exists():
|
|
53
|
+
with open(history_file) as f:
|
|
54
|
+
return json.load(f)
|
|
55
|
+
return {"file": filepath, "runs": []}
|
|
56
|
+
|
|
57
|
+
def get_trends(self, filepath):
|
|
58
|
+
"""Analisa tendências."""
|
|
59
|
+
history = self.get_history(filepath)
|
|
60
|
+
if len(history["runs"]) < 2:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
latest = history["runs"][-1]
|
|
64
|
+
previous = history["runs"][-2]
|
|
65
|
+
|
|
66
|
+
current_count = latest["issue_count"]
|
|
67
|
+
prev_count = previous["issue_count"]
|
|
68
|
+
|
|
69
|
+
if current_count < prev_count:
|
|
70
|
+
trend = "📈 melhorando"
|
|
71
|
+
elif current_count > prev_count:
|
|
72
|
+
trend = "📉 degradando"
|
|
73
|
+
else:
|
|
74
|
+
trend = "➡️ estável"
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
"trend": trend,
|
|
78
|
+
"current_count": current_count,
|
|
79
|
+
"previous_count": prev_count,
|
|
80
|
+
"change": current_count - prev_count
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
def export_json(self, filepath):
|
|
84
|
+
"""Exporta histórico em JSON."""
|
|
85
|
+
return self.get_history(filepath)
|
|
86
|
+
|
|
87
|
+
def clear(self):
|
|
88
|
+
"""Limpa todo o histórico."""
|
|
89
|
+
import shutil
|
|
90
|
+
shutil.rmtree(self.history_dir, ignore_errors=True)
|
|
91
|
+
self.history_dir.mkdir()
|
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: robotframework-quality-scanner
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Quality scanner for Robot Framework automation - static analysis, performance, duplication detection, and automatic report generation
|
|
5
|
+
Home-page: https://github.com/luisPinheiro536/qa-static-analysis
|
|
6
|
+
Author: Luis
|
|
7
|
+
Author-email: luis@example.com
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
Project-URL: Repository, https://github.com/luisPinheiro536/qa-static-analysis.git
|
|
10
|
+
Project-URL: Issues, https://github.com/luisPinheiro536/qa-static-analysis/issues
|
|
11
|
+
Project-URL: Documentation, https://github.com/luisPinheiro536/qa-static-analysis/wiki
|
|
12
|
+
Keywords: robotframework,quality,testing,static-analysis,automation
|
|
13
|
+
Platform: UNKNOWN
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Topic :: Software Development :: Testing
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: robotframework (>=6.0)
|
|
25
|
+
Provides-Extra: all
|
|
26
|
+
Requires-Dist: black (>=22.0) ; extra == 'all'
|
|
27
|
+
Requires-Dist: flake8 (>=4.0) ; extra == 'all'
|
|
28
|
+
Requires-Dist: flask (>=2.0) ; extra == 'all'
|
|
29
|
+
Requires-Dist: pytest (>=7.0) ; extra == 'all'
|
|
30
|
+
Provides-Extra: api
|
|
31
|
+
Requires-Dist: flask (>=2.0) ; extra == 'api'
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: black (>=22.0) ; extra == 'dev'
|
|
34
|
+
Requires-Dist: flake8 (>=4.0) ; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest (>=7.0) ; extra == 'dev'
|
|
36
|
+
|
|
37
|
+
# robotframework-quality-scanner
|
|
38
|
+
|
|
39
|
+
Uma **Robot Framework Library** para escanear projetos de automação **Web, Mobile e API**, identificar **más práticas**, gerar **logs estruturados**, **relatórios** e **sugestões de correção baseadas em boas práticas oficiais**.
|
|
40
|
+
|
|
41
|
+
**Versão**: 0.2.0
|
|
42
|
+
**Status**: Beta com suporte para caching, histórico, 4 analisadores especializados e API REST.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 🎯 Objetivo
|
|
47
|
+
|
|
48
|
+
* Analisar arquivos `.robot` e `.resource`
|
|
49
|
+
* Detectar anti-patterns comuns em automação
|
|
50
|
+
* Classificar problemas por severidade (CRITICAL, HIGH, MEDIUM, LOW)
|
|
51
|
+
* Explicar impacto técnico e sugerir soluções
|
|
52
|
+
* **Cache 10x mais rápido** para análises repetidas
|
|
53
|
+
* Rastrear **histórico e tendências** de qualidade
|
|
54
|
+
* Gerar **múltiplos relatórios** (JSON, HTML, TXT)
|
|
55
|
+
* Integrar facilmente com **CI/CD e ferramentas externas** via API REST
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## ✨ Features v0.2.0
|
|
60
|
+
|
|
61
|
+
### ✅ Implementado
|
|
62
|
+
|
|
63
|
+
1. **4 Analisadores Especializados**
|
|
64
|
+
- `PerformanceAnalyzer`: Deep nesting, Sleep longo, linhas muito longas
|
|
65
|
+
- `DuplicationAnalyzer`: Código duplicado, testes similares
|
|
66
|
+
- `DependencyAnalyzer`: Validação de imports, organização
|
|
67
|
+
- `TestDataAnalyzer`: Dados hardcoded, padrões data-driven
|
|
68
|
+
|
|
69
|
+
2. **Cache de Análises** (10x performance)
|
|
70
|
+
- Invalidação por hash de arquivo
|
|
71
|
+
- Armazenamento em pickle
|
|
72
|
+
|
|
73
|
+
3. **Histórico com Tendências**
|
|
74
|
+
- Rastreia 100 últimas análises por arquivo
|
|
75
|
+
- Detecta: 📈 melhorando, ➡️ estável, 📉 degradando
|
|
76
|
+
- Exporta em JSON
|
|
77
|
+
|
|
78
|
+
4. **Auto-Fix Automático**
|
|
79
|
+
- Remove trailing whitespace
|
|
80
|
+
- Normaliza indentação (tabs → spaces)
|
|
81
|
+
- Adiciona [Documentation]
|
|
82
|
+
- Capitaliza keywords
|
|
83
|
+
|
|
84
|
+
5. **API REST**
|
|
85
|
+
- Endpoints para análise de arquivo/diretório
|
|
86
|
+
- Geração de relatórios (JSON, HTML, TXT)
|
|
87
|
+
- Health check e sumário
|
|
88
|
+
|
|
89
|
+
6. **Relatórios Múltiplos**
|
|
90
|
+
- Console (estruturado)
|
|
91
|
+
- JSON (programável)
|
|
92
|
+
- HTML (visual)
|
|
93
|
+
- TXT (simples)
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 📦 Instalação
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pip install robotframework-quality-scanner
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 🚀 Uso Rápido
|
|
106
|
+
|
|
107
|
+
### Python
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from robotframework_quality_scanner import QualityScanner
|
|
111
|
+
|
|
112
|
+
scanner = QualityScanner()
|
|
113
|
+
|
|
114
|
+
# Escanear com geração automática de relatórios
|
|
115
|
+
issues, reports = scanner.scan("./tests/", generate_reports=True)
|
|
116
|
+
|
|
117
|
+
# Exibir relatório executivo
|
|
118
|
+
print(reports['executive'].to_text())
|
|
119
|
+
|
|
120
|
+
# Salvar todos os relatórios em arquivos
|
|
121
|
+
scanner.save_reports("./quality-reports")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Relatórios Automáticos
|
|
125
|
+
|
|
126
|
+
Ao final da execução, a biblioteca gera automaticamente dois relatórios:
|
|
127
|
+
|
|
128
|
+
1. **Relatório Executivo** - Sumário de qualidade com:
|
|
129
|
+
- Score de qualidade (0-100)
|
|
130
|
+
- Distribuição por severidade
|
|
131
|
+
- Distribuição por categoria
|
|
132
|
+
- Top 10 issues mais frequentes
|
|
133
|
+
- Top 5 arquivos com mais problemas
|
|
134
|
+
- Recomendações automáticas
|
|
135
|
+
|
|
136
|
+
2. **Relatório de Cobertura** - Análise de testes com:
|
|
137
|
+
- Cobertura de documentação de keywords
|
|
138
|
+
- Cobertura de uso de keywords
|
|
139
|
+
- Detecção de keywords não utilizadas
|
|
140
|
+
- Métricas por arquivo
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
# Gerar formatos específicos
|
|
144
|
+
exec_text = scanner.generate_executive_report(issues, format='text')
|
|
145
|
+
exec_html = scanner.generate_executive_report(issues, format='html')
|
|
146
|
+
exec_json = scanner.generate_executive_report(issues, format='json')
|
|
147
|
+
|
|
148
|
+
cov_text = scanner.generate_coverage_report(format='text')
|
|
149
|
+
cov_html = scanner.generate_coverage_report(format='html')
|
|
150
|
+
|
|
151
|
+
# Salvar em diretório específico
|
|
152
|
+
scanner.save_reports("./output/reports")
|
|
153
|
+
# Cria:
|
|
154
|
+
# ├── executive_report.html
|
|
155
|
+
# ├── executive_report.txt
|
|
156
|
+
# ├── executive_report.json
|
|
157
|
+
# ├── coverage_report.html
|
|
158
|
+
# └── coverage_report.txt
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Exemplo Completo
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from robotframework_quality_scanner import QualityScanner
|
|
165
|
+
|
|
166
|
+
scanner = QualityScanner()
|
|
167
|
+
issues, reports = scanner.scan("./tests/", use_cache=False, generate_reports=True)
|
|
168
|
+
|
|
169
|
+
print(f"[SUMÁRIO] {len(issues)} problemas encontrados")
|
|
170
|
+
print(f"[QUALIDADE] {reports['executive'].to_dict()['summary']['quality_score']}/100")
|
|
171
|
+
|
|
172
|
+
# Salvar relatórios
|
|
173
|
+
output = scanner.save_reports("./quality-reports")
|
|
174
|
+
print(f"✓ Relatórios salvos em: {output}")
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## 🔍 Analisadores
|
|
181
|
+
|
|
182
|
+
| Analisador | Regra | Severidade | Descrição |
|
|
183
|
+
|-----------|-------|-----------|-----------|
|
|
184
|
+
| Web | WEB001 | HIGH | Sleep detectado |
|
|
185
|
+
| Web | WEB002 | MEDIUM | XPath absoluto |
|
|
186
|
+
| Web | WEB003 | MEDIUM | URL hardcoded |
|
|
187
|
+
| Performance | PERF001 | MEDIUM | Deep nesting (>4 níveis) |
|
|
188
|
+
| Performance | PERF002 | HIGH | Sleep muito longo (>5s) |
|
|
189
|
+
| Performance | PERF003 | LOW | Linha muito longa (>120 chars) |
|
|
190
|
+
| Duplication | DUP001 | MEDIUM | Linha duplicada |
|
|
191
|
+
| Duplication | DUP002 | LOW | Testes similares (80%+) |
|
|
192
|
+
| Dependency | DEP001 | HIGH | Library com URL/path |
|
|
193
|
+
| Dependency | DEP002 | MEDIUM | Resource com extensão não-robot |
|
|
194
|
+
| Dependency | DEP003 | LOW | Library após Resource |
|
|
195
|
+
| TestData | DATA001 | MEDIUM | Email hardcoded |
|
|
196
|
+
| TestData | DATA002 | MEDIUM | URL hardcoded |
|
|
197
|
+
| TestData | DATA003 | MEDIUM | Telefone hardcoded |
|
|
198
|
+
| TestData | DATA004 | LOW | Oportunidade data-driven |
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 🧪 Testes
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
pip install -e ".[dev]"
|
|
206
|
+
pytest tests/ -v
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## 🚀 CI/CD
|
|
212
|
+
|
|
213
|
+
```yaml
|
|
214
|
+
- run: pip install robotframework-quality-scanner
|
|
215
|
+
- run: python -c "
|
|
216
|
+
from robotframework_quality_scanner import QualityScanner
|
|
217
|
+
s = QualityScanner()
|
|
218
|
+
issues = s.scan('./tests')
|
|
219
|
+
high = [i for i in issues if i.severity in ['CRITICAL', 'HIGH']]
|
|
220
|
+
exit(len(high) if high else 0)
|
|
221
|
+
"
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## 📞 Suporte
|
|
227
|
+
|
|
228
|
+
- 📄 [GitHub Issues](https://github.com/luisPinheiro536/qa-static-analysis/issues)
|
|
229
|
+
- 📧 luis@example.com
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
**Desenvolvido com ❤️ para a comunidade de QA Automation**
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## 📦 Estrutura do Projeto
|
|
238
|
+
|
|
239
|
+
```text
|
|
240
|
+
robotframework-quality-scanner/
|
|
241
|
+
├── robotframework_quality_scanner/
|
|
242
|
+
│ ├── __init__.py
|
|
243
|
+
│ ├── scanner.py
|
|
244
|
+
│ ├── logger.py
|
|
245
|
+
│ ├── rules/
|
|
246
|
+
│ │ ├── __init__.py
|
|
247
|
+
│ │ ├── base.py
|
|
248
|
+
│ │ ├── web_rules.py
|
|
249
|
+
│ │ ├── api_rules.py
|
|
250
|
+
│ │ └── mobile_rules.py
|
|
251
|
+
│ ├── models/
|
|
252
|
+
│ │ ├── issue.py
|
|
253
|
+
│ │ └── report.py
|
|
254
|
+
│ ├── reporters/
|
|
255
|
+
│ │ ├── console_reporter.py
|
|
256
|
+
│ │ └── json_reporter.py
|
|
257
|
+
│ └── suggestions/
|
|
258
|
+
│ ├── web.py
|
|
259
|
+
│ ├── api.py
|
|
260
|
+
│ └── mobile.py
|
|
261
|
+
├── examples/
|
|
262
|
+
│ ├── bad_web.robot
|
|
263
|
+
│ ├── bad_api.robot
|
|
264
|
+
│ └── bad_mobile.robot
|
|
265
|
+
├── tests/
|
|
266
|
+
├── .github/workflows/ci.yml
|
|
267
|
+
├── README.md
|
|
268
|
+
├── pyproject.toml
|
|
269
|
+
└── setup.py
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## 🧠 Modelo de Issue
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
class Issue:
|
|
278
|
+
def __init__(self, rule_id, category, severity, description,
|
|
279
|
+
file, line, recommendation, reference):
|
|
280
|
+
self.rule_id = rule_id
|
|
281
|
+
self.category = category
|
|
282
|
+
self.severity = severity
|
|
283
|
+
self.description = description
|
|
284
|
+
self.file = file
|
|
285
|
+
self.line = line
|
|
286
|
+
self.recommendation = recommendation
|
|
287
|
+
self.reference = reference
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## 🔎 Scanner Principal
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
class QualityScanner:
|
|
296
|
+
def scan(self, path):
|
|
297
|
+
issues = []
|
|
298
|
+
issues += WebRules().analyze(path)
|
|
299
|
+
issues += ApiRules().analyze(path)
|
|
300
|
+
issues += MobileRules().analyze(path)
|
|
301
|
+
return issues
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## ✅ Regras Implementadas (10)
|
|
307
|
+
|
|
308
|
+
### Web (Selenium)
|
|
309
|
+
|
|
310
|
+
1. Uso de `Sleep`
|
|
311
|
+
2. XPath absoluto
|
|
312
|
+
3. Falta de waits explícitos
|
|
313
|
+
4. Hardcoded URL
|
|
314
|
+
|
|
315
|
+
### API
|
|
316
|
+
|
|
317
|
+
5. Validação apenas de status code
|
|
318
|
+
6. Sem validação de schema JSON
|
|
319
|
+
7. Headers hardcoded
|
|
320
|
+
|
|
321
|
+
### Mobile (Appium)
|
|
322
|
+
|
|
323
|
+
8. Tap por coordenadas
|
|
324
|
+
9. Uso de `Sleep` em mobile
|
|
325
|
+
10. Ausência de `accessibility_id`
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## 🕸️ Exemplo de Regra (WEB001)
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
if 'Sleep' in line:
|
|
333
|
+
issues.append(Issue(
|
|
334
|
+
rule_id='WEB001',
|
|
335
|
+
category='WEB',
|
|
336
|
+
severity='HIGH',
|
|
337
|
+
description='Uso de Sleep detectado.',
|
|
338
|
+
file=file,
|
|
339
|
+
line=line_no,
|
|
340
|
+
recommendation='Use Wait Until Element Is Visible.',
|
|
341
|
+
reference='https://robotframework.org/SeleniumLibrary/'
|
|
342
|
+
))
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## 🧪 Exemplos Ruins
|
|
348
|
+
|
|
349
|
+
### bad_web.robot
|
|
350
|
+
|
|
351
|
+
```robot
|
|
352
|
+
*** Test Cases ***
|
|
353
|
+
Login
|
|
354
|
+
Open Browser http://site.com chrome
|
|
355
|
+
Sleep 5s
|
|
356
|
+
Click Element /html/body/div[2]/button
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## 📊 Logs no Console
|
|
362
|
+
|
|
363
|
+
```text
|
|
364
|
+
[HIGH] WEB001 - bad_web.robot:5
|
|
365
|
+
Uso de Sleep detectado
|
|
366
|
+
Sugestão: Use Wait Until Element Is Visible
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## 📄 JSON Report (CI/CD)
|
|
372
|
+
|
|
373
|
+
```json
|
|
374
|
+
{
|
|
375
|
+
"rule_id": "WEB001",
|
|
376
|
+
"severity": "HIGH",
|
|
377
|
+
"file": "bad_web.robot",
|
|
378
|
+
"recommendation": "Use waits explícitos"
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
## 🤖 Uso no Robot Framework
|
|
385
|
+
|
|
386
|
+
```robot
|
|
387
|
+
*** Settings ***
|
|
388
|
+
Library QualityScanner
|
|
389
|
+
|
|
390
|
+
*** Test Cases ***
|
|
391
|
+
Scan Project
|
|
392
|
+
Scan Project ./examples
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## 📦 pyproject.toml (PyPI)
|
|
398
|
+
|
|
399
|
+
```toml
|
|
400
|
+
[project]
|
|
401
|
+
name = "robotframework-quality-scanner"
|
|
402
|
+
version = "0.1.0"
|
|
403
|
+
description = "Quality scanner for Robot Framework automation"
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## 🔁 GitHub Actions (.github/workflows/ci.yml)
|
|
409
|
+
|
|
410
|
+
```yaml
|
|
411
|
+
name: CI
|
|
412
|
+
on: [push]
|
|
413
|
+
jobs:
|
|
414
|
+
scan:
|
|
415
|
+
runs-on: ubuntu-latest
|
|
416
|
+
steps:
|
|
417
|
+
- uses: actions/checkout@v3
|
|
418
|
+
- uses: actions/setup-python@v4
|
|
419
|
+
- run: pip install .
|
|
420
|
+
- run: robot -L TRACE examples/
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## 🚀 Roadmap
|
|
426
|
+
|
|
427
|
+
### v0.2.0 – Engine & Reports
|
|
428
|
+
|
|
429
|
+
* Rule Engine configurável via YAML
|
|
430
|
+
* HTML Report estilo Allure
|
|
431
|
+
* Quality Gate por severidade
|
|
432
|
+
|
|
433
|
+
### v0.3.0 – CI/CD & Segurança
|
|
434
|
+
|
|
435
|
+
* Exportação SARIF (GitHub Code Scanning)
|
|
436
|
+
* GitHub Action oficial
|
|
437
|
+
|
|
438
|
+
### v1.0.0 – Educação & Comunidade
|
|
439
|
+
|
|
440
|
+
* Guia educacional para times QA
|
|
441
|
+
* Catálogo de boas práticas
|
|
442
|
+
* Regras comunitárias
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## 🧩 Rule Engine em YAML
|
|
447
|
+
|
|
448
|
+
As regras são definidas externamente em YAML, permitindo fácil extensão:
|
|
449
|
+
|
|
450
|
+
```yaml
|
|
451
|
+
rules:
|
|
452
|
+
- id: WEB001
|
|
453
|
+
category: WEB
|
|
454
|
+
severity: HIGH
|
|
455
|
+
match: "Sleep"
|
|
456
|
+
description: Uso de Sleep detectado
|
|
457
|
+
recommendation: Utilize waits explícitos
|
|
458
|
+
reference: https://robotframework.org/SeleniumLibrary/
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
O scanner carrega dinamicamente essas regras e aplica regex/keywords nos arquivos `.robot`.
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## 📊 HTML Report (Estilo Allure)
|
|
466
|
+
|
|
467
|
+
O relatório HTML apresenta:
|
|
468
|
+
|
|
469
|
+
* Cards por severidade
|
|
470
|
+
* Tabela detalhada de issues
|
|
471
|
+
* Resumo executivo (total, críticos, avisos)
|
|
472
|
+
|
|
473
|
+
Arquivo gerado: `quality-report.html`
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## 🚦 Quality Gate
|
|
478
|
+
|
|
479
|
+
É possível configurar falha do build por severidade:
|
|
480
|
+
|
|
481
|
+
```yaml
|
|
482
|
+
quality_gate:
|
|
483
|
+
fail_on:
|
|
484
|
+
- CRITICAL
|
|
485
|
+
- HIGH
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
Se uma issue dessas severidades for encontrada, o scanner retorna exit code ≠ 0.
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## 🛡️ SARIF (GitHub Code Scanning)
|
|
493
|
+
|
|
494
|
+
O relatório pode ser exportado em SARIF para integração nativa com GitHub Security:
|
|
495
|
+
|
|
496
|
+
* Visualização direto no Pull Request
|
|
497
|
+
* Histórico de problemas
|
|
498
|
+
* Comentários automáticos
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
## 🤖 GitHub Action Oficial
|
|
503
|
+
|
|
504
|
+
```yaml
|
|
505
|
+
name: Robot Framework Quality Scanner
|
|
506
|
+
runs:
|
|
507
|
+
using: "docker"
|
|
508
|
+
steps:
|
|
509
|
+
- run: quality-scanner ./tests --rules rules.yaml
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Permite uso simples em qualquer pipeline GitHub.
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
## 🎓 Ferramenta Educacional para QA
|
|
517
|
+
|
|
518
|
+
A library pode ser usada como:
|
|
519
|
+
|
|
520
|
+
* Checklist automatizado de boas práticas
|
|
521
|
+
* Ferramenta de onboarding de QAs
|
|
522
|
+
* Base para treinamentos internos
|
|
523
|
+
* Apoio em code review de testes
|
|
524
|
+
|
|
525
|
+
Cada issue explica:
|
|
526
|
+
|
|
527
|
+
* O problema
|
|
528
|
+
* O impacto
|
|
529
|
+
* A melhor prática
|
|
530
|
+
* Referência oficial
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
## 🌍 Visão de Longo Prazo
|
|
535
|
+
|
|
536
|
+
Este projeto pode evoluir para:
|
|
537
|
+
|
|
538
|
+
* Padrão de mercado em qualidade de automação
|
|
539
|
+
* Plugin educacional para IDEs
|
|
540
|
+
* Base de conhecimento comunitária
|
|
541
|
+
|
|
542
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
robotframework_quality_scanner/__init__.py,sha256=SMr9z8TAgpcn5mTWC45mb2z1wz7DNzC4E-MGtu6-egY,89
|
|
2
|
+
robotframework_quality_scanner/scanner.py,sha256=yUH1GsRL7zD2TEslLTDuGInDvaGbkWe7QkjvanyJDHo,6370
|
|
3
|
+
robotframework_quality_scanner/analyzers/__init__.py,sha256=nlCNkqOyGr2g7J9m5H-Leg0KTZ9qFAT0S09YDiMRKuM,30
|
|
4
|
+
robotframework_quality_scanner/analyzers/dependency_analyzer.py,sha256=2ICeH9TwUKvbpQeXrbz0mef3frXXg6T4zq9E-uPrX-M,2285
|
|
5
|
+
robotframework_quality_scanner/analyzers/duplication_analyzer.py,sha256=Hrz6VnUXJ8ISJeytNOyF0qEctlWVzXicVJq8Hlvx_gQ,1715
|
|
6
|
+
robotframework_quality_scanner/analyzers/performance_analyzer.py,sha256=mFCdc61-MVVRWlzMGok2lSqbSVpWCD7Z6MngFximgXk,2253
|
|
7
|
+
robotframework_quality_scanner/analyzers/test_data_analyzer.py,sha256=aeNsFYe9LHsPQ7R11uQt4fr4kQ3ZCM-DedY_vM-pNgI,2826
|
|
8
|
+
robotframework_quality_scanner/api/__init__.py,sha256=xl-neC26bRT40f60h_PS4w-mNshZiim8p_EGBA5WrTU,3404
|
|
9
|
+
robotframework_quality_scanner/utils/__init__.py,sha256=q98sL93w76bJTpUUEGzsUpOG7grLgFISi7TP4eS6IJo,1708
|
|
10
|
+
robotframework_quality_scanner/utils/autofix.py,sha256=OE5KvYzh8uoRuFjacgKad6FdYCXrWfVI4XaWIgqVDqw,1915
|
|
11
|
+
robotframework_quality_scanner/utils/history.py,sha256=BELwUsK9khIUYkhfDUCm5BQTkv5VhebA2PSIpFNGhg4,3066
|
|
12
|
+
robotframework_quality_scanner-0.3.0.dist-info/METADATA,sha256=Zowpn4ksUEMWAPJmroAb1WUx_WqEqnccSww7iUFl-Wc,13000
|
|
13
|
+
robotframework_quality_scanner-0.3.0.dist-info/WHEEL,sha256=g4nMs7d-Xl9-xC9XovUrsDHGXt-FT0E17Yqo92DEfvY,92
|
|
14
|
+
robotframework_quality_scanner-0.3.0.dist-info/top_level.txt,sha256=l_98_FUfXp-gKkhWwc_F4CMWu7hezGyvgsc8IunhHNk,31
|
|
15
|
+
robotframework_quality_scanner-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
robotframework_quality_scanner
|