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.
@@ -0,0 +1,5 @@
1
+ __version__ = "0.1.0"
2
+
3
+ from .scanner import QualityScanner
4
+
5
+ __all__ = ["QualityScanner"]
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.34.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ robotframework_quality_scanner