Repository navigation
Técnicas de teste além do exemplo: regras, StrictMode, cobertura, propriedade, fuzz, máquina de estados, mutação, perfil de base e lint de API - #9
Merged
Conversation
…s executáveis ArchitectureRulesTest lê o código como texto (sem dependência nova) e falha com o motivo quando alguém — pessoa ou agente — esquece uma convenção: collectAsState direto numa tela (no lugar de collectAsStateOnMain), runBlocking fora da lista decidida (cada um com o porquê), OverlayService subindo ou descendo sem o start/stop do companion, teste que grava de verdade sem parar o serviço no @after, espera às cegas pelo "🎙" e Regex de teste instrumentado com "}" solto (o ICU do Android recusa). Conferido que cada regra falha com uma violação provisória e que "\\}" e "{2}" não são acusados.
…de citar o que elas proíbem
… para os testes StrictModeGuard acusa disco e rede feitos NA thread principal e recursos que ninguém fecha; só registra (log com a 1ª linha do nosso código + lista em memória), nunca derruba o app. Ligado no App.onCreate só com BuildConfig.DEBUG. O `runBlocking` esperando outra thread não aparece aqui (é espera, não E/S) — esse fica com o ArchitectureRulesTest. Na 1ª medição, ao abrir o teclado e o app: o cofre cifrado aberto e gravado na principal, o caminho do DataStore a cada repositório criado, a varredura da pasta de modelos e as preferências do teclado.
StrictModeKeyboardTest abre o teclado e falha com disco/rede na thread principal vindo do código do teclado ou do que ele usa (o caminho que o Android cronometra: 1 s para o 1º quadro). Começou vermelho com cinco, todas corrigidas: - settingsFlow lia o cofre cifrado (abre o EncryptedSharedPreferences: disco + Keystore) no map, na thread de quem coleta — o teclado coleta na principal. flowOn(Dispatchers.IO). - SettingsRepository montava o caminho do DataStore por filesDir (confere e cria a pasta a cada repositório): agora por applicationInfo.dataDir, só texto; a pasta segue criada pelo produceFile, na thread do DataStore. - O onStartInputView criava o LocalTranscriber, que varre a pasta de modelos, mesmo com voz na nuvem: syncWith só cria com a origem Local (com outra, só solta o que estiver preso). - As preferências do teclado (expandido) são abertas no IO quando o serviço nasce. A lista de conhecidas do teste fica vazia. Unidade: 1.431/1.431.
Cobertura com o JaCoCo do próprio AGP (enableUnitTestCoverage, só com -Pcoverage: o dia a dia não paga a instrumentação); scripts/coverage-summary.py resume por pacote, do menos coberto ao mais, no resumo do job. Mapa, não meta: hoje 36,6% das linhas — as telas, o teclado e os serviços aparecem baixos porque são cobertos pelos instrumentados; os buracos de lógica pura (overlay 28%, data 28%, ime.bars 19%) são os alvos dos testes de propriedade e de máquina de estados. No job de unitários, os APKs são montados e publicados ANTES dos testes: os jobs do emulador esperam por eles, e no mesmo passo os testes os empurravam ~30 s (medido na rodada #35). Testes e cobertura vêm depois, fora do caminho crítico.
…das barras, tempo e orçamento da orbe, repetição Em vez de três exemplos escritos à mão, milhares de entradas geradas conferem uma regra que vale para todas: as barras e grades que a pessoa monta voltam do DataStore iguais (2.000 configurações cada, respeitando o que o codec normaliza de propósito); o frame da orbe sempre existe (tempo negativo, enorme, NaN, infinito); os frames "à frente" são distintos, dentro do pacote e sem o de agora; o orçamento do cache fica entre o básico e o teto e nunca encolhe quando a orbe pede mais; a repetição do backspace só acelera, até o mínimo. kotest-property 6.0.5 (Kotlin 2.2.0), no JUnit4 de sempre.
…chado por teste de propriedade O GeometryPropertyTest gerou fileiras com pesos e larguras quaisquer e achou: somando as larguras em Float, a última tecla de uma fileira de 931 px acabava em 930,99994 — uma fresta na borda direita onde o toque não caía em tecla nenhuma. Agora cada borda sai do peso acumulado (Double) e a última é a própria largura; vizinhas seguem encostadas (mesma conta). As propriedades da grade e das barras: teclas encostadas, na ordem e cobrindo a largura; todo ponto da fileira cai numa tecla; o centro acerta a tecla e a folga de escorregar a mantém; botões da barra encostados, alvo mínimo, rolagem nunca negativa.
…barras, credenciais e argumentos do agente Com os geradores da kotest-property (o Jazzer pede JUnit5): JSON gerado, mutado (cortado, com caracteres trocados) e com aninhamento fundo contra decodeBars/decodeGrids, CredentialCodec.decode, SkinPackManifest.parse e os argumentos de ferramenta que o modelo manda (recusar, nunca lançar; aceito só com o campo obrigatório); e .zip hostil contra o SkinPackReader — bytes aleatórios, pacote válido corrompido, nomes com "..", caminho absoluto, controle e unicode (nada não canônico chega ao disco) e SVG com DOCTYPE/script/entidade. ~30 mil entradas, ~4 s; o prazo de cada teste pega regex com tempo explosivo. Nada escapou: os leitores aguentam. Conferido que uma falha plantada no decodeBars é achada, com a entrada.
…de estado Um RESUME que chegava em SENDING (arrasto da orbe decidido em PAUSED e solto depois de o mic do teclado ou a notificação mandarem enviar) ligava o gravador sem a máquina sair de SENDING; a transcrição terminava e o app ficava em repouso com o microfone aberto. PAUSE agora só vale em RECORDING e RESUME só em PAUSED. Do mesmo jeito, START em RECORDING e STOP em SENDING devolviam o mesmo estado e reaplicá-lo religava o gravador / reenviava o áudio: onHold, onRelease, START e STOP passam pela mesma guarda que o onTap já tinha (só age se o estado mudou).
…tórias 3000 sequências de até 30 comandos (tocar, segurar, as intenções, cancelar, desfazer, concluir, trocar de campo, as saídas do limite) com o relógio virtual andando entre eles, transcrição lenta ou com erro, limite de tempo e transcrição parcial ligados ou não. Depois de cada passo: o microfone capta se e só se o estado é RECORDING, o gravador nunca é ligado duas vezes, a tela mostra o estado real e o aviso de desfazer fica equilibrado; no fim, sempre volta ao repouso. Achou os dois defeitos do commit anterior.
ViewTreeObserver.registerFrameCommitCallback é da API 29 e o minSdk é 24: do Android 7 ao 9, abrir o teclado lançaria NoSuchMethodError no onStartInputView. Veio com o diagnóstico de cbebd2b; abaixo do 10 o teclado abre sem ele.
Tarefa :app:pitest pela linha de comando do PIT 1.30.0 (o plugin Gradle para Android não acompanha o AGP 9): reusa o classpath do testDebugUnitTest e só muta o jar de classes do app. Alvos padrão: núcleo da gravação, geometria/teclas/dicionário do teclado, atalhos, pacote de skin, agente e OAuth; -PpitTargets troca. scripts/mutation-summary.py resume por classe e lista os sobreviventes; .github/workflows/mutation.yml roda toda segunda, por push em ci/mutacao… ou à mão. Fora do gate de PR. 1ª medição local: 2673 mutantes em 7 min, força 81% (1890 de 2327 cobertos).
Regras de arquitetura, StrictMode, cobertura, propriedade, fuzz, máquina de estados e mutação: onde está cada uma, o que pega e quando roda, mais as armadilhas (kotest no JUnit4, geradores que respeitam as normalizações, PIT sem o plugin Gradle).
Um shard verde (88 testes, 36 telas com 0 px de diferença) ficou 35 min parado depois do "Terminate Emulator" do android-emulator-runner, até o timeout do passo; nos outros shards essa etapa leva 1 s. A action sobe o emulador com a saída ligada a ela e só termina quando nenhum processo do emulador segura essa saída — o QEMU, o netsimd e o crashpad são processos à parte. O script agora manda emu kill, espera até 30 s e mata à força o que sobrou na pasta emulator/ do SDK, deixando no log quem foi.
O PIT mostrou que nenhum teste conferia o tempo que a orbe, o teclado e a barra mostram (apagar publishTime/timeFreeze passava). Agora, depois de cada passo: o cronômetro só zera ao enviar ou voltar ao repouso, nunca anda para trás e nunca passa do tempo virtual passado em RECORDING. 12 dos 15 mutantes do cronômetro passam a ser detectados (força do coordenador de 70% para 74%).
KeyFlow.onCreateInputView e KeyFlow.onStartInputView (síncronas) e KeyFlow.keyboardOpen (assíncrona, Android 10+), do 1º passo do ciclo de mostrar ao 1º quadro entregue. Aparecem num trace do Perfetto e são o que o benchmark de abertura mede. A recriação ao vivo da view (tema, barras, skin) não abre seção.
Módulo :baselineprofile (androidx.baselineprofile 1.5.0 com AGP 9.2): o gerador percorre o app a frio e o teclado a frio sobre outro app (campo numa Activity do próprio APK de teste), digitando e fechando; OpeningBenchmarks mede app e teclado com e sem o perfil. O app ganha o profileinstaller e o perfil gerado (app/src/release/generated/baselineProfiles/, 16.764 regras, com o desenho do teclado, as barras e o dicionário). As variantes de benchmark são assinadas com a keystore de debug e levam x86_64: rodam só no emulador, com ANDROID_SERIAL. Medido no emulador API 34 (mediana sem → com perfil): onCreateInputView 22 → 8,5 ms, e 65 → 16 ms com a CPU limitada como o CI. A abertura inteira (~0,35 s rápido, ~1 s lento) e o app a frio não mudam além do ruído: o resto é sistema e renderização, não o nosso código.
No gate de 2026-10-10 a resposta do agente não veio em 10 s uma vez e passou na repetição, sem rastro; a esteira rodou a classe 208 vezes (metade com a CPU a 200%) sem falhar. Agora a falha conta quantas vezes o motor foi chamado (0 = o envio nem saiu), o que as ferramentas rodaram e os textos na tela — conferido forçando uma falha no emulador.
No gate verde de f8bcfb9 a linha de "processos vivos" saiu em 5 dos 6 shards, sempre com o vigia do próprio emulador (emulator -kill <pid> -sleep 20), que sai sozinho e não segura a action. Ele fica de fora: agora a linha só aparece no caso anormal (o que travou um shard por 35 min), e a espera cobre os três processos, não só o QEMU.
registerFrameCommitCallback é do Android 10 e o minSdk é 24. A galeria é só de debug e roda no emulador do gate (Android 14), mas era o último NewApi que o lint acusava: abaixo do 10 ela espera o passo de animação do quadro seguinte.
Chamada a API mais nova que o minSdk 24, sem conferir a versão, quebra em aparelho antigo e nenhum teste pega: o gate roda no Android 14. Foi assim que um registerFrameCommitCallback no caminho de abrir o teclado passou (corrigido em 3c330eb). O lint do app fica só com essa checagem (as outras nunca foram triadas) e falha o job; conferido tirando a guarda — acusa a linha e sai com erro.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Código novo que testa o código que já existe, de oito jeitos diferentes, mais o que cada um achou e foi corrigido. Tudo descrito em
.specs/codebase/TESTING.md(seção "Técnicas além do teste de exemplo").Técnicas
ArchitectureRulesTestStrictModeGuard(debug) +StrictModeKeyboardTest-Pcoverage+scripts/coverage-summary.py:app:lintDebug(sóNewApi)app/src/test/…/properties/(kotest-property)app/src/test/…/fuzz/OverlayCoordinatorStateMachineTest:app:pitest+.github/workflows/mutation.yml:baselineprofileDefeitos do app corrigidos
registerFrameCommitCallback(Android 10) sem conferir a versão no diagnóstico de abertura. O lint de API agora barra isso.CI
Medições
onCreateInputView22 → 8,5 ms; com a CPU limitada como o CI, 65 → 16 ms. A abertura inteira do teclado e o app a frio não mudam além do ruído.Em aberto
KeyflowPanelAgentTest#a_read_tool_runs_without_asking_and_its_card_says_donedeu instável uma vez no gate; a esteira não reproduziu em 208 voltas. O teste agora diz o que havia na tela se a espera estourar.