airev 1.1.1__tar.gz → 1.3.0__tar.gz
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.
- {airev-1.1.1 → airev-1.3.0}/PKG-INFO +1 -1
- {airev-1.1.1 → airev-1.3.0}/src/airev.egg-info/PKG-INFO +1 -1
- {airev-1.1.1 → airev-1.3.0}/src/airev.egg-info/SOURCES.txt +1 -0
- {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/__init__.py +1 -1
- {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/cli.py +19 -7
- {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/formatters/terminal.py +25 -2
- {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/locales/en.yaml +7 -0
- {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/locales/pt-br.yaml +7 -0
- {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/models.py +2 -1
- {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/prompt_builder.py +48 -0
- {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/prompts/review_system.md +1 -1
- {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/response_parser.py +5 -0
- {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/updater/__init__.py +2 -1
- {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/updater/http_client.py +1 -1
- airev-1.3.0/src/code_reviewer/updater/upgrade.py +182 -0
- {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/updater/version_check.py +18 -0
- airev-1.3.0/tests/test_cli.py +29 -0
- {airev-1.1.1 → airev-1.3.0}/tests/test_prompt_builder.py +67 -0
- {airev-1.1.1 → airev-1.3.0}/tests/test_response_parser.py +7 -0
- {airev-1.1.1 → airev-1.3.0}/tests/test_updater.py +215 -8
- airev-1.1.1/src/code_reviewer/updater/upgrade.py +0 -87
- {airev-1.1.1 → airev-1.3.0}/README.md +0 -0
- {airev-1.1.1 → airev-1.3.0}/pyproject.toml +0 -0
- {airev-1.1.1 → airev-1.3.0}/setup.cfg +0 -0
- {airev-1.1.1 → airev-1.3.0}/src/airev.egg-info/dependency_links.txt +0 -0
- {airev-1.1.1 → airev-1.3.0}/src/airev.egg-info/entry_points.txt +0 -0
- {airev-1.1.1 → airev-1.3.0}/src/airev.egg-info/requires.txt +0 -0
- {airev-1.1.1 → airev-1.3.0}/src/airev.egg-info/top_level.txt +0 -0
- {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/context_builder.py +0 -0
- {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/diff_parser.py +0 -0
- {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/formatters/__init__.py +0 -0
- {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/formatters/progress.py +0 -0
- {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/i18n/__init__.py +0 -0
- {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/runners/__init__.py +0 -0
- {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/runners/base.py +0 -0
- {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/runners/copilot.py +0 -0
- {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/runners/gemini.py +0 -0
- {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/updater/notifier.py +0 -0
- {airev-1.1.1 → airev-1.3.0}/tests/test_context_builder.py +0 -0
- {airev-1.1.1 → airev-1.3.0}/tests/test_copilot_runner.py +0 -0
- {airev-1.1.1 → airev-1.3.0}/tests/test_diff_parser.py +0 -0
- {airev-1.1.1 → airev-1.3.0}/tests/test_i18n.py +0 -0
- {airev-1.1.1 → airev-1.3.0}/tests/test_progress.py +0 -0
- {airev-1.1.1 → airev-1.3.0}/tests/test_terminal_formatter.py +0 -0
|
@@ -29,6 +29,7 @@ src/code_reviewer/updater/http_client.py
|
|
|
29
29
|
src/code_reviewer/updater/notifier.py
|
|
30
30
|
src/code_reviewer/updater/upgrade.py
|
|
31
31
|
src/code_reviewer/updater/version_check.py
|
|
32
|
+
tests/test_cli.py
|
|
32
33
|
tests/test_context_builder.py
|
|
33
34
|
tests/test_copilot_runner.py
|
|
34
35
|
tests/test_diff_parser.py
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""CLI Entry Point - Comando principal do
|
|
1
|
+
"""CLI Entry Point - Comando principal do airev."""
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
4
|
import time
|
|
@@ -75,6 +75,13 @@ def main():
|
|
|
75
75
|
type=click.Choice(get_available_languages()),
|
|
76
76
|
help="Idioma das mensagens (default: pt-br)",
|
|
77
77
|
)
|
|
78
|
+
@click.option(
|
|
79
|
+
"--text-quality",
|
|
80
|
+
"-t",
|
|
81
|
+
is_flag=True,
|
|
82
|
+
default=False,
|
|
83
|
+
help="Ativa verificação de ortografia e clareza em mensagens de usuário",
|
|
84
|
+
)
|
|
78
85
|
def review(
|
|
79
86
|
base: str,
|
|
80
87
|
runner: str,
|
|
@@ -83,18 +90,21 @@ def review(
|
|
|
83
90
|
no_progress: bool,
|
|
84
91
|
progress: bool,
|
|
85
92
|
lang: str,
|
|
93
|
+
text_quality: bool,
|
|
86
94
|
):
|
|
87
95
|
"""Analisa o diff da branch atual contra a branch base.
|
|
88
96
|
|
|
89
97
|
Exemplos:
|
|
90
98
|
|
|
91
|
-
|
|
99
|
+
airev review --base main
|
|
100
|
+
|
|
101
|
+
airev review --base develop --runner copilot
|
|
92
102
|
|
|
93
|
-
|
|
103
|
+
airev review --base main --json-output
|
|
94
104
|
|
|
95
|
-
|
|
105
|
+
airev review --base main --no-progress
|
|
96
106
|
|
|
97
|
-
|
|
107
|
+
airev review --base main --text-quality
|
|
98
108
|
"""
|
|
99
109
|
workdir = workdir or Path.cwd()
|
|
100
110
|
start_time = time.perf_counter()
|
|
@@ -173,7 +183,9 @@ def review(
|
|
|
173
183
|
|
|
174
184
|
# Monta o prompt
|
|
175
185
|
with reporter.status(t("cli.building_prompt")):
|
|
176
|
-
prompt = build_prompt(
|
|
186
|
+
prompt = build_prompt(
|
|
187
|
+
diff_files, context_graphs, current_branch, base, text_quality=text_quality
|
|
188
|
+
)
|
|
177
189
|
|
|
178
190
|
# Obtém o runner
|
|
179
191
|
try:
|
|
@@ -232,7 +244,7 @@ def runners():
|
|
|
232
244
|
|
|
233
245
|
@main.command()
|
|
234
246
|
def upgrade():
|
|
235
|
-
"""Atualiza o
|
|
247
|
+
"""Atualiza o airev para a versão mais recente."""
|
|
236
248
|
from rich.console import Console
|
|
237
249
|
|
|
238
250
|
console = Console()
|
|
@@ -5,7 +5,7 @@ from typing import TextIO
|
|
|
5
5
|
import sys
|
|
6
6
|
|
|
7
7
|
from ..i18n import t
|
|
8
|
-
from ..models import Finding, ReviewResult, Severity
|
|
8
|
+
from ..models import Category, Finding, ReviewResult, Severity
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
# Códigos ANSI para cores
|
|
@@ -71,6 +71,28 @@ def format_severity(severity: Severity) -> str:
|
|
|
71
71
|
return _colorize(f"[{severity.value}]", Colors.BOLD, Colors.BLUE)
|
|
72
72
|
|
|
73
73
|
|
|
74
|
+
def format_category_badge(category: Category) -> str:
|
|
75
|
+
"""Formata o badge da categoria.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
category: Categoria do finding
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
String formatada com ícone e cor para a categoria
|
|
82
|
+
"""
|
|
83
|
+
# Ícones e cores por categoria
|
|
84
|
+
category_styles = {
|
|
85
|
+
Category.SECURITY: ("🔒", Colors.RED),
|
|
86
|
+
Category.PERFORMANCE: ("⚡", Colors.YELLOW),
|
|
87
|
+
Category.BUG: ("🐛", Colors.MAGENTA),
|
|
88
|
+
Category.RESOURCE_LEAK: ("💧", Colors.CYAN),
|
|
89
|
+
Category.TEXT_QUALITY: ("✏️", Colors.CYAN),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
icon, color = category_styles.get(category, ("•", Colors.WHITE))
|
|
93
|
+
return f"{icon} {_colorize(category.value, color)}"
|
|
94
|
+
|
|
95
|
+
|
|
74
96
|
def format_finding(finding: Finding) -> str:
|
|
75
97
|
"""Formata um finding para exibição.
|
|
76
98
|
|
|
@@ -84,10 +106,11 @@ def format_finding(finding: Finding) -> str:
|
|
|
84
106
|
|
|
85
107
|
# Header: [SEVERITY] arquivo:linha - Título
|
|
86
108
|
severity_str = format_severity(finding.severity)
|
|
109
|
+
category_badge = format_category_badge(finding.category)
|
|
87
110
|
location = _colorize(f"{finding.file}:{finding.line}", Colors.CYAN)
|
|
88
111
|
title = _colorize(finding.title, Colors.BOLD)
|
|
89
112
|
|
|
90
|
-
lines.append(f" {severity_str} {location} - {title}")
|
|
113
|
+
lines.append(f" {severity_str} {category_badge} {location} - {title}")
|
|
91
114
|
|
|
92
115
|
# Descrição
|
|
93
116
|
if finding.description:
|
|
@@ -47,6 +47,13 @@ terminal:
|
|
|
47
47
|
# Findings
|
|
48
48
|
suggestion: "Suggestion:"
|
|
49
49
|
|
|
50
|
+
# Categories
|
|
51
|
+
category_security: "security"
|
|
52
|
+
category_performance: "performance"
|
|
53
|
+
category_bug: "bug"
|
|
54
|
+
category_resource_leak: "resource leak"
|
|
55
|
+
category_text_quality: "text quality"
|
|
56
|
+
|
|
50
57
|
# Summary
|
|
51
58
|
summary: "SUMMARY:"
|
|
52
59
|
findings_count: "{count} finding(s)"
|
|
@@ -47,6 +47,13 @@ terminal:
|
|
|
47
47
|
# Findings
|
|
48
48
|
suggestion: "Sugestão:"
|
|
49
49
|
|
|
50
|
+
# Categorias
|
|
51
|
+
category_security: "segurança"
|
|
52
|
+
category_performance: "performance"
|
|
53
|
+
category_bug: "bug"
|
|
54
|
+
category_resource_leak: "vazamento de recurso"
|
|
55
|
+
category_text_quality: "qualidade de texto"
|
|
56
|
+
|
|
50
57
|
# Resumo
|
|
51
58
|
summary: "RESUMO:"
|
|
52
59
|
findings_count: "{count} finding(s)"
|
|
@@ -21,6 +21,7 @@ class Category(str, Enum):
|
|
|
21
21
|
PERFORMANCE = "performance"
|
|
22
22
|
BUG = "bug"
|
|
23
23
|
RESOURCE_LEAK = "resource-leak"
|
|
24
|
+
TEXT_QUALITY = "text-quality"
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
class DiffLine(BaseModel):
|
|
@@ -90,7 +91,7 @@ class Finding(BaseModel):
|
|
|
90
91
|
line: int = Field(description="Número da linha")
|
|
91
92
|
severity: Severity = Field(description="Severidade: CRITICAL, WARNING, INFO")
|
|
92
93
|
category: Category = Field(
|
|
93
|
-
description="Categoria: security, performance, bug, resource-leak"
|
|
94
|
+
description="Categoria: security, performance, bug, resource-leak, text-quality"
|
|
94
95
|
)
|
|
95
96
|
title: str = Field(description="Título curto do problema")
|
|
96
97
|
description: str = Field(description="Descrição detalhada do problema")
|
|
@@ -152,11 +152,54 @@ def format_references_for_prompt(context_graphs: list[ContextGraph]) -> str:
|
|
|
152
152
|
return "\n".join(parts)
|
|
153
153
|
|
|
154
154
|
|
|
155
|
+
def get_text_quality_section(language_name: str) -> str:
|
|
156
|
+
"""Retorna a seção de instruções para verificação de qualidade de texto.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
language_name: Nome do idioma para verificação
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
String com instruções de verificação de texto
|
|
163
|
+
"""
|
|
164
|
+
return f"""
|
|
165
|
+
## QUALIDADE DE TEXTO
|
|
166
|
+
|
|
167
|
+
Verifique ortografia e clareza semântica em mensagens voltadas ao usuário, no idioma **{language_name}**.
|
|
168
|
+
|
|
169
|
+
### O que verificar:
|
|
170
|
+
|
|
171
|
+
**Padrões de código:**
|
|
172
|
+
- `raise *Error("...")` e `raise *Exception("...")`
|
|
173
|
+
- `print("...")` e `console.log("...")`
|
|
174
|
+
- Parâmetros nomeados: `message=`, `label=`, `title=`, `description=`, `text=`
|
|
175
|
+
- Funções de UI: `flash("...")`, `toast("...")`, `alert("...")`
|
|
176
|
+
|
|
177
|
+
**Arquivos de i18n:**
|
|
178
|
+
- Arquivos em `locales/**/*`
|
|
179
|
+
- Arquivos em `i18n/**/*`
|
|
180
|
+
- Arquivos `messages.*` e `strings.*`
|
|
181
|
+
|
|
182
|
+
### O que ignorar:
|
|
183
|
+
|
|
184
|
+
- Identificadores: snake_case, camelCase, PascalCase
|
|
185
|
+
- Termos técnicos: HTTP, JSON, API, SQL, URL, etc.
|
|
186
|
+
- Nomes próprios e termos de domínio específico
|
|
187
|
+
- Chaves de configuração e variáveis de ambiente
|
|
188
|
+
|
|
189
|
+
### Formato dos findings:
|
|
190
|
+
|
|
191
|
+
- Categoria: `text-quality`
|
|
192
|
+
- Severidade: sempre `INFO`
|
|
193
|
+
- Inclua a correção sugerida no campo `suggestion`
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
|
|
155
197
|
def build_prompt(
|
|
156
198
|
diff_files: list[DiffFile],
|
|
157
199
|
context_graphs: list[ContextGraph],
|
|
158
200
|
branch: str,
|
|
159
201
|
base: str,
|
|
202
|
+
text_quality: bool = False,
|
|
160
203
|
) -> str:
|
|
161
204
|
"""Monta o prompt completo para a IA.
|
|
162
205
|
|
|
@@ -165,6 +208,7 @@ def build_prompt(
|
|
|
165
208
|
context_graphs: Grafos de contexto com backtracking
|
|
166
209
|
branch: Nome da branch sendo analisada
|
|
167
210
|
base: Nome da branch base
|
|
211
|
+
text_quality: Se True, inclui verificação de ortografia e clareza
|
|
168
212
|
|
|
169
213
|
Returns:
|
|
170
214
|
Prompt completo pronto para enviar à IA
|
|
@@ -183,11 +227,15 @@ def build_prompt(
|
|
|
183
227
|
lang_code = get_language()
|
|
184
228
|
language_name = LANGUAGE_NAMES.get(lang_code, lang_code)
|
|
185
229
|
|
|
230
|
+
# Seção de text-quality (condicional)
|
|
231
|
+
text_quality_section = get_text_quality_section(language_name) if text_quality else ""
|
|
232
|
+
|
|
186
233
|
# Substitui placeholders
|
|
187
234
|
prompt = template.replace("{diff}", diff_section)
|
|
188
235
|
prompt = prompt.replace("{context}", context_section)
|
|
189
236
|
prompt = prompt.replace("{references}", references_section)
|
|
190
237
|
prompt = prompt.replace("{json_schema}", json_schema)
|
|
191
238
|
prompt = prompt.replace("{language}", language_name)
|
|
239
|
+
prompt = prompt.replace("{text_quality_section}", text_quality_section)
|
|
192
240
|
|
|
193
241
|
return prompt
|
|
@@ -25,7 +25,7 @@ O DIFF abaixo mostra APENAS as linhas alteradas, não o arquivo completo. Códig
|
|
|
25
25
|
- **Performance**: N+1 queries, loops desnecessários, operações O(n²), falta de índices, carregamento excessivo
|
|
26
26
|
- **Bugs potenciais**: null pointer, race conditions, off-by-one, divisão por zero, exceções não tratadas
|
|
27
27
|
- **Recursos não fechados**: conexões de banco, arquivos, sockets, locks não liberados
|
|
28
|
-
|
|
28
|
+
{text_quality_section}
|
|
29
29
|
## FORMATO DE SAÍDA
|
|
30
30
|
|
|
31
31
|
Retorne APENAS um JSON válido no formato abaixo. Não inclua explicações fora do JSON.
|
|
@@ -121,6 +121,11 @@ def normalize_category(value: str) -> Category:
|
|
|
121
121
|
"resource_leak": Category.RESOURCE_LEAK,
|
|
122
122
|
"leak": Category.RESOURCE_LEAK,
|
|
123
123
|
"memory": Category.RESOURCE_LEAK,
|
|
124
|
+
"text-quality": Category.TEXT_QUALITY,
|
|
125
|
+
"text_quality": Category.TEXT_QUALITY,
|
|
126
|
+
"spelling": Category.TEXT_QUALITY,
|
|
127
|
+
"grammar": Category.TEXT_QUALITY,
|
|
128
|
+
"typo": Category.TEXT_QUALITY,
|
|
124
129
|
}
|
|
125
130
|
|
|
126
131
|
return mapping.get(value_lower, Category.BUG)
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"""Módulo de auto-update para verificação e atualização de versões."""
|
|
2
2
|
|
|
3
|
-
from .version_check import check_for_update, UpdateInfo
|
|
3
|
+
from .version_check import check_for_update, clear_cache, UpdateInfo
|
|
4
4
|
from .notifier import notify_update
|
|
5
5
|
from .upgrade import detect_installer, run_upgrade
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"check_for_update",
|
|
9
|
+
"clear_cache",
|
|
9
10
|
"UpdateInfo",
|
|
10
11
|
"notify_update",
|
|
11
12
|
"detect_installer",
|
|
@@ -30,7 +30,7 @@ class UrllibClient:
|
|
|
30
30
|
try:
|
|
31
31
|
request = urllib.request.Request(
|
|
32
32
|
url,
|
|
33
|
-
headers={"Accept": "application/json", "User-Agent": "
|
|
33
|
+
headers={"Accept": "application/json", "User-Agent": "airev"},
|
|
34
34
|
)
|
|
35
35
|
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
36
36
|
data = response.read().decode("utf-8")
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Funcionalidades de upgrade do pacote."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from .. import __version__
|
|
11
|
+
from .version_check import check_for_update, clear_cache
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def detect_installer() -> str:
|
|
15
|
+
"""Detecta qual instalador foi usado (pipx ou pip).
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
"pipx" se instalado via pipx, "pip" caso contrário
|
|
19
|
+
"""
|
|
20
|
+
exe_path = Path(sys.executable)
|
|
21
|
+
|
|
22
|
+
# pipx instala em ~/.local/pipx/venvs/
|
|
23
|
+
if "pipx" in exe_path.parts:
|
|
24
|
+
return "pipx"
|
|
25
|
+
|
|
26
|
+
return "pip"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_installed_version(installer: str) -> str | None:
|
|
30
|
+
"""Obtém a versão real instalada do airev consultando o instalador.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
installer: "pipx" ou "pip"
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
String com a versão instalada ou None se não conseguir obter
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
if installer == "pipx":
|
|
40
|
+
# Tenta usar pipx list --json
|
|
41
|
+
result = subprocess.run(
|
|
42
|
+
["pipx", "list", "--json"],
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
check=False,
|
|
46
|
+
)
|
|
47
|
+
if result.returncode == 0:
|
|
48
|
+
data = json.loads(result.stdout)
|
|
49
|
+
venvs = data.get("venvs", {})
|
|
50
|
+
if "airev" in venvs:
|
|
51
|
+
metadata = venvs["airev"].get("metadata", {})
|
|
52
|
+
return metadata.get("main_package", {}).get("package_version")
|
|
53
|
+
# Fallback: tenta sem --json (versões antigas do pipx)
|
|
54
|
+
return _get_version_from_pipx_list_text()
|
|
55
|
+
else:
|
|
56
|
+
# pip show airev
|
|
57
|
+
result = subprocess.run(
|
|
58
|
+
[sys.executable, "-m", "pip", "show", "airev"],
|
|
59
|
+
capture_output=True,
|
|
60
|
+
text=True,
|
|
61
|
+
check=False,
|
|
62
|
+
)
|
|
63
|
+
if result.returncode == 0:
|
|
64
|
+
for line in result.stdout.splitlines():
|
|
65
|
+
if line.startswith("Version:"):
|
|
66
|
+
return line.split(":", 1)[1].strip()
|
|
67
|
+
except (subprocess.SubprocessError, json.JSONDecodeError, KeyError):
|
|
68
|
+
pass
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _get_version_from_pipx_list_text() -> str | None:
|
|
73
|
+
"""Fallback para obter versão do pipx list sem --json.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
String com a versão ou None se não encontrar
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
result = subprocess.run(
|
|
80
|
+
["pipx", "list"],
|
|
81
|
+
capture_output=True,
|
|
82
|
+
text=True,
|
|
83
|
+
check=False,
|
|
84
|
+
)
|
|
85
|
+
if result.returncode == 0:
|
|
86
|
+
# Procura por linha como "airev 1.0.0" ou "package airev 1.0.0"
|
|
87
|
+
for line in result.stdout.splitlines():
|
|
88
|
+
if "airev" in line.lower():
|
|
89
|
+
parts = line.split()
|
|
90
|
+
for i, part in enumerate(parts):
|
|
91
|
+
if "airev" in part.lower() and i + 1 < len(parts):
|
|
92
|
+
version = parts[i + 1].strip(",")
|
|
93
|
+
# Verifica se parece uma versão
|
|
94
|
+
if version and version[0].isdigit():
|
|
95
|
+
return version
|
|
96
|
+
except subprocess.SubprocessError:
|
|
97
|
+
pass
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def run_upgrade(console: Console | None = None) -> bool:
|
|
102
|
+
"""Executa upgrade do pacote.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
console: Console Rich para output (opcional)
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
True se upgrade foi executado com sucesso, False caso contrário
|
|
109
|
+
"""
|
|
110
|
+
if console is None:
|
|
111
|
+
console = Console()
|
|
112
|
+
|
|
113
|
+
# Verifica se há atualização disponível
|
|
114
|
+
console.print("[dim]Verificando atualizações...[/]")
|
|
115
|
+
update_info = check_for_update()
|
|
116
|
+
|
|
117
|
+
if update_info is None:
|
|
118
|
+
# Limpa cache mesmo quando não há atualização (garante consistência)
|
|
119
|
+
clear_cache()
|
|
120
|
+
console.print(
|
|
121
|
+
f"[green]✓[/] Você já está na versão mais recente ([cyan]{__version__}[/])"
|
|
122
|
+
)
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
installer = detect_installer()
|
|
126
|
+
|
|
127
|
+
# Obtém versão antes do upgrade para comparação
|
|
128
|
+
version_before = get_installed_version(installer) or __version__
|
|
129
|
+
|
|
130
|
+
console.print(
|
|
131
|
+
f"[yellow]→[/] Atualizando de {version_before} "
|
|
132
|
+
f"para {update_info.latest_version}..."
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Monta comando de upgrade
|
|
136
|
+
if installer == "pipx":
|
|
137
|
+
cmd = ["pipx", "upgrade", "airev"]
|
|
138
|
+
else:
|
|
139
|
+
cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "airev"]
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
result = subprocess.run(
|
|
143
|
+
cmd,
|
|
144
|
+
capture_output=True,
|
|
145
|
+
text=True,
|
|
146
|
+
check=False,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Sempre limpa o cache após tentativa de upgrade
|
|
150
|
+
clear_cache()
|
|
151
|
+
|
|
152
|
+
if result.returncode == 0:
|
|
153
|
+
# Verifica se a versão realmente mudou
|
|
154
|
+
version_after = get_installed_version(installer)
|
|
155
|
+
|
|
156
|
+
if version_after and version_after != version_before:
|
|
157
|
+
console.print(
|
|
158
|
+
f"[green]✓[/] Atualizado de {version_before} para {version_after}"
|
|
159
|
+
)
|
|
160
|
+
return True
|
|
161
|
+
else:
|
|
162
|
+
# Comando executou mas versão não mudou
|
|
163
|
+
console.print(
|
|
164
|
+
f"[green]✓[/] Você já está na versão mais recente "
|
|
165
|
+
f"([cyan]{version_after or version_before}[/])"
|
|
166
|
+
)
|
|
167
|
+
return False
|
|
168
|
+
else:
|
|
169
|
+
console.print(f"[red]✗[/] Falha ao atualizar: {result.stderr.strip()}")
|
|
170
|
+
console.print(f"[dim]Tente manualmente: {' '.join(cmd)}[/]")
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
except FileNotFoundError:
|
|
174
|
+
clear_cache()
|
|
175
|
+
console.print(f"[red]✗[/] Comando '{cmd[0]}' não encontrado")
|
|
176
|
+
if installer == "pipx":
|
|
177
|
+
console.print("[dim]Tente: pip install --upgrade airev[/]")
|
|
178
|
+
return False
|
|
179
|
+
except Exception as e:
|
|
180
|
+
clear_cache()
|
|
181
|
+
console.print(f"[red]✗[/] Erro inesperado: {e}")
|
|
182
|
+
return False
|
|
@@ -110,6 +110,24 @@ def _write_cache(latest_version: str) -> None:
|
|
|
110
110
|
pass
|
|
111
111
|
|
|
112
112
|
|
|
113
|
+
def clear_cache() -> bool:
|
|
114
|
+
"""Remove o arquivo de cache de verificação de versão.
|
|
115
|
+
|
|
116
|
+
Deve ser chamada após upgrade para garantir que a próxima
|
|
117
|
+
verificação consulte o PyPI novamente.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
True se o cache foi removido ou não existia, False se falhou
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
if CACHE_FILE.exists():
|
|
124
|
+
CACHE_FILE.unlink()
|
|
125
|
+
return True
|
|
126
|
+
except OSError:
|
|
127
|
+
# Falha ao remover cache - ignora silenciosamente
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
|
|
113
131
|
def get_latest_version(timeout: float = DEFAULT_TIMEOUT) -> str | None:
|
|
114
132
|
"""Consulta PyPI para obter a versão mais recente.
|
|
115
133
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Testes para o CLI."""
|
|
2
|
+
|
|
3
|
+
from click.testing import CliRunner
|
|
4
|
+
|
|
5
|
+
from code_reviewer.cli import review
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestReviewCommand:
|
|
9
|
+
"""Testes para o comando review."""
|
|
10
|
+
|
|
11
|
+
def test_flag_text_quality_reconhecida(self):
|
|
12
|
+
"""Verifica que a flag --text-quality é aceita pelo CLI."""
|
|
13
|
+
runner = CliRunner()
|
|
14
|
+
|
|
15
|
+
# Invoca com --help para verificar que a flag existe
|
|
16
|
+
result = runner.invoke(review, ["--help"])
|
|
17
|
+
|
|
18
|
+
assert result.exit_code == 0
|
|
19
|
+
assert "--text-quality" in result.output
|
|
20
|
+
assert "ortografia" in result.output or "spelling" in result.output.lower()
|
|
21
|
+
|
|
22
|
+
def test_flag_text_quality_short_form(self):
|
|
23
|
+
"""Verifica que a forma curta -t funciona."""
|
|
24
|
+
runner = CliRunner()
|
|
25
|
+
|
|
26
|
+
result = runner.invoke(review, ["--help"])
|
|
27
|
+
|
|
28
|
+
assert result.exit_code == 0
|
|
29
|
+
assert "-t" in result.output
|
|
@@ -188,3 +188,70 @@ class TestBuildPrompt:
|
|
|
188
188
|
assert "REGRAS" in prompt
|
|
189
189
|
assert "test.py" in prompt
|
|
190
190
|
assert "review" in prompt # Schema JSON
|
|
191
|
+
|
|
192
|
+
def test_prompt_sem_text_quality_por_padrao(self):
|
|
193
|
+
"""Verifica que seção text-quality não aparece quando flag desativada."""
|
|
194
|
+
diff_files = [
|
|
195
|
+
DiffFile(
|
|
196
|
+
path="test.py",
|
|
197
|
+
hunks=[
|
|
198
|
+
DiffHunk(
|
|
199
|
+
function_name="test",
|
|
200
|
+
start_line_old=1,
|
|
201
|
+
start_line_new=1,
|
|
202
|
+
added_lines=[],
|
|
203
|
+
removed_lines=[],
|
|
204
|
+
)
|
|
205
|
+
],
|
|
206
|
+
)
|
|
207
|
+
]
|
|
208
|
+
context_graphs = []
|
|
209
|
+
|
|
210
|
+
prompt = build_prompt(diff_files, context_graphs, "feature/test", "main")
|
|
211
|
+
|
|
212
|
+
# Não deve conter seção de text-quality
|
|
213
|
+
assert "QUALIDADE DE TEXTO" not in prompt
|
|
214
|
+
assert "text-quality" not in prompt
|
|
215
|
+
|
|
216
|
+
def test_prompt_com_text_quality_ativo(self):
|
|
217
|
+
"""Verifica que seção text-quality aparece quando flag ativada."""
|
|
218
|
+
diff_files = [
|
|
219
|
+
DiffFile(
|
|
220
|
+
path="test.py",
|
|
221
|
+
hunks=[
|
|
222
|
+
DiffHunk(
|
|
223
|
+
function_name="test",
|
|
224
|
+
start_line_old=1,
|
|
225
|
+
start_line_new=1,
|
|
226
|
+
added_lines=[],
|
|
227
|
+
removed_lines=[],
|
|
228
|
+
)
|
|
229
|
+
],
|
|
230
|
+
)
|
|
231
|
+
]
|
|
232
|
+
context_graphs = []
|
|
233
|
+
|
|
234
|
+
prompt = build_prompt(
|
|
235
|
+
diff_files, context_graphs, "feature/test", "main", text_quality=True
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Deve conter seção de text-quality
|
|
239
|
+
assert "QUALIDADE DE TEXTO" in prompt
|
|
240
|
+
assert "text-quality" in prompt
|
|
241
|
+
assert "ortografia" in prompt
|
|
242
|
+
assert "raise *Error" in prompt
|
|
243
|
+
assert "locales/" in prompt
|
|
244
|
+
|
|
245
|
+
def test_prompt_text_quality_menciona_ignorar_identificadores(self):
|
|
246
|
+
"""Verifica que instruções de exclusão estão presentes."""
|
|
247
|
+
diff_files = []
|
|
248
|
+
context_graphs = []
|
|
249
|
+
|
|
250
|
+
prompt = build_prompt(
|
|
251
|
+
diff_files, context_graphs, "feature/test", "main", text_quality=True
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Deve instruir a ignorar identificadores
|
|
255
|
+
assert "snake_case" in prompt
|
|
256
|
+
assert "camelCase" in prompt
|
|
257
|
+
assert "termos técnicos" in prompt or "Termos técnicos" in prompt
|
|
@@ -117,6 +117,13 @@ class TestNormalizeCategory:
|
|
|
117
117
|
assert normalize_category("resource-leak") == Category.RESOURCE_LEAK
|
|
118
118
|
assert normalize_category("memory") == Category.RESOURCE_LEAK
|
|
119
119
|
|
|
120
|
+
def test_text_quality(self):
|
|
121
|
+
assert normalize_category("text-quality") == Category.TEXT_QUALITY
|
|
122
|
+
assert normalize_category("text_quality") == Category.TEXT_QUALITY
|
|
123
|
+
assert normalize_category("spelling") == Category.TEXT_QUALITY
|
|
124
|
+
assert normalize_category("grammar") == Category.TEXT_QUALITY
|
|
125
|
+
assert normalize_category("typo") == Category.TEXT_QUALITY
|
|
126
|
+
|
|
120
127
|
def test_desconhecido_para_bug(self):
|
|
121
128
|
assert normalize_category("unknown") == Category.BUG
|
|
122
129
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Testes para o módulo de auto-update."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import subprocess
|
|
4
5
|
import sys
|
|
5
6
|
from datetime import datetime, timedelta, timezone
|
|
6
7
|
from io import StringIO
|
|
@@ -17,11 +18,16 @@ from code_reviewer.updater.version_check import (
|
|
|
17
18
|
_read_cache,
|
|
18
19
|
_write_cache,
|
|
19
20
|
check_for_update,
|
|
21
|
+
clear_cache,
|
|
20
22
|
compare_versions,
|
|
21
23
|
get_latest_version,
|
|
22
24
|
)
|
|
23
25
|
from code_reviewer.updater.notifier import notify_update
|
|
24
|
-
from code_reviewer.updater.upgrade import
|
|
26
|
+
from code_reviewer.updater.upgrade import (
|
|
27
|
+
detect_installer,
|
|
28
|
+
get_installed_version,
|
|
29
|
+
run_upgrade,
|
|
30
|
+
)
|
|
25
31
|
|
|
26
32
|
|
|
27
33
|
class TestHttpClient:
|
|
@@ -159,6 +165,48 @@ class TestCache:
|
|
|
159
165
|
assert cache is None
|
|
160
166
|
|
|
161
167
|
|
|
168
|
+
class TestClearCache:
|
|
169
|
+
"""Testes para clear_cache."""
|
|
170
|
+
|
|
171
|
+
def test_clear_existing_cache(self, tmp_path):
|
|
172
|
+
"""Deve remover arquivo de cache existente."""
|
|
173
|
+
cache_file = tmp_path / "update-check.json"
|
|
174
|
+
cache_file.write_text('{"test": true}')
|
|
175
|
+
|
|
176
|
+
with patch(
|
|
177
|
+
"code_reviewer.updater.version_check.CACHE_FILE", cache_file
|
|
178
|
+
):
|
|
179
|
+
result = clear_cache()
|
|
180
|
+
|
|
181
|
+
assert result is True
|
|
182
|
+
assert not cache_file.exists()
|
|
183
|
+
|
|
184
|
+
def test_clear_nonexistent_cache(self, tmp_path):
|
|
185
|
+
"""Deve retornar True quando cache não existe."""
|
|
186
|
+
cache_file = tmp_path / "nonexistent.json"
|
|
187
|
+
|
|
188
|
+
with patch(
|
|
189
|
+
"code_reviewer.updater.version_check.CACHE_FILE", cache_file
|
|
190
|
+
):
|
|
191
|
+
result = clear_cache()
|
|
192
|
+
|
|
193
|
+
assert result is True
|
|
194
|
+
|
|
195
|
+
def test_clear_cache_permission_error(self, tmp_path):
|
|
196
|
+
"""Deve retornar False quando não consegue remover."""
|
|
197
|
+
cache_file = tmp_path / "update-check.json"
|
|
198
|
+
cache_file.write_text('{"test": true}')
|
|
199
|
+
|
|
200
|
+
with patch(
|
|
201
|
+
"code_reviewer.updater.version_check.CACHE_FILE", cache_file
|
|
202
|
+
), patch(
|
|
203
|
+
"pathlib.Path.unlink", side_effect=OSError("Permission denied")
|
|
204
|
+
):
|
|
205
|
+
result = clear_cache()
|
|
206
|
+
|
|
207
|
+
assert result is False
|
|
208
|
+
|
|
209
|
+
|
|
162
210
|
class TestCheckForUpdate:
|
|
163
211
|
"""Testes para check_for_update."""
|
|
164
212
|
|
|
@@ -252,7 +300,7 @@ class TestDetectInstaller:
|
|
|
252
300
|
|
|
253
301
|
def test_detect_pipx(self):
|
|
254
302
|
"""Deve detectar pipx quando no path do executável."""
|
|
255
|
-
fake_path = Path("/home/user/.local/pipx/venvs/
|
|
303
|
+
fake_path = Path("/home/user/.local/pipx/venvs/airev/bin/python")
|
|
256
304
|
with patch.object(sys, "executable", str(fake_path)):
|
|
257
305
|
result = detect_installer()
|
|
258
306
|
assert result == "pipx"
|
|
@@ -272,59 +320,167 @@ class TestDetectInstaller:
|
|
|
272
320
|
assert result == "pip"
|
|
273
321
|
|
|
274
322
|
|
|
323
|
+
class TestGetInstalledVersion:
|
|
324
|
+
"""Testes para get_installed_version."""
|
|
325
|
+
|
|
326
|
+
def test_get_version_pipx_json(self):
|
|
327
|
+
"""Deve obter versão do pipx list --json."""
|
|
328
|
+
pipx_json = {
|
|
329
|
+
"venvs": {
|
|
330
|
+
"airev": {
|
|
331
|
+
"metadata": {
|
|
332
|
+
"main_package": {
|
|
333
|
+
"package_version": "1.2.3"
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
mock_result = MagicMock(returncode=0, stdout=json.dumps(pipx_json))
|
|
340
|
+
|
|
341
|
+
with patch("subprocess.run", return_value=mock_result):
|
|
342
|
+
result = get_installed_version("pipx")
|
|
343
|
+
|
|
344
|
+
assert result == "1.2.3"
|
|
345
|
+
|
|
346
|
+
def test_get_version_pipx_not_installed(self):
|
|
347
|
+
"""Deve retornar None quando airev não está instalado no pipx."""
|
|
348
|
+
pipx_json = {"venvs": {}}
|
|
349
|
+
mock_result = MagicMock(returncode=0, stdout=json.dumps(pipx_json))
|
|
350
|
+
|
|
351
|
+
with patch("subprocess.run", return_value=mock_result):
|
|
352
|
+
result = get_installed_version("pipx")
|
|
353
|
+
|
|
354
|
+
assert result is None
|
|
355
|
+
|
|
356
|
+
def test_get_version_pip(self):
|
|
357
|
+
"""Deve obter versão do pip show."""
|
|
358
|
+
pip_output = """Name: airev
|
|
359
|
+
Version: 1.0.0
|
|
360
|
+
Summary: Code reviewer CLI
|
|
361
|
+
"""
|
|
362
|
+
mock_result = MagicMock(returncode=0, stdout=pip_output)
|
|
363
|
+
|
|
364
|
+
with patch("subprocess.run", return_value=mock_result):
|
|
365
|
+
result = get_installed_version("pip")
|
|
366
|
+
|
|
367
|
+
assert result == "1.0.0"
|
|
368
|
+
|
|
369
|
+
def test_get_version_pip_not_installed(self):
|
|
370
|
+
"""Deve retornar None quando pip show falha."""
|
|
371
|
+
mock_result = MagicMock(returncode=1, stdout="")
|
|
372
|
+
|
|
373
|
+
with patch("subprocess.run", return_value=mock_result):
|
|
374
|
+
result = get_installed_version("pip")
|
|
375
|
+
|
|
376
|
+
assert result is None
|
|
377
|
+
|
|
378
|
+
def test_get_version_subprocess_error(self):
|
|
379
|
+
"""Deve retornar None em erro de subprocess."""
|
|
380
|
+
with patch("subprocess.run", side_effect=subprocess.SubprocessError):
|
|
381
|
+
result = get_installed_version("pipx")
|
|
382
|
+
|
|
383
|
+
assert result is None
|
|
384
|
+
|
|
385
|
+
|
|
275
386
|
class TestRunUpgrade:
|
|
276
387
|
"""Testes para run_upgrade."""
|
|
277
388
|
|
|
278
389
|
def test_upgrade_no_update_available(self):
|
|
279
|
-
"""Deve retornar False quando não há update."""
|
|
390
|
+
"""Deve retornar False e limpar cache quando não há update."""
|
|
280
391
|
from rich.console import Console
|
|
281
392
|
|
|
282
393
|
console = Console(file=StringIO(), force_terminal=True)
|
|
283
394
|
|
|
284
395
|
with patch(
|
|
285
396
|
"code_reviewer.updater.upgrade.check_for_update", return_value=None
|
|
286
|
-
)
|
|
397
|
+
), patch(
|
|
398
|
+
"code_reviewer.updater.upgrade.clear_cache", return_value=True
|
|
399
|
+
) as mock_clear:
|
|
287
400
|
result = run_upgrade(console)
|
|
288
401
|
|
|
289
402
|
assert result is False
|
|
403
|
+
mock_clear.assert_called_once()
|
|
290
404
|
|
|
291
|
-
def
|
|
292
|
-
"""Deve executar pipx upgrade
|
|
405
|
+
def test_upgrade_success_pipx_version_changed(self):
|
|
406
|
+
"""Deve executar pipx upgrade e verificar mudança de versão."""
|
|
293
407
|
from rich.console import Console
|
|
294
408
|
|
|
295
409
|
console = Console(file=StringIO(), force_terminal=True)
|
|
296
410
|
update_info = UpdateInfo(current_version="0.1.0", latest_version="0.2.0")
|
|
297
411
|
mock_result = MagicMock(returncode=0, stderr="")
|
|
298
412
|
|
|
413
|
+
# Simula versão mudando de 0.1.0 para 0.2.0 após upgrade
|
|
414
|
+
version_calls = iter(["0.1.0", "0.2.0"])
|
|
415
|
+
|
|
299
416
|
with patch(
|
|
300
417
|
"code_reviewer.updater.upgrade.check_for_update",
|
|
301
418
|
return_value=update_info,
|
|
302
419
|
), patch(
|
|
303
420
|
"code_reviewer.updater.upgrade.detect_installer", return_value="pipx"
|
|
304
421
|
), patch(
|
|
422
|
+
"code_reviewer.updater.upgrade.get_installed_version",
|
|
423
|
+
side_effect=lambda _: next(version_calls),
|
|
424
|
+
), patch(
|
|
425
|
+
"code_reviewer.updater.upgrade.clear_cache", return_value=True
|
|
426
|
+
) as mock_clear, patch(
|
|
305
427
|
"subprocess.run", return_value=mock_result
|
|
306
428
|
) as mock_run:
|
|
307
429
|
result = run_upgrade(console)
|
|
308
430
|
|
|
309
431
|
assert result is True
|
|
310
432
|
mock_run.assert_called_once()
|
|
433
|
+
mock_clear.assert_called_once()
|
|
311
434
|
cmd = mock_run.call_args[0][0]
|
|
312
435
|
assert "pipx" in cmd
|
|
313
436
|
assert "upgrade" in cmd
|
|
314
437
|
|
|
315
|
-
def
|
|
316
|
-
"""Deve
|
|
438
|
+
def test_upgrade_success_but_version_unchanged(self):
|
|
439
|
+
"""Deve retornar False quando versão não muda após upgrade."""
|
|
440
|
+
from rich.console import Console
|
|
441
|
+
|
|
442
|
+
console = Console(file=StringIO(), force_terminal=True)
|
|
443
|
+
update_info = UpdateInfo(current_version="0.1.0", latest_version="0.2.0")
|
|
444
|
+
mock_result = MagicMock(returncode=0, stderr="")
|
|
445
|
+
|
|
446
|
+
with patch(
|
|
447
|
+
"code_reviewer.updater.upgrade.check_for_update",
|
|
448
|
+
return_value=update_info,
|
|
449
|
+
), patch(
|
|
450
|
+
"code_reviewer.updater.upgrade.detect_installer", return_value="pipx"
|
|
451
|
+
), patch(
|
|
452
|
+
"code_reviewer.updater.upgrade.get_installed_version",
|
|
453
|
+
return_value="0.1.0", # Versão não muda
|
|
454
|
+
), patch(
|
|
455
|
+
"code_reviewer.updater.upgrade.clear_cache", return_value=True
|
|
456
|
+
) as mock_clear, patch(
|
|
457
|
+
"subprocess.run", return_value=mock_result
|
|
458
|
+
):
|
|
459
|
+
result = run_upgrade(console)
|
|
460
|
+
|
|
461
|
+
assert result is False
|
|
462
|
+
mock_clear.assert_called_once()
|
|
463
|
+
|
|
464
|
+
def test_upgrade_success_pip_version_changed(self):
|
|
465
|
+
"""Deve executar pip install --upgrade e verificar mudança."""
|
|
317
466
|
from rich.console import Console
|
|
318
467
|
|
|
319
468
|
console = Console(file=StringIO(), force_terminal=True)
|
|
320
469
|
update_info = UpdateInfo(current_version="0.1.0", latest_version="0.2.0")
|
|
321
470
|
mock_result = MagicMock(returncode=0, stderr="")
|
|
322
471
|
|
|
472
|
+
version_calls = iter(["0.1.0", "0.2.0"])
|
|
473
|
+
|
|
323
474
|
with patch(
|
|
324
475
|
"code_reviewer.updater.upgrade.check_for_update",
|
|
325
476
|
return_value=update_info,
|
|
326
477
|
), patch(
|
|
327
478
|
"code_reviewer.updater.upgrade.detect_installer", return_value="pip"
|
|
479
|
+
), patch(
|
|
480
|
+
"code_reviewer.updater.upgrade.get_installed_version",
|
|
481
|
+
side_effect=lambda _: next(version_calls),
|
|
482
|
+
), patch(
|
|
483
|
+
"code_reviewer.updater.upgrade.clear_cache", return_value=True
|
|
328
484
|
), patch(
|
|
329
485
|
"subprocess.run", return_value=mock_result
|
|
330
486
|
) as mock_run:
|
|
@@ -335,3 +491,54 @@ class TestRunUpgrade:
|
|
|
335
491
|
cmd = mock_run.call_args[0][0]
|
|
336
492
|
assert "--upgrade" in cmd
|
|
337
493
|
assert "airev" in cmd
|
|
494
|
+
|
|
495
|
+
def test_upgrade_clears_cache_on_failure(self):
|
|
496
|
+
"""Deve limpar cache mesmo quando upgrade falha."""
|
|
497
|
+
from rich.console import Console
|
|
498
|
+
|
|
499
|
+
console = Console(file=StringIO(), force_terminal=True)
|
|
500
|
+
update_info = UpdateInfo(current_version="0.1.0", latest_version="0.2.0")
|
|
501
|
+
mock_result = MagicMock(returncode=1, stderr="Error")
|
|
502
|
+
|
|
503
|
+
with patch(
|
|
504
|
+
"code_reviewer.updater.upgrade.check_for_update",
|
|
505
|
+
return_value=update_info,
|
|
506
|
+
), patch(
|
|
507
|
+
"code_reviewer.updater.upgrade.detect_installer", return_value="pipx"
|
|
508
|
+
), patch(
|
|
509
|
+
"code_reviewer.updater.upgrade.get_installed_version",
|
|
510
|
+
return_value="0.1.0",
|
|
511
|
+
), patch(
|
|
512
|
+
"code_reviewer.updater.upgrade.clear_cache", return_value=True
|
|
513
|
+
) as mock_clear, patch(
|
|
514
|
+
"subprocess.run", return_value=mock_result
|
|
515
|
+
):
|
|
516
|
+
result = run_upgrade(console)
|
|
517
|
+
|
|
518
|
+
assert result is False
|
|
519
|
+
mock_clear.assert_called_once()
|
|
520
|
+
|
|
521
|
+
def test_upgrade_clears_cache_on_exception(self):
|
|
522
|
+
"""Deve limpar cache quando ocorre exceção."""
|
|
523
|
+
from rich.console import Console
|
|
524
|
+
|
|
525
|
+
console = Console(file=StringIO(), force_terminal=True)
|
|
526
|
+
update_info = UpdateInfo(current_version="0.1.0", latest_version="0.2.0")
|
|
527
|
+
|
|
528
|
+
with patch(
|
|
529
|
+
"code_reviewer.updater.upgrade.check_for_update",
|
|
530
|
+
return_value=update_info,
|
|
531
|
+
), patch(
|
|
532
|
+
"code_reviewer.updater.upgrade.detect_installer", return_value="pipx"
|
|
533
|
+
), patch(
|
|
534
|
+
"code_reviewer.updater.upgrade.get_installed_version",
|
|
535
|
+
return_value="0.1.0",
|
|
536
|
+
), patch(
|
|
537
|
+
"code_reviewer.updater.upgrade.clear_cache", return_value=True
|
|
538
|
+
) as mock_clear, patch(
|
|
539
|
+
"subprocess.run", side_effect=FileNotFoundError
|
|
540
|
+
):
|
|
541
|
+
result = run_upgrade(console)
|
|
542
|
+
|
|
543
|
+
assert result is False
|
|
544
|
+
mock_clear.assert_called_once()
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
"""Funcionalidades de upgrade do pacote."""
|
|
2
|
-
|
|
3
|
-
import subprocess
|
|
4
|
-
import sys
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
from rich.console import Console
|
|
8
|
-
|
|
9
|
-
from .. import __version__
|
|
10
|
-
from .version_check import check_for_update
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def detect_installer() -> str:
|
|
14
|
-
"""Detecta qual instalador foi usado (pipx ou pip).
|
|
15
|
-
|
|
16
|
-
Returns:
|
|
17
|
-
"pipx" se instalado via pipx, "pip" caso contrário
|
|
18
|
-
"""
|
|
19
|
-
exe_path = Path(sys.executable)
|
|
20
|
-
|
|
21
|
-
# pipx instala em ~/.local/pipx/venvs/
|
|
22
|
-
if "pipx" in exe_path.parts:
|
|
23
|
-
return "pipx"
|
|
24
|
-
|
|
25
|
-
return "pip"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def run_upgrade(console: Console | None = None) -> bool:
|
|
29
|
-
"""Executa upgrade do pacote.
|
|
30
|
-
|
|
31
|
-
Args:
|
|
32
|
-
console: Console Rich para output (opcional)
|
|
33
|
-
|
|
34
|
-
Returns:
|
|
35
|
-
True se upgrade foi executado, False se já está atualizado ou falhou
|
|
36
|
-
"""
|
|
37
|
-
if console is None:
|
|
38
|
-
console = Console()
|
|
39
|
-
|
|
40
|
-
# Verifica se há atualização disponível
|
|
41
|
-
console.print("[dim]Verificando atualizações...[/]")
|
|
42
|
-
update_info = check_for_update()
|
|
43
|
-
|
|
44
|
-
if update_info is None:
|
|
45
|
-
console.print(
|
|
46
|
-
f"[green]✓[/] Você já está na versão mais recente ([cyan]{__version__}[/])"
|
|
47
|
-
)
|
|
48
|
-
return False
|
|
49
|
-
|
|
50
|
-
installer = detect_installer()
|
|
51
|
-
console.print(
|
|
52
|
-
f"[yellow]→[/] Atualizando de {update_info.current_version} "
|
|
53
|
-
f"para {update_info.latest_version}..."
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
# Monta comando de upgrade
|
|
57
|
-
if installer == "pipx":
|
|
58
|
-
cmd = ["pipx", "upgrade", "airev"]
|
|
59
|
-
else:
|
|
60
|
-
cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "airev"]
|
|
61
|
-
|
|
62
|
-
try:
|
|
63
|
-
result = subprocess.run(
|
|
64
|
-
cmd,
|
|
65
|
-
capture_output=True,
|
|
66
|
-
text=True,
|
|
67
|
-
check=False,
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
if result.returncode == 0:
|
|
71
|
-
console.print(
|
|
72
|
-
f"[green]✓[/] Atualizado para versão {update_info.latest_version}"
|
|
73
|
-
)
|
|
74
|
-
return True
|
|
75
|
-
else:
|
|
76
|
-
console.print(f"[red]✗[/] Falha ao atualizar: {result.stderr.strip()}")
|
|
77
|
-
console.print(f"[dim]Tente manualmente: {' '.join(cmd)}[/]")
|
|
78
|
-
return False
|
|
79
|
-
|
|
80
|
-
except FileNotFoundError:
|
|
81
|
-
console.print(f"[red]✗[/] Comando '{cmd[0]}' não encontrado")
|
|
82
|
-
if installer == "pipx":
|
|
83
|
-
console.print("[dim]Tente: pip install --upgrade airev[/]")
|
|
84
|
-
return False
|
|
85
|
-
except Exception as e:
|
|
86
|
-
console.print(f"[red]✗[/] Erro inesperado: {e}")
|
|
87
|
-
return False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|