From ed5e936c096f07c6741b75bbd9a15b45fafa200d Mon Sep 17 00:00:00 2001 From: Gil Cleber Barboza Date: Tue, 21 Jul 2026 11:33:51 -0300 Subject: [PATCH] fix: parse reporters utPLSQL 3.2.x + Windows cmd.exe bypass (PRDs-35,36) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - parseReportersOutput: regex ^([A-Za-z0-9_]+) sem .trim() para ignorar descrições indentadas do formato utPLSQL 3.2.x (PRD-36) - coverageEnabled flag previne mensagem falsa 'GRANT EXECUTE' quando reporter de cobertura não está disponível - Windows launcher: usa cmd.exe diretamente com shell:false, bypassing o wrap duplo de quoting do Node.js (PRD-35) - Log [debug] CLI com args sem connection string - +9 testes unitários (quoteArg, parseReportersOutput, applyCoverage) - CHANGELOG, .vscodeignore atualizados - Versão 0.7.2 --- .vscodeignore | 2 + CHANGELOG.md | 12 + docs/prd/index.md | 6 +- .../proposed/prd-35-windows-coverage-fix.md | 225 ++++++++++++++++++ .../prd/proposed/prd-36-reporter-parse-fix.md | 177 ++++++++++++++ docs/wiki/FAQ.md | 4 +- .../Instala\303\247\303\243o-e-requisitos.md" | 2 +- package-lock.json | 4 +- package.json | 2 +- src/cli.ts | 8 +- src/cliReporters.ts | 5 +- src/runner.ts | 23 +- src/test/unit/cli.test.ts | 22 ++ src/test/unit/cliReporters.test.ts | 49 +++- src/test/unit/runner.test.ts | 42 +++- 15 files changed, 567 insertions(+), 16 deletions(-) create mode 100644 docs/prd/proposed/prd-35-windows-coverage-fix.md create mode 100644 docs/prd/proposed/prd-36-reporter-parse-fix.md diff --git a/.vscodeignore b/.vscodeignore index 2ac41f9..429749d 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -17,3 +17,5 @@ node_modules/.bin/** .opencode/** coverage/** .env +*.rar +*.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index e204259..e83d862 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 0.7.2 + +- Correção: parse de reporters com formato utPLSQL 3.2.x — nomes com sufixo `:` e + descrições indentadas não eram reconhecidos, desabilitando cobertura silenciosamente (PRD-36). +- Flag `coverageEnabled` impede diagnóstico falso de "GRANT EXECUTE" quando o reporter + de cobertura não está disponível. +- Correção: relatório de cobertura não gerado no Windows com modo `launcher` (PRD-35): + bypass do wrap duplo de quoting entre `quoteArg` e o Node.js no `cmd.exe`. +- Blindagem de testes para `quoteArg` com caminhos Windows, padrões regex e strings com `=`. +- Diagnóstico aprimorado em `applyCoverage` quando o arquivo de cobertura não existe. +- Log de argumentos CLI (sem connection) para facilitar debugging. + ## 0.7.1 - Alinhamento `engines.node` com CI (PRD-18): requisito relaxado de `^24.0.0` para `>=20.0.0`. diff --git a/docs/prd/index.md b/docs/prd/index.md index 2d69be8..c667c20 100644 --- a/docs/prd/index.md +++ b/docs/prd/index.md @@ -127,6 +127,8 @@ CONCLUIR | 32 | [Quick-Fix Setup Diagnostics](proposed/prd-32-quickfix-setup-diagnostics.md) | 0.9.0 | 2026-07-21 | | 33 | [PL/SQL Debugger Integration](proposed/prd-33-plsql-debugger-integration.md) | 1.0.0 | 2026-07-21 | | 34 | [Multi-Connection Profiles](proposed/prd-34-multi-connection-profiles.md) | 1.0.0 | 2026-07-21 | +| 35 | [Correção de cobertura no Windows + blindagem de testes](proposed/prd-35-windows-coverage-fix.md) | 0.7.2 | 2026-07-21 | +| 36 | [Correção do parse de reporters com descrições](proposed/prd-36-reporter-parse-fix.md) | 0.7.2 | 2026-07-21 | --- @@ -175,7 +177,9 @@ docs/prd/ ├── prd-30-schema-aware-organization.md ├── prd-32-quickfix-setup-diagnostics.md ├── prd-33-plsql-debugger-integration.md - └── prd-34-multi-connection-profiles.md + ├── prd-34-multi-connection-profiles.md + ├── prd-35-windows-coverage-fix.md + └── prd-36-reporter-parse-fix.md ``` --- diff --git a/docs/prd/proposed/prd-35-windows-coverage-fix.md b/docs/prd/proposed/prd-35-windows-coverage-fix.md new file mode 100644 index 0000000..1779e1d --- /dev/null +++ b/docs/prd/proposed/prd-35-windows-coverage-fix.md @@ -0,0 +1,225 @@ +# PRD-35 — Correção de cobertura no Windows + blindagem de testes para argumentos CLI + +| Campo | Valor | +|---|---| +| Status | Proposto | +| Autor | Gil Cleber Barboza | +| Data | 2026-07-21 | +| Componente | Extensão `paneb.vscode-utplsql` | +| Versão alvo | 0.7.2 | +| Arquivos afetados | `src/cli.ts`, `src/runner.ts`, `src/test/unit/cli.test.ts`, `src/test/unit/runner.test.ts` | + +## 1. Resumo + +Corrigir a falha de geração de cobertura no Windows com `invocation: launcher`, +causada pela dupla camada de quoting entre `quoteArg` e o wrap interno do +Node.js no `cmd.exe`. A solução bypassa o wrap extra chamando `cmd.exe` +diretamente com `shell: false`. Inclui blindagem de testes unitários para +prevenir regressões em argumentos com caminhos Windows, padrões regex e +strings com `=`. + +## 2. Contexto e problema + +No modo `launcher`, o `runCli` concatena todos os argumentos via `quoteArg` e +os passa como string única para `cp.spawn` com `shell: true`. No Windows, o +Node.js internamente faz: + +``` +spawn('cmd.exe', ['/d', '/s', '/c', '""']) +``` + +O `` já contém aspas dos args citados por `quoteArg` (ex: +`"-type_mapping=packages=PACKAGE BODY/..."`). O `/S` do `cmd.exe` tira a +primeira e última `"`, mas com múltiplas aspas internas o resultado pode não +reconstituir corretamente os argumentos originais — especialmente com os +padrões regex dos `coverageSourceArgs` que contêm `\\`, `\w+` e `$`. + +Além disso, `listReporters` em `runner.ts:106` roda o CLI via `shell: true` +para validar se o reporter de cobertura existe. Se essa chamada falhar (mesmo +que o CLI de teste funcione), os args de cobertura nunca são adicionados, mas +`applyCoverage` é chamado de qualquer forma (linhas 174-176), exibindo a +mensagem enganosa sobre `GRANT EXECUTE ON SYS.DBMS_PROFILER`. + +### Lacuna de testes + +Os testes atuais de `quoteArg` (`cli.test.ts`) não cobrem: + +| Cenário | Risco | +|---|---| +| Caminhos Windows com `\` (`-o=C:\...\coverage.xml`) | `cmd.exe` interpreta `\"` como escape | +| Padrões regex com `\\` e `$` (`coverageSourceArgs`) | Quoting duplo quebra metacaracteres | +| Strings com `=` e espaços (`-type_mapping=...`) | Quoting parcial gera inconsistência | +| Diagnóstico do `applyCoverage` com arquivo ausente | Sem verificação da mensagem de erro | + +## 3. Objetivos / Não-objetivos + +**Objetivos** +- Corrigir a falha de cobertura no Windows modo `launcher` +- Eliminar a dupla camada de quoting (`Node.js` + `quoteArg`) no Windows +- Adicionar testes unitários para `quoteArg` com caminhos Windows, regex e `=` +- Adicionar teste para diagnóstico do `applyCoverage` +- Logar args do CLI (sem connection) para facilitar debugging futuro + +**Não-objetivos** +- Alterar `quoteArg` (mantém compatibilidade com Unix) +- Alterar `buildInvocation` ou `listReporters` +- Testes de integração cross-platform para o caminho Windows (CI roda Linux) +- Suporte a novos modos de invocação + +## 4. Requisitos + +### RF1 — Bypass do wrap duplo no Windows + +No `runCli`, detectar Windows e usar `cmd.exe` diretamente com `shell: false`, +evitando que o Node.js adicione `"..."` ao redor do comando já quotado. + +```typescript +// src/cli.ts — dentro de runCli +const child = shell + ? (process.platform === 'win32' + ? cp.spawn('cmd.exe', ['/d', '/s', '/c', [file, ...args].map(quoteArg).join(' ')], + { cwd, shell: false, windowsHide: true }) + : cp.spawn([file, ...args].map(quoteArg).join(' '), + { cwd, shell: true, windowsHide: true })) + : cp.spawn(file, args, { cwd, shell: false, windowsHide: true }); +``` + +Linux/macOS mantêm o comportamento atual inalterado. + +### RF2 — Diagnóstico aprimorado em `applyCoverage` + +Quando `coverage.xml` não existe, exibir: +- Caminho esperado do arquivo +- Conteúdo do diretório temp (lista de arquivos ou "não encontrado") +- Sugestão de tentar modo `java` como alternativa + +### RF3 — Log de args do CLI + +Antes de executar o CLI, logar os argumentos sem a string de conexão: + +```typescript +// src/runner.ts — após buildInvocation, antes de runCli +const safeArgs = inv.args.map(a => + a === connection ? '***' : a.replace(connection, '***') +); +run.appendOutput(`[debug] CLI: ${inv.file} ${safeArgs.join(' ')}\r\n`); +``` + +### RF4 — Testes de `quoteArg` com caminhos e regex + +Adicionar a `src/test/unit/cli.test.ts`: + +| Teste | Input | Esperado | +|---|---|---| +| Caminho Windows sem espaço | `-o=C:\Temp\cov.xml` | `-o=C:\Temp\cov.xml` (sem alteração) | +| Caminho Windows com espaço | `-o=C:\My Docs\cov.xml` | `"-o=C:\My Docs\cov.xml"` | +| Regex `coverageSourceArgs` | `-regex_expression=.*[/\\](\w+)\.sql$` | `"-regex_expression=.*[/\\](\w+)\.sql$"` | +| `type_mapping` com `/` e `=` | `-type_mapping=p=PACKAGE BODY/f=FUNCTION` | `"-type_mapping=p=PACKAGE BODY/f=FUNCTION"` | + +### RF5 — Teste do diagnóstico `applyCoverage` + +Adicionar a `src/test/unit/runner.test.ts`: + +Teste que verifica se, quando o arquivo de cobertura não existe, a saída +contém o caminho esperado e a lista de arquivos do diretório temp. + +**Não-funcionais** +- RNF1 — `npm test` (129 testes) continua passando sem alteração +- RNF2 — `npm run lint` (biome) sem novos warnings +- RNF3 — `npm run test:integration` (18 testes) continua passando + +## 5. Solução proposta + +### 5.1 `src/cli.ts` — Separação do spawn Windows + +O único trecho alterado é a construção do `child` dentro de `runCli`: + +```typescript +const child = shell + ? (process.platform === 'win32' + ? cp.spawn('cmd.exe', ['/d', '/s', '/c', [file, ...args].map(quoteArg).join(' ')], + { cwd, shell: false, windowsHide: true }) + : cp.spawn([file, ...args].map(quoteArg).join(' '), + { cwd, shell: true, windowsHide: true })) + : cp.spawn(file, args, { cwd, shell: false, windowsHide: true }); +``` + +**Justificativa**: No Unix, `spawn(string, { shell: true })` usa `/bin/sh -c`, +que não adiciona wrap extra — a string é passada literalmente. No Windows, o +Node.js adiciona `"..."` ao redor para o `cmd.exe`, introduzindo a camada +extra. Chamar `cmd.exe` diretamente elimina essa diferença. + +### 5.2 `src/runner.ts` — Log de args + diagnóstico + +- **Log de args**: inserir entre `buildInvocation` e `runCli`, usando + `run.appendOutput` +- **Diagnóstico `applyCoverage`**: mostra caminho esperado, lista de arquivos + no dir temp, sugestão do modo `java` + +### 5.3 `src/test/unit/cli.test.ts` — Blindagem de `quoteArg` + +Novos testes unitários (função pura, sem dependência de SO): + +``` +quoteArg: caminho Windows sem espaco nao e alterado +quoteArg: caminho Windows com espaco e citado +quoteArg: regex de coverageSourceArgs e citado pelo $ +quoteArg: type_mapping com espacos e barra e citado +``` + +### 5.4 `src/test/unit/runner.test.ts` — Diagnóstico applyCoverage + +Teste que cria diretório temp vazio, chama `applyCoverage` para arquivo +inexistente e verifica: +- `appendOutput` contém `[cobertura] relatório não gerado` +- `appendOutput` contém o caminho esperado no formato `esperado em:` +- `appendOutput` contém `(vazio)` como lista de arquivos + +## 6. Configuração + +Nenhuma. Sem novas settings, comandos ou menus. + +## 7. Plano de testes + +- **Unitários**: + - `cli.test.ts`: 4 novos testes de `quoteArg` (RF4) + - `runner.test.ts`: 1 novo teste de `applyCoverage` diagnóstico (RF5) +- **Integração**: + - Manter os 18 testes existentes; não adicionar novos (Windows cross-platform + inviável no CI Linux) +- **Validação manual**: + - Windows 10/11 com `invocation: launcher`: rodar `runFileCoverage` e + verificar se cobertura aparece + - Mesmo cenário com `invocation: java`: verificar que não houve regressão + - Linux/macOS: rodar `runFileCoverage` e verificar que cobertura funciona + (não quebrou) + +## 8. Riscos e mitigação + +| Risco | Mitigação | +|---|---| +| `cmd.exe /d /s /c` sem aspas externas pode interpretar `&` ou `\|` na connection string como comando | A connection string já está no args array e o `quoteArg` cita qualquer arg com metacaracteres; `cmd.exe` trata aspas internas como toggle | +| Mudança no `runCli` afeta `listReporters`/`getCliInfo` que também usam `shell: true` | Testado — ambos passam nos testes de integração (modo launcher no Linux e java) | +| Teste `applyCoverage` depende de `fs.mkdtempSync` — pode colidir com outros testes | Usa `os.tmpdir()` isolado, limpeza no `finally`, igual aos testes existentes em `runner.test.ts` | + +## 9. Rollout + +- **Release alvo**: patch 0.7.2 +- **Estratégia**: o código Windows é condicional (`process.platform === 'win32'`), + então Unix não é afetado; deploy direto +- **CHANGELOG.md**: `Fix: relatório de cobertura não gerado no Windows com modo launcher` + +## 10. Critérios de aceite + +- `npm test` — 129 unitários passam + 5 novos = 134 +- `npm run lint` — sem novos warnings +- `npm run test:integration` — 18 testes passam (Linux) +- No Windows nativo, `runFileCoverage` com `launcher` gera cobertura +- No Windows nativo, `runFileCoverage` com `java` continua funcionando +- No Linux, `runFileCoverage` com `launcher` continua funcionando (sem regressão) + +## 11. Questões em aberto + +- Confirmar se o `.bat` do utPLSQL-cli no Windows usa `%*` (forward de todos os + argumentos). Se usar `%1`–`%9`, args de cobertura que vêm depois podem ser + truncados — mas isso é externo à extensão. diff --git a/docs/prd/proposed/prd-36-reporter-parse-fix.md b/docs/prd/proposed/prd-36-reporter-parse-fix.md new file mode 100644 index 0000000..e5f38fe --- /dev/null +++ b/docs/prd/proposed/prd-36-reporter-parse-fix.md @@ -0,0 +1,177 @@ +# PRD-36 — Correção do parse de `reporters` com descrições + flag `coverageEnabled` + +| Campo | Valor | +|---|---| +| Status | Proposto | +| Autor | Gil Cleber Barboza | +| Data | 2026-07-21 | +| Componente | Extensão `paneb.vscode-utplsql` | +| Versão alvo | 0.7.2 | +| Arquivos afetados | `src/cliReporters.ts`, `src/runner.ts`, `src/test/unit/cliReporters.test.ts` | + +## 1. Resumo + +O `parseReportersOutput` mantém a linha inteira do output do CLI (ex: +`UT_COVERAGE_COBERTURA_REPORTER - desc`), mas a comparação em `runner.ts` +espera o nome exato. Com o utPLSQL >= 3.2.x, o comando `reporters` inclui +descrições após o nome, fazendo a comparação falhar silenciosamente. A +extensão conclui que o reporter não existe e desabilita cobertura. + +Adicionalmente, a flag `coverage` em `runner.ts` não é atualizada quando o +reporter não é encontrado, fazendo `applyCoverage` rodar desnecessariamente e +emitir mensagem de diagnóstico enganosa. + +## 2. Contexto e problema + +### Causa raiz + +O utPLSQL 3.2.x formata a saída de `reporters` com descrições: + +``` +Core reporters: +UT_DOCUMENTATION_REPORTER - Plain-text documentation reporter +UT_JUNIT_REPORTER - JUnit-compatible reporter +UT_COVERAGE_COBERTURA_REPORTER - Cobertura XML report + +Extension reporters: +UT_COVERAGE_HTML_REPORTER - Coverage HTML report +``` + +`parseReportersOutput` (`src/cliReporters.ts:10-14`) faz apenas `.map(l => l.trim())`, +preservando a linha completa (`UT_COVERAGE_COBERTURA_REPORTER - Cobertura XML report`). + +A comparação em `runner.ts:110` é exata: +```typescript +!reporters.some((r) => r.toUpperCase() === 'UT_COVERAGE_COBERTURA_REPORTER') +``` + +A string `UT_COVERAGE_COBERTURA_REPORTER - Cobertura XML report` `.toUpperCase()` +não bate com `UT_COVERAGE_COBERTURA_REPORTER` → cobertura desabilitada. + +### Efeito colateral + +Como a flag `coverage` não é atualizada, `applyCoverage` (linha 177) é chamado +de qualquer forma, emitindo o diagnóstico falso sobre `GRANT EXECUTE ON SYS.DBMS_PROFILER`. + +## 3. Objetivos / Não-objetivos + +**Objetivos** +- Corrigir `parseReportersOutput` para extrair apenas o identificador do reporter +- Adicionar flag `coverageEnabled` local em `executeRun` para impedir `applyCoverage` quando reporter não disponível +- Simplificar diagnóstico do `applyCoverage` (remover sugestão de cmd.exe) +- Adicionar testes para linhas com descrição após o nome + +**Não-objetivos** +- Alterar `listReporters` (usa `parseReportersOutput` internamente) +- Alterar `buildInvocation` +- Suportar seções hierárquicas no output (Core/Extension reporters) — ficam como tokens extras inofensivos +- Alterar o formato de output do utPLSQL CLI + +## 4. Requisitos + +### RF1 — `parseReportersOutput` extrai apenas identificador + +Usar regex `^([A-Za-z0-9_]+)` no lugar de `l.trim()`: + +```typescript +export function parseReportersOutput(stdout: string): string[] { + return stdout + .split(/\r?\n/) + .map((l) => { + const m = l.trim().match(/^([A-Za-z0-9_]+)/); + return m ? m[1] : ''; + }) + .filter((l) => l.length > 0 && !l.startsWith('#') && !l.startsWith('[')); +} +``` + +| Entrada | Token extraído | +|---|---| +| `UT_COVERAGE_COBERTURA_REPORTER` | `UT_COVERAGE_COBERTURA_REPORTER` | +| `UT_COVERAGE_COBERTURA_REPORTER - desc` | `UT_COVERAGE_COBERTURA_REPORTER` | +| `UT_REPORTER:desc` | `UT_REPORTER` | +| ` UT_REPORTER ` | `UT_REPORTER` | +| `[section]` | `` → filtrado | +| `Core reporters:` | `Core` → inofensivo | + +### RF2 — Flag `coverageEnabled` em `executeRun` + +```typescript +let coverageEnabled = coverage; +if (coverage) { + const reporters = await listReporters(cfg, connection); + if ('error' in reporters) { + coverageEnabled = false; + // ... + } else if (!reporters.some(...)) { + coverageEnabled = false; + // ... + } +} +// ... +if (coverageEnabled) { + applyCoverage(coveragePath, root, cfg.sourcePath, run, state, folders); +} +``` + +### RF3 — Simplificar diagnóstico `applyCoverage` + +Remover sugestão de `cmd.exe` e modo `java` (a causa raiz não é o shell). + +### RF4 — Testes novos em `cliReporters.test.ts` + +| Teste | Entrada | Esperado | +|---|---|---| +| Nome com descrição após espaço e `-` | `UT_A - desc` | `['UT_A']` | +| Nome com descrição após espaço | `UT_A desc` | `['UT_A']` | +| Múltiplos reporters com descrições | `UT_A - x\nUT_B - y` | `['UT_A', 'UT_B']` | + +## 5. Solução proposta + +### 5.1 `src/cliReporters.ts` + +Única alteração: a linha 13 — trocar `.map((l) => l.trim())` por +`.map((l) => { const m = l.trim().match(/^([A-Za-z0-9_]+)/); return m ? m[1] : ''; })`. + +### 5.2 `src/runner.ts` + +Duas alterações: +1. **Linha 105**: introduzir `let coverageEnabled = coverage;` e setar para `false` nos branches onde o reporter não está disponível +2. **Linha 177**: trocar `if (coverage)` por `if (coverageEnabled)` +3. **Linha 298-304**: remover sugestão de `cmd.exe`/`java` + +## 6. Configuração + +Nenhuma. + +## 7. Plano de testes + +- **Unitários**: + - `cliReporters.test.ts`: 3 novos testes de parse com descrições (RF4) + - 6 testes existentes continuam passando +- **Integração**: 18 testes existentes continuam passando +- **Validação manual**: Windows/Linux com utPLSQL 3.2.x → `runFileCoverage` gera cobertura + +## 8. Riscos e mitigação + +| Risco | Mitigação | +|---|---| +| Nomes de reporter com `-` (ex: `MY-REPORTER`) | Regex captura só `MY`. Reporters oficiais usam `_`. | +| Section headers como `Core reporters:` viram `Core` | Inofensivo — nenhum reporter oficial chama `Core` | + +## 9. Rollout + +- Release 0.7.2 (patch, agrupado com PRD-35) +- CHANGELOG: `Fix: parse de reporters com descrições no utPLSQL 3.2.x` + +## 10. Critérios de aceite + +- `npm test` — 134 + 3 = 137 unitários passam +- `npm run lint` — sem novos warnings +- `npm run test:integration` — 18 testes passam +- Com utPLSQL 3.2.x, cobertura funciona +- Sem reporter de cobertura, não aparece diagnóstico falso + +## 11. Questões em aberto + +- Confirmar formato exato da saída de `utplsql reporters` no 3.2.2. diff --git a/docs/wiki/FAQ.md b/docs/wiki/FAQ.md index 04eaf08..f1a2757 100644 --- a/docs/wiki/FAQ.md +++ b/docs/wiki/FAQ.md @@ -159,6 +159,6 @@ publicação no Marketplace é feita **exclusivamente** via GitHub release ```bash npm run package -# gera: vscode-utplsql-0.7.1.vsix -code --install-extension vscode-utplsql-0.7.1.vsix +# gera: vscode-utplsql-0.7.2.vsix +code --install-extension vscode-utplsql-0.7.2.vsix ``` diff --git "a/docs/wiki/Instala\303\247\303\243o-e-requisitos.md" "b/docs/wiki/Instala\303\247\303\243o-e-requisitos.md" index 2e6e3ae..2476121 100644 --- "a/docs/wiki/Instala\303\247\303\243o-e-requisitos.md" +++ "b/docs/wiki/Instala\303\247\303\243o-e-requisitos.md" @@ -18,7 +18,7 @@ e instale: **Linha de comando:** ```bash -code --install-extension vscode-utplsql-0.7.1.vsix +code --install-extension vscode-utplsql-0.7.2.vsix ``` **Interface:** Painel de Extensões (`Ctrl+Shift+X`) → `...` (canto superior direito) diff --git a/package-lock.json b/package-lock.json index e8cd37c..c9bd199 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-utplsql", - "version": "0.7.1", + "version": "0.7.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vscode-utplsql", - "version": "0.7.1", + "version": "0.7.2", "license": "MIT", "dependencies": { "fast-xml-parser": "^4.4.1" diff --git a/package.json b/package.json index 2ff35bc..ed34287 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "vscode-utplsql", "displayName": "utPLSQL Test Runner", "description": "Roda testes utPLSQL pelo Test Explorer do VSCode, com menu de contexto e cobertura visual (estilo pytest).", - "version": "0.7.1", + "version": "0.7.2", "publisher": "paneb", "author": "Gil Cleber Barboza", "license": "MIT", diff --git a/src/cli.ts b/src/cli.ts index 76be724..901fede 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -33,7 +33,13 @@ export function runCli( // (necessário com shell). shell=false (java direto): passa o array — sem cmd, // sem quoting, metacaracteres de regex passam literais. const child = shell - ? cp.spawn([file, ...args].map(quoteArg).join(' '), { cwd, shell: true, windowsHide: true }) + ? process.platform === 'win32' + ? cp.spawn('cmd.exe', ['/d', '/s', '/c', [file, ...args].map(quoteArg).join(' ')], { + cwd, + shell: false, + windowsHide: true, + }) + : cp.spawn([file, ...args].map(quoteArg).join(' '), { cwd, shell: true, windowsHide: true }) : cp.spawn(file, args, { cwd, shell: false, windowsHide: true }); let stdout = ''; diff --git a/src/cliReporters.ts b/src/cliReporters.ts index fa8b754..0ce1184 100644 --- a/src/cliReporters.ts +++ b/src/cliReporters.ts @@ -10,7 +10,10 @@ const dummyToken: vscode.CancellationToken = { export function parseReportersOutput(stdout: string): string[] { return stdout .split(/\r?\n/) - .map((l) => l.trim()) + .map((l) => { + const m = l.match(/^([A-Za-z0-9_]+)/); + return m ? m[1] : ''; + }) .filter((l) => l.length > 0 && !l.startsWith('#') && !l.startsWith('[')); } diff --git a/src/runner.ts b/src/runner.ts index 0b4f319..a910dc9 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -102,12 +102,15 @@ export async function executeRun( args.push(`-f=${extraReporter}`); } + let coverageEnabled = coverage; if (coverage) { const reporters = await listReporters(cfg, connection); if ('error' in reporters) { + coverageEnabled = false; run.appendOutput(`[aviso] Não foi possível listar reporters: ${reporters.error}\r\n`); run.appendOutput('[aviso] Continuando sem cobertura.\r\n'); } else if (!reporters.some((r) => r.toUpperCase() === 'UT_COVERAGE_COBERTURA_REPORTER')) { + coverageEnabled = false; run.appendOutput( '\r\n[aviso] Reporter UT_COVERAGE_COBERTURA_REPORTER não disponível no banco.\r\n' + 'Cobertura desabilitada. Verifique se o pacote utPLSQL está atualizado.\r\n', @@ -162,6 +165,9 @@ export async function executeRun( return; } + const safeArgs = inv.args.map((a) => (a === connection ? '***' : a.replace(connection, '***'))); + run.appendOutput(`[debug] CLI: ${inv.file} ${safeArgs.join(' ')}\r\n`); + const result = await runCli(inv.file, inv.args, inv.shell, root, token, (chunk) => { run.appendOutput(chunk.replace(/\r?\n/g, '\r\n')); }); @@ -171,7 +177,7 @@ export async function executeRun( } applyResults(junitPath, leafTests, run, state); - if (coverage) { + if (coverageEnabled) { applyCoverage(coveragePath, root, cfg.sourcePath, run, state, folders); } @@ -274,7 +280,7 @@ export function lastSegment(classname: string): string { return parts.length ? parts[parts.length - 1] : classname; } -function applyCoverage( +export function applyCoverage( coveragePath: string, _root: string, sourcePath: string, @@ -284,8 +290,19 @@ function applyCoverage( ): void { state.clearCoverage(); if (!fs.existsSync(coveragePath)) { + const tmpDir = path.dirname(coveragePath); + const siblingFiles = (() => { + try { + return fs.readdirSync(tmpDir).join(', ') || '(vazio)'; + } catch { + return '(diretório não encontrado)'; + } + })(); run.appendOutput( - '\r\n[cobertura] relatório não gerado — verifique o GRANT EXECUTE ON SYS.DBMS_PROFILER.\r\n', + `\r\n[cobertura] relatório não gerado.\r\n` + + ` esperado em: ${coveragePath}\r\n` + + ` arquivos em ${tmpDir}: ${siblingFiles}\r\n` + + ` verifique o GRANT EXECUTE ON SYS.DBMS_PROFILER.\r\n`, ); return; } diff --git a/src/test/unit/cli.test.ts b/src/test/unit/cli.test.ts index 6918364..106007a 100644 --- a/src/test/unit/cli.test.ts +++ b/src/test/unit/cli.test.ts @@ -42,6 +42,28 @@ test('quoteArg: trailing whitespace', () => { assert.strictEqual(quoteArg('foo '), '"foo "'); }); +test('quoteArg: caminho Windows sem espaco nao e alterado', () => { + assert.strictEqual(quoteArg('-o=C:\\Temp\\cov.xml'), '-o=C:\\Temp\\cov.xml'); +}); + +test('quoteArg: caminho Windows com espaco e citado', () => { + assert.strictEqual(quoteArg('-o=C:\\My Docs\\cov.xml'), '"-o=C:\\My Docs\\cov.xml"'); +}); + +test('quoteArg: regex de coverageSourceArgs e citado pelo $', () => { + assert.strictEqual( + quoteArg('-regex_expression=.*[/\\\\](\\w+)\\.sql$'), + '"-regex_expression=.*[/\\\\](\\w+)\\.sql$"', + ); +}); + +test('quoteArg: type_mapping com espacos e barra e citado', () => { + assert.strictEqual( + quoteArg('-type_mapping=p=PACKAGE BODY/f=FUNCTION'), + '"-type_mapping=p=PACKAGE BODY/f=FUNCTION"', + ); +}); + test('runCli: executa echo com shell e retorna stdout', async () => { const result = await runCli('echo', ['hello', 'world'], true, tmpCwd, neverCancel); assert.strictEqual(result.code, 0); diff --git a/src/test/unit/cliReporters.test.ts b/src/test/unit/cliReporters.test.ts index d37e280..6a35b61 100644 --- a/src/test/unit/cliReporters.test.ts +++ b/src/test/unit/cliReporters.test.ts @@ -45,10 +45,14 @@ test('parseReportersOutput: CRLF é tratado', () => { assert.deepStrictEqual(reporters, ['UT_DOCUMENTATION_REPORTER', 'UT_JUNIT_REPORTER']); }); -test('parseReportersOutput: linhas com espaços extras são trimadas', () => { - const stdout = ' UT_DOCUMENTATION_REPORTER \n UT_JUNIT_REPORTER '; +test('parseReportersOutput: reporter com sufixo dois-pontos (formato 3.2.x)', () => { + const stdout = 'UT_DOCUMENTATION_REPORTER:\nUT_JUNIT_REPORTER:\nUT_COVERAGE_COBERTURA_REPORTER:'; const reporters = parseReportersOutput(stdout); - assert.deepStrictEqual(reporters, ['UT_DOCUMENTATION_REPORTER', 'UT_JUNIT_REPORTER']); + assert.deepStrictEqual(reporters, [ + 'UT_DOCUMENTATION_REPORTER', + 'UT_JUNIT_REPORTER', + 'UT_COVERAGE_COBERTURA_REPORTER', + ]); }); test('parseReportersOutput: linha só com espaço não entra', () => { @@ -57,6 +61,45 @@ test('parseReportersOutput: linha só com espaço não entra', () => { assert.deepStrictEqual(reporters, ['UT_DOCUMENTATION_REPORTER', 'UT_JUNIT_REPORTER']); }); +test('parseReportersOutput: descricao apos espaco e dash e ignorada', () => { + const stdout = 'UT_A - desc\nUT_B - outra desc'; + const reporters = parseReportersOutput(stdout); + assert.deepStrictEqual(reporters, ['UT_A', 'UT_B']); +}); + +test('parseReportersOutput: descricao apos espaco e ignorada', () => { + const stdout = 'UT_A desc\nUT_B outra desc'; + const reporters = parseReportersOutput(stdout); + assert.deepStrictEqual(reporters, ['UT_A', 'UT_B']); +}); + +test('parseReportersOutput: descricao apos dois-pontos e ignorada', () => { + const stdout = 'UT_COVERAGE_COBERTURA_REPORTER:Cobertura XML\nUT_JUNIT_REPORTER'; + const reporters = parseReportersOutput(stdout); + assert.deepStrictEqual(reporters, ['UT_COVERAGE_COBERTURA_REPORTER', 'UT_JUNIT_REPORTER']); +}); + +test('parseReportersOutput: formato 3.2.x com descricoes indentadas', () => { + const stdout = [ + 'UT_DOCUMENTATION_REPORTER:', + ' A textual pretty-print of unit test results.', + '', + 'UT_JUNIT_REPORTER:', + ' Provides outcomes in JUnit format.', + ' Based on specification v4.', + '', + 'UT_COVERAGE_COBERTURA_REPORTER:', + ' Generates a Cobertura coverage report.', + ' Designed for Jenkins and TFS.', + ].join('\n'); + const reporters = parseReportersOutput(stdout); + assert.deepStrictEqual(reporters, [ + 'UT_DOCUMENTATION_REPORTER', + 'UT_JUNIT_REPORTER', + 'UT_COVERAGE_COBERTURA_REPORTER', + ]); +}); + test('listReporters: retorna lista quando CLI executa com sucesso', async () => { mock.method(invocation, 'buildInvocation', () => ({ file: 'echo', diff --git a/src/test/unit/runner.test.ts b/src/test/unit/runner.test.ts index 9a6242f..2ec8100 100644 --- a/src/test/unit/runner.test.ts +++ b/src/test/unit/runner.test.ts @@ -4,7 +4,7 @@ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { test } from 'node:test'; -import { applyResults, findByNameOnly, lastSegment } from '../../runner'; +import { applyCoverage, applyResults, findByNameOnly, lastSegment } from '../../runner'; import type { ItemMeta } from '../../types'; function makeState() { @@ -413,3 +413,43 @@ test('applyResults: arquivo inexistente marca todos como erro', () => { applyResults('/caminho/inexistente.xml', leafTests, run as any, state as any); assert.strictEqual(errored.length, 1); }); + +test('applyCoverage: arquivo ausente gera diagnostico com caminho', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'runner-cov-test-')); + const coveragePath = path.join(tmpDir, 'coverage.xml'); + const output: string[] = []; + + const run = { + passed: () => {}, + failed: () => {}, + skipped: () => {}, + errored: () => {}, + appendOutput: (s: string) => output.push(s), + enqueued: () => {}, + started: () => {}, + addCoverage: () => {}, + end: () => {}, + }; + + const state = { + clearCoverage: () => {}, + setCoverage: () => {}, + getMeta: () => undefined, + setMeta: () => {}, + getCoverage: () => [], + } as any; + + try { + applyCoverage(coveragePath, '/root', 'install', run as any, state, []); + const all = output.join(''); + assert.match(all, /\[cobertura\] relatório não gerado/); + assert.match(all, /esperado em:/); + assert.match(all, /\(vazio\)/); + } finally { + try { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } catch { + /* ignore */ + } + } +});