Download testes/conftest.py from AIDAON/aida-api: direct link, hf CLI and curl.
- Browser
- Download file 9.78 kB
-
https://huggingface.co/spaces/AIDAON/aida-api/resolve/main/testes/conftest.py
- Command line
-
hf download hf://spaces/AIDAON/aida-api/testes/conftest.py
-
curl -L -o conftest.py https://huggingface.co/spaces/AIDAON/aida-api/resolve/main/testes/conftest.py
9.78 kB
| """Fixtures compartilhadas da suíte do AIDA. | |
| Duas decisões guiam este arquivo: | |
| **Nada aqui carrega modelo.** Subir o pipeline real levaria dezenas de segundos | |
| por sessão e exigiria os `.joblib` treinados no disco — a suíte deixaria de | |
| rodar em qualquer máquina limpa e em CI. Onde o teste precisa de um pipeline, | |
| usamos um dublê com o mesmo contrato. | |
| **Nada aqui escreve fora do `tmp_path`.** Os módulos de cache e de artefatos | |
| resolvem seus diretórios na importação (`from ... import CACHE_ANALISES_DIR`), | |
| então a redireção acontece por `monkeypatch.setattr` no módulo já importado — | |
| trocar a variável em `aida.configuracao` depois do import não teria efeito. | |
| """ | |
| from __future__ import annotations | |
| import sys | |
| from pathlib import Path | |
| import pytest | |
| RAIZ = Path(__file__).resolve().parents[1] | |
| if str(RAIZ) not in sys.path: | |
| sys.path.insert(0, str(RAIZ)) | |
| # --------------------------------------------------------------------------- # | |
| # Imagens sintéticas | |
| # --------------------------------------------------------------------------- # | |
| def criar_imagem(tmp_path): | |
| """Gera um JPEG pequeno com conteúdo real (não uma cor sólida). | |
| Cor sólida quebraria os testes dos métodos forenses de um jeito enganoso: | |
| gradiente e ruído residual ficariam identicamente nulos, e a normalização | |
| por percentis não teria o que separar. | |
| """ | |
| import numpy as np | |
| from PIL import Image | |
| def _criar(nome: str = "amostra.jpg", lado: int = 96, semente: int = 7) -> Path: | |
| rng = np.random.default_rng(semente) | |
| eixo = np.linspace(0, 255, lado, dtype=np.float64) | |
| base = np.stack( | |
| [ | |
| np.tile(eixo, (lado, 1)), | |
| np.tile(eixo.reshape(-1, 1), (1, lado)), | |
| np.full((lado, lado), 128.0), | |
| ], | |
| axis=-1, | |
| ) | |
| base += rng.normal(0, 12, base.shape) | |
| arr = np.clip(base, 0, 255).astype("uint8") | |
| destino = tmp_path / nome | |
| Image.fromarray(arr).save(destino, quality=88) | |
| return destino | |
| return _criar | |
| # --------------------------------------------------------------------------- # | |
| # Cache isolado | |
| # --------------------------------------------------------------------------- # | |
| def cache(tmp_path, monkeypatch): | |
| """`aida.inferencia.cache_analise` apontando para um diretório descartável.""" | |
| from aida.inferencia import cache_analise | |
| pasta = tmp_path / "cache_analises" | |
| indice = pasta / "por_conteudo" | |
| pasta.mkdir(parents=True, exist_ok=True) | |
| indice.mkdir(parents=True, exist_ok=True) | |
| monkeypatch.setattr(cache_analise, "CACHE_ANALISES_DIR", pasta) | |
| monkeypatch.setattr(cache_analise, "CACHE_ANALISES_INDICE_DIR", indice) | |
| # O TTL vem do ambiente; um valor herdado da máquina do desenvolvedor | |
| # tornaria o teste de expiração não determinístico. | |
| monkeypatch.delenv("AIDA_CACHE_TTL_H", raising=False) | |
| return cache_analise | |
| # --------------------------------------------------------------------------- # | |
| # Payload de análise | |
| # --------------------------------------------------------------------------- # | |
| def payload_analise(): | |
| """Resposta do pipeline, no formato que `site_api/app.py` devolve. | |
| Os valores imitam um caso INCONCLUSIVO de propósito: é o estado que as | |
| integrações mais esquecem de tratar, e o que a interface tem mais chance de | |
| exibir errado. | |
| """ | |
| from aida.configuracao import CONFIG | |
| def _payload(id_analise: str = "ANALISE-0001", **sobrescritas): | |
| base = { | |
| "id_analise": id_analise, | |
| "nome_imagem": f"{id_analise}.jpg", | |
| "nome_imagem_original": "foto-do-usuario.jpg", | |
| "caminho_imagem": "/tmp/servidor/interno/foto.jpg", | |
| "data_hora": "2026-08-16 10:00:00", | |
| "duracao_s": 1.42, | |
| "resultado": "INCONCLUSIVO", | |
| "inconclusivo": True, | |
| "fora_de_dominio": False, | |
| "probabilidade_ia": 0.4123, | |
| "probabilidade_real": 0.5877, | |
| "probabilidade_ia_exibicao": 0.48, | |
| "probabilidade_real_exibicao": 0.52, | |
| "probabilidade_ia_conteudo": 0.44, | |
| "limiar": 0.4816, | |
| "limiar_exibicao": 0.5, | |
| "banda_inconclusiva": [0.30, 0.66], | |
| "banda_inconclusiva_exibicao": [0.32, 0.68], | |
| "confianca": "baixa", | |
| "motivos": ["a probabilidade caiu dentro da banda de abstenção"], | |
| "titulo": "Resultado inconclusivo", | |
| "explicacao": "Os módulos não convergiram.", | |
| "ressalva": "Este resultado é indício, não prova.", | |
| "limitacoes": ["Imagem pouco nítida."], | |
| "avisos_qualidade": ["Compressão alta."], | |
| "avisos_sistema": [], | |
| "scores_modulos": {"forense": 0.31, "clip": 0.55}, | |
| "fusao": {"metodo": "media", "calibrado": False, "desacordo": 0.24}, | |
| "qualidade": {"qualidade_score": 0.62, "qualidade_nitidez": 0.4}, | |
| "proveniencia": { | |
| "status": "NO_PROVENANCE_FOUND", | |
| "c2pa_presente": False, | |
| "declaracao_ia": False, | |
| "softwares": [], | |
| "evidencias": [], | |
| "atribuicao": {"gerador": "desconhecido"}, | |
| "metadados_presentes": {"exif": True, "xmp": False, "iptc": False, "campos": 50}, | |
| }, | |
| "atribuicao_gerador": {"gerador": "desconhecido", "confianca": 0.0}, | |
| "divergencia": None, | |
| "evidencias": { | |
| "arquivos": { | |
| "gradiente": f"/disco/servidor/{id_analise}_gradiente.png", | |
| "espectro_magnitude": f"/disco/servidor/{id_analise}_espectro_magnitude.png", | |
| }, | |
| "patches_suspeitos": [], | |
| "perfil_radial": [1.0, 0.8, 0.6], | |
| "perfil_angular": [0.2, 0.3], | |
| }, | |
| "metadados": { | |
| "arquivo": f"{id_analise}.jpg", | |
| "extensao": ".jpg", | |
| "formato_pil": "JPEG", | |
| "modo": "RGB", | |
| "bytes": 53333, | |
| "largura": 576, | |
| "altura": 1024, | |
| "pixels": 589824, | |
| "bits_por_pixel": 0.72, | |
| "jpeg_qualidade_estimada": 88.0, | |
| "progressivo": 0, | |
| "subamostragem": 2, | |
| "tem_exif": 0, | |
| "qtd_exif": 0, | |
| "tem_icc": 0, | |
| }, | |
| "hash_sha256": "a" * 64, | |
| "modelo_utilizado": "media", | |
| "modo_rapido": False, | |
| "versao_modelo": CONFIG.execucao.versao_modelo, | |
| "principais_metricas": {f"caracteristica_{i}": float(i) for i in range(853)}, | |
| } | |
| base.update(sobrescritas) | |
| return base | |
| return _payload | |
| # --------------------------------------------------------------------------- # | |
| # Aplicação Flask com pipeline dublê | |
| # --------------------------------------------------------------------------- # | |
| def cliente_api(tmp_path, monkeypatch, cache, payload_analise): | |
| """Cliente de teste da API, sem carregar modelo nenhum. | |
| `site_api/app.py` instancia o pipeline na importação. O dublê é injetado no | |
| módulo de origem **antes** do import, que é o único ponto em que a | |
| substituição ainda é vista pelo `from ... import` do app. | |
| """ | |
| import importlib | |
| from aida.inferencia import pipeline as modulo_pipeline | |
| class PipelineDuble: | |
| """Mesmo contrato que `site_api/app.py` consome do pipeline real.""" | |
| def __init__(self, *_args, **kwargs): | |
| self.modulos = type("Modulos", (), {"disponiveis": ("forense",)})() | |
| self.ensemble = None | |
| self.modo_rapido = bool(kwargs.get("modo_rapido")) | |
| self.usar_proveniencia = True | |
| self.dominio = None | |
| self.avisos = [] | |
| self.politica = type( | |
| "Politica", | |
| (), | |
| { | |
| "para_dict": staticmethod(lambda: {"limiar": 0.4816}), | |
| "versao_modelo": "duble-1", | |
| }, | |
| )() | |
| monkeypatch.setattr(modulo_pipeline, "PipelineAIDA", PipelineDuble) | |
| modulo_app = importlib.import_module("site_api.app") | |
| modulo_app = importlib.reload(modulo_app) | |
| # Depois do reload, o app tem as suas próprias referências às pastas e à | |
| # função de análise; é nele que a substituição precisa acontecer. | |
| explicabilidade = tmp_path / "explicabilidade" | |
| explicabilidade.mkdir(parents=True, exist_ok=True) | |
| monkeypatch.setattr(modulo_app, "EXPLICABILIDADE_DIR", explicabilidade) | |
| monkeypatch.setattr(modulo_app, "cache_analise", cache) | |
| analises_feitas = [] | |
| def analisar_duble(caminho_imagem, id_analise=None, **_kwargs): | |
| analises_feitas.append(id_analise) | |
| resposta = payload_analise(id_analise or "SEM-ID") | |
| # Os PNGs de evidência existem de verdade no diretório temporário: sem | |
| # isso, `GET /evidencia/...` responderia 404 e o teste de integração | |
| # passaria a medir só a montagem da URL. | |
| for nome in resposta["evidencias"]["arquivos"]: | |
| destino = explicabilidade / f"{id_analise}_{nome}.png" | |
| destino.write_bytes(b"\x89PNG\r\n\x1a\n" + b"0" * 32) | |
| resposta["evidencias"]["arquivos"][nome] = str(destino) | |
| return resposta | |
| monkeypatch.setattr(modulo_app, "analisar_imagem", analisar_duble) | |
| modulo_app.app.config["TESTING"] = True | |
| with modulo_app.app.test_client() as cliente: | |
| cliente.modulo = modulo_app | |
| cliente.analises_feitas = analises_feitas | |
| yield cliente | |