Skip to content

Corrige/Melhora PidProviderXML devido a um bug e uma melhoria packtools 4.16.11 - #1469

Merged
robertatakenaka merged 22 commits into
scieloorg:mainfrom
robertatakenaka:fix_retorno_pid_provider_register_pequena_mudanca
Sep 1, 2026
Merged

Corrige/Melhora PidProviderXML devido a um bug e uma melhoria packtools 4.16.11#1469
robertatakenaka merged 22 commits into
scieloorg:mainfrom
robertatakenaka:fix_retorno_pid_provider_register_pequena_mudanca

Conversation

@robertatakenaka

@robertatakenaka robertatakenaka commented Aug 29, 2026

Copy link
Copy Markdown
Member

O que esse PR faz?

Corrige, no próprio core, o efeito de 2 bugs existentes no packtools 4.16.11, sem precisar atualizar a versão da dependência:

  1. XMLWithPre.get_article_data() retorna uma chave partial_body que não deveria fazer parte desse dict — ela é resquício de um formato antigo e não deveria mais ser exposta ali.
  2. PidProviderXMLAdapter.get_data_to_compare() retorna a chave z_partial_body no lugar de body_fragment_fingerprint, que é o valor que de fato deveria ser usado na comparação de corpo do artigo.

Como não é possível alterar o pacote packtools neste momento, este PR resolve os dois problemas isolando-os em funções wrapper no core (fix_get_article_data() e fix_get_data_to_compare()), que corrigem o dado depois que ele sai do packtools, antes de ser usado no restante do pid_provider:

  • fix_get_article_data() remove a chave indevida partial_body do dict retornado por xml_with_pre.get_article_data() (bug 1).
  • fix_get_data_to_compare() sobrescreve z_partial_body no dict retornado por xml_adapter.get_data_to_compare() com o valor correto, xml_adapter.xml_with_pre.body_fragment_fingerprint (bug 2).
  • Atualiza QueryBuilderPidProviderXML para ler o hash legado (z_partial_body) diretamente de xml_adapter.xml_with_pre.z_partial_body (não mais de xml_adapter.data, que era afetado pelo bug 2), remove o uso do fingerprint do corpo inteiro (z_body/body_fingerprint, que não deveria ter sido usado em partial_body_query) e ajusta validate_input_data para checar a chave correta body_fragment no lugar de partial_body.
  • Propaga o mesmo tratamento em pid_provider/models.py, onde readable_data, o diff de conflito de pid v3 e as comparações de registro passam a usar as funções wrapper em vez de chamar os métodos do packtools diretamente.
  • Realinha test_query_params.py e test_select_record.py com o comportamento real do código (13 de 55 testes estavam falhando antes por descreverem um refactor não implementado).
  • Documenta funções e atributos que não tinham docstring (compare, compare_lists, compare_items, get_score, zero_to_none, fix_get_article_data, validate_input_data, pkg_name_list, article_data_query, get_article_data_query) e corrige comentários desatualizados no __init__ de QueryBuilderPidProviderXML e em partial_body_query, que ainda referenciavam um atributo (z_body/body_fingerprint) e uma origem de dado (xml_adapter.z_partial_body a partir de adapter.data) que não refletem mais o código atual.

Onde a revisão poderia começar?

pid_provider/query_params.py — em especial fix_get_data_to_compare() e fix_get_article_data() (os wrappers que corrigem os dois bugs do packtools) e QueryBuilderPidProviderXML.partial_body_query, que consome esses dados corrigidos. Em seguida, pid_provider/models.py para ver onde essas funções passam a ser chamadas no lugar dos métodos originais do packtools.

Como este poderia ser testado manualmente?

  1. Confirmar que a versão do packtools instalada no ambiente permanece 4.16.11 (não houve bump em requirements/setup — a correção é só no core).
  2. Rodar a suíte de testes do módulo: pytest pid_provider/tests/test_query_params.py pid_provider/tests/test_select_record.py (todos devem passar).
  3. Submeter (ou reprocessar) um XML já registrado anteriormente e confirmar que ele ainda é encontrado como duplicado (sem gerar um novo registro), verificando que readable_data não contém mais a chave partial_body.
  4. Conferir que dois artigos com o mesmo rótulo genérico de z_partial_body legado (ex.: "ARTIGO DE REVISÃO") mas corpos diferentes não colidem mais em uma mesma query de correspondência — evidência de que a comparação passou a usar body_fragment_fingerprint corretamente, e não o valor incorreto vindo do bug 2.

Algum cenário de contexto que queira dar?

Foram identificados 2 bugs no packtools 4.16.11:

  1. XMLWithPre.get_article_data() expõe uma chave partial_body que não deveria fazer parte desse dict.
  2. PidProviderXMLAdapter.get_data_to_compare() retorna z_partial_body no lugar do valor correto, body_fragment_fingerprint.

Corrigir isso no próprio pacote exigiria uma nova release do packtools e, potencialmente, uma atualização de versão no core — o que traria mudanças de escopo maior nos requirements e não é desejável agora. Por isso, optamos por contornar os dois bugs isoladamente no código do pid_provider, via funções wrapper (fix_get_article_data() e fix_get_data_to_compare()), mantendo o packtools fixado em 4.16.11. Isso resolve o problema imediato sem tocar em requirements, e essas correções podem ser removidas do core quando o packtools corrigir os bugs em uma versão futura.

Screenshots

Não aplicável.

Quais são os tickets relevantes?

#1470

Referências


Segurança da informação (NSI.04)

Seção obrigatória. Marque as opções aplicáveis e justifique quando necessário. Referência: NSI.04 - Norma de Desenvolvimento Seguro.

Este PR manipula dados sensíveis ou pessoais (LGPD)?

  • Sim — descreva os controles de proteção aplicados (criptografia, mascaramento, anonimização, etc.):
  • Não

Este PR altera autenticação, autorização, controle de acesso ou gerenciamento de sessão?

  • Sim — descreva o que mudou e por quê:
  • Não

Este PR introduz, atualiza ou remove dependências de terceiros?

  • Sim — as novas dependências foram verificadas no SBOM/Trivy sem vulnerabilidades críticas/altas em aberto?
    • Verificado e aprovado
    • Pendente / vulnerabilidade aceita com justificativa:
  • Não — nenhuma dependência é atualizada; o packtools permanece fixado em 4.16.11

Este PR foi validado pelo pipeline de segurança (SonarQube / Trivy)?

  • Sim — link do job:
  • Não aplicável a este PR (justifique):

Este PR concatena, monta ou executa comandos SQL, HTML ou JavaScript a partir de entrada externa?

  • Sim — confirme que há sanitização/parametrização (prepared statements, escaping, etc.):
  • Não

Este PR expõe novos endpoints, telas ou serviços?

  • Sim — HTTPS obrigatório está garantido e o acesso segue o princípio de menor privilégio?
  • Não

Algum segredo, senha, chave ou token está sendo adicionado ao código-fonte?

  • Não, nenhum segredo foi commitado
  • Sim (bloquear merge e corrigir antes de prosseguir)

…packtools

Adiciona fix_get_article_data() e fix_get_data_to_compare() como camadas de compatibilidade em torno de xml_with_pre.get_article_data() e xml_adapter.get_data_to_compare(), já que o formato retornado por esses métodos mudou entre versões do packtools (partial_body -> body_fragment; z_partial_body -> body_fragment_fingerprint).

fix_get_article_data() remove a chave legada 'partial_body' do dict retornado.
fix_get_data_to_compare() força a chave 'z_partial_body' a vir de xml_with_pre.body_fragment_fingerprint, independente do que get_data_to_compare() retornar.

Em QueryBuilderPidProviderXML:
- z_partial_body agora é lido diretamente de xml_adapter.xml_with_pre.z_partial_body (hash legado), não mais de xml_adapter.data.
- z_body (body_fingerprint, hash do corpo inteiro) deixou de ser usado — removido de __init__ e de partial_body_query.
- partial_body_query agora combina apenas z_partial_body (legado) e z_body_fragment (body_fragment_fingerprint), como set, caindo em isnull=True quando nenhum dos dois existe.
- validate_input_data passa a checar a chave 'body_fragment' (novo nome) em vez de 'partial_body'.
Substitui as chamadas diretas a xml_with_pre.get_article_data() e xml_adapter.get_data_to_compare() por fix_get_article_data() e fix_get_data_to_compare(), garantindo que os dados usados em readable_data, no diff de conflito de pid v3 e nas comparações de registro fiquem normalizados entre versões do packtools.

Também corrige get_registered_versions() para usar body_fragment_fingerprint (novo fingerprint) em vez do atributo antigo z_partial_body diretamente no dict de readable_data.
…al do código

O arquivo de testes anterior descrevia um refactor que não estava implementado no código-fonte atual, fazendo 13 de 55 testes falharem. Principais ajustes:

- make_xml_adapter() passa a configurar z_partial_body diretamente em xml_with_pre (não mais em adapter.data), e usa 'body_fragment' como chave do dict de get_article_data(), incluindo uma chave 'partial_body' só para exercitar o pop feito por fix_get_article_data().
- Remove body_fingerprint (corpo inteiro) das comparações de partial_body_query e article_data_query, já que esse fingerprint não é mais usado.
- Adiciona testes cobrindo validate_input_data com a chave 'body_fragment' e o caso de branco.
- Ajusta CompareTests para refletir que compare() usa input_data.get(label) e não pula labels ausentes, incluindo o caso de ZeroDivisionError quando registered_items é vazio.
…paração

get_best_match() agora recebe um dict de comparação que inclui a chave 'z_partial_body' (adicionada por fix_get_data_to_compare), então a asserção passa a usar ANY para esse valor em vez de comparar apenas por {'title': 'Foo'}.
Adiciona docstrings que faltavam (compare, compare_lists, compare_items, get_score, zero_to_none, fix_get_article_data) e corrige documentação desatualizada:

- __init__ de QueryBuilderPidProviderXML: o comentário ainda descrevia um atributo z_body/body_fingerprint (fingerprint do corpo INTEIRO do artigo) que não é mais atribuído no código atual; substituído por uma docstring listando os atributos realmente definidos (z_body_fragment, z_partial_body, adapter_data, xml_with_pre_data).
- partial_body_query: corrige a referência de onde vem o hash legado, que dizia vir de xml_adapter.z_partial_body mas na verdade é lido de xml_adapter.xml_with_pre.z_partial_body (via self.z_partial_body).
- validate_input_data, pkg_name_list, article_data_query e get_article_data_query passam a ter docstrings explicando o comportamento.
- Remove comentários de linha redundantes em pkg_name_list e identifier_queries que só repetiam o que o código já deixa claro, agora que a lógica está descrita na docstring do método.
Comment thread pid_provider/query_params.py Outdated


def fix_get_article_data(xml_with_pre, max_length=None):
data = xml_with_pre.get_article_data(max_length)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Atentar que aqui max_length tem valor padrão None e é repassado explicitamente ao Packtools.

No Packtools 4.16.11, get_article_data() usa 300 quando o argumento é omitido, mas get_article_data(None) chama get_body_fragment(None) e retorna o corpo inteiro.

Não seria melhor colocar o padrão de 300 caracteres ou por meio de max_length=300 ou chmando get_article_data() sem argumento quando max_length is None?

Comment thread pid_provider/models.py Outdated
"z_collab": self.z_collab,
"z_links": self.z_links,
"z_partial_body": self.z_partial_body,
"z_partial_body": self.body_fragment_fingerprint,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Não deveria ser self.xml_with_pre.body_fragment_fingerprint? O outro caminho gera AttributeError ao tentar usar o data_to_compare (PidProviderXML.data_to_compare)

@pitangainnovare pitangainnovare left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Há dois comentários, sendo que em um deles menciona um AttributeError obtido.

Analisar.

- Renomeia a chave 'z_partial_body' para 'body_fragment_fingerprint' em fix_get_data_to_compare, alinhando o nome ao valor efetivamente comparado.
- fix_get_article_data passa a priorizar xml_with_pre.readable_data quando disponível, caindo para get_article_data(max_length=300) como fallback.
- compare_items passa a incluir 'input_data' na resposta quando o score não é 1, permitindo rastrear o valor comparado em caso de divergência.
…roviderXML

- Substitui os campos 'created'/'updated'/'record_status' fixos por uma property record_status, que calcula o estado do registro ('created' ou 'updated') com base na diferença de tempo entre criação e atualização.
- Adiciona 'ppx_id' ao dicionário de dados retornado.
- get_readable_data remove 'partial_body' do dado armazenado antes de retorná-lo.
- data_to_compare passa a incluir article_titles e body_fragment apenas quando presentes, e usa body_fragment_fingerprint no lugar de z_partial_body.
- get_best_match passa a usar um limiar de aceitação dinâmico (min_rate), reduzido para 0.49 quando há poucos dados disponíveis para comparação, e retorna também a resposta detalhada de cada comparação junto ao dado do candidato.
- Evita processar atualização quando o registro correspondente não possui readable_data.
- is_equal_to é considerado falso quando o registro não possui readable_data, evitando falso positivo de igualdade.
- Melhora a mensagem de erro de PidProviderXMLPidV3ConflictError, tornando-a mais informativa sobre o resultado do pareamento.

@pitangainnovare pitangainnovare left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Os dois problemas apontados anteriormente foram resolvidos.

Os testes precisam ser adaptados aos comportamentos introduzidos nos últimos commits. Ao rodar os testes automáticos por meio de:

docker compose -p core -f local.yml run --rm --no-deps django pytest pid_provider -q

Foram obtidos 19 falhas:

WARN[0000] /home/rafaeljpd/Repos/pitanga/scl/core-pr-1469/local.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion 
Container core-django-run-16102b4ede99 Creating 
Container core-django-run-16102b4ede99 Created 
PostgreSQL is available
Test session starts (platform: linux, Python 3.11.13, pytest 7.4.3, pytest-sugar 0.9.7)
django: version: 5.2.7, settings: config.settings.test (from option)
rootdir: /app
configfile: pytest.ini
plugins: sugar-0.9.7, django-4.8.0, Faker-40.36.0, anyio-3.7.1, django-test-migrations-1.3.0


―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― PidProviderXMLBestMatchesTests.test_get_best_match_no_candidates_approved ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

self = <pid_provider.tests.test_get_best_match.PidProviderXMLBestMatchesTests testMethod=test_get_best_match_no_candidates_approved>, mock_compare = <MagicMock name='compare' id='124830705684432'>

    @patch("pid_provider.models.compare")
    def test_get_best_match_no_candidates_approved(self, mock_compare):
        """Quando nenhum candidato atinge score > 0.6, nem 'registered' nem 'matched' devem existir."""
    
        item_fraco = MagicMock(spec=PidProviderXML)
        item_fraco.id = 201
        item_fraco.updated.isoformat.return_value = "2026-06-27T14:00:00"
        item_fraco.data_to_compare = {"title": "Quase igual, mas nao o suficiente"}
        item_fraco.data = {"id": 201, "title": "Quase igual, mas nao o suficiente"}
    
        mock_compare.return_value = {"percentual_score": 0.48}
    
        result = PidProviderXML.get_best_match([item_fraco], self.xml_adapter_data_mock)
    
        self.assertNotIn("registered", result)
        self.assertNotIn("matched", result)
        self.assertEqual(len(result["unmatched"]), 1)
>       self.assertEqual(result["unmatched"][0]["id"], 201)
E       KeyError: 'id'

pid_provider/tests/test_get_best_match.py:67: KeyError

 pid_provider/tests/test_get_best_match.py1% ▎         

――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― PidProviderXMLBestMatchesTests.test_get_best_match_single_match_does_not_expose_matched_key ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

self = <pid_provider.tests.test_get_best_match.PidProviderXMLBestMatchesTests testMethod=test_get_best_match_single_match_does_not_expose_matched_key>, mock_compare = <MagicMock name='compare' id='124830711403856'>

    @patch("pid_provider.models.compare")
    def test_get_best_match_single_match_does_not_expose_matched_key(self, mock_compare):
        """Com apenas 1 item aprovado (>0.6), 'registered' deve existir mas 'matched' NÃO deve ser exposto."""
    
        item_bom = MagicMock(spec=PidProviderXML)
        item_bom.id = 101
        item_bom.updated.isoformat.return_value = "2026-06-27T12:00:00"
        item_bom.data_to_compare = {"title": "Titulo Original", "z_surnames": "Silva; Santos"}
        item_bom.data = {"id": 101, "title": "Titulo Original", "z_surnames": "Silva; Santos"}
    
        item_ruim = MagicMock(spec=PidProviderXML)
        item_ruim.id = 102
        item_ruim.updated.isoformat.return_value = "2026-06-27T13:00:00"
        item_ruim.data_to_compare = {"title": "Outro Titulo Completamente Diferente", "z_surnames": "Alves"}
        item_ruim.data = {"id": 102, "title": "Outro Titulo Completamente Diferente", "z_surnames": "Alves"}
    
        def side_effect_compare(item_data, xml_adapter_data):
            if item_data["title"] == "Titulo Original":
                return {"percentual_score": 0.95}
            return {"percentual_score": 0.20}
    
        mock_compare.side_effect = side_effect_compare
    
        # Enviados fora de ordem propositalmente
        candidates = [item_ruim, item_bom]
        result = PidProviderXML.get_best_match(candidates, self.xml_adapter_data_mock)
    
        # Apenas 1 item passou do corte -> "matched" não deve aparecer
        self.assertNotIn("matched", result)
    
        # "registered" deve existir e ser o OBJETO item de maior score
        self.assertEqual(result["registered"], item_bom)
    
        # "unmatched" sempre é exposto
        self.assertEqual(len(result["unmatched"]), 1)
>       self.assertEqual(result["unmatched"][0]["id"], 102)
E       KeyError: 'id'

pid_provider/tests/test_get_best_match.py:48: KeyError

 pid_provider/tests/test_get_best_match.py2% ▎         

―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― PidProviderXMLBestMatchesTests.test_get_best_match_three_matches_only_secondary_items_in_matched ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

self = <pid_provider.tests.test_get_best_match.PidProviderXMLBestMatchesTests testMethod=test_get_best_match_three_matches_only_secondary_items_in_matched>, mock_compare = <MagicMock name='compare' id='124830705618896'>

    @patch("pid_provider.models.compare")
    def test_get_best_match_three_matches_only_secondary_items_in_matched(self, mock_compare):
        """Com 3+ itens aprovados, 'registered' fica com o 1º colocado e 'matched' com os demais, na mesma ordem de score."""
    
        item_1 = MagicMock(spec=PidProviderXML)
        item_1.id = 401
        item_1.updated.isoformat.return_value = "2026-06-01T00:00:00"
        item_1.data_to_compare = {"title": "A"}
        item_1.data = {"id": 401, "title": "A"}
    
        item_2 = MagicMock(spec=PidProviderXML)
        item_2.id = 402
        item_2.updated.isoformat.return_value = "2026-06-01T00:00:00"
        item_2.data_to_compare = {"title": "B"}
        item_2.data = {"id": 402, "title": "B"}
    
        item_3 = MagicMock(spec=PidProviderXML)
        item_3.id = 403
        item_3.updated.isoformat.return_value = "2026-06-01T00:00:00"
        item_3.data_to_compare = {"title": "C"}
        item_3.data = {"id": 403, "title": "C"}
    
        def side_effect_compare(item_data, xml_adapter_data):
            scores = {"A": 0.95, "B": 0.85, "C": 0.75}
            return {"percentual_score": scores[item_data["title"]]}
    
        mock_compare.side_effect = side_effect_compare
    
        result = PidProviderXML.get_best_match([item_3, item_1, item_2], self.xml_adapter_data_mock)
    
        # item_1 (0.95) é o de maior score -> vira "registered" e some da lista "matched"
        self.assertEqual(result["registered"], item_1)
    
        # "matched" deve conter apenas item_2 (0.85) e item_3 (0.75), nessa ordem
        self.assertEqual(len(result["matched"]), 2)
>       self.assertEqual(result["matched"][0]["id"], 402)
E       KeyError: 'id'

pid_provider/tests/test_get_best_match.py:136: KeyError

 pid_provider/tests/test_get_best_match.py3% ▍         

――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― PidProviderXMLBestMatchesTests.test_get_best_match_two_matches_excludes_registered_from_matched ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

self = <pid_provider.tests.test_get_best_match.PidProviderXMLBestMatchesTests testMethod=test_get_best_match_two_matches_excludes_registered_from_matched>, mock_compare = <MagicMock name='compare' id='124830706124240'>

    @patch("pid_provider.models.compare")
    def test_get_best_match_two_matches_excludes_registered_from_matched(self, mock_compare):
        """Com 2 itens aprovados, 'registered' recebe o de maior score e 'matched' deve conter só o restante (matched[1:])."""
    
        item_antigo = MagicMock(spec=PidProviderXML)
        item_antigo.id = 301
        item_antigo.updated.isoformat.return_value = "2026-01-01T00:00:00"
        item_antigo.data_to_compare = {"title": "Clone"}
        item_antigo.data = {"id": 301, "title": "Clone"}
    
        item_recente = MagicMock(spec=PidProviderXML)
        item_recente.id = 302
        item_recente.updated.isoformat.return_value = "2026-06-27T00:00:00"  # Mais recente
        item_recente.data_to_compare = {"title": "Clone"}
        item_recente.data = {"id": 302, "title": "Clone"}
    
        # Mesmo score alto para os dois -> desempate por 'updated'
        mock_compare.return_value = {"percentual_score": 0.90}
    
        result = PidProviderXML.get_best_match([item_antigo, item_recente], self.xml_adapter_data_mock)
    
        # reverse=True em (score, updated.isoformat(), id);
        # "2026-06-27..." > "2026-01-01..." lexicograficamente, então item_recente vem primeiro (registered).
        self.assertEqual(result["registered"], item_recente)
    
        # "matched" agora é matched[1:] -> exclui o item que virou "registered"
        self.assertIn("matched", result)
        self.assertEqual(len(result["matched"]), 1)
>       self.assertEqual(result["matched"][0]["id"], 301)
E       KeyError: 'id'

pid_provider/tests/test_get_best_match.py:97: KeyError

 pid_provider/tests/test_get_best_match.py4%pid_provider/tests/test_register.py ✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓                                                                                                                                                                                       27% ██▊       
 pid_provider/tests/test_select_record.py ✓✓✓                                                                                                                                                                                                    30% ███       

――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― PidProviderXMLSelectRecordTests.test_select_record_passes_candidates_and_comparison_data_to_get_best_match ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

self = <pid_provider.tests.test_select_record.PidProviderXMLSelectRecordTests testMethod=test_select_record_passes_candidates_and_comparison_data_to_get_best_match>, mock_get_best_match = <MagicMock name='get_best_match' id='124830705917456'>

    @patch("pid_provider.models.PidProviderXML.get_best_match")
    def test_select_record_passes_candidates_and_comparison_data_to_get_best_match(self, mock_get_best_match):
        """get_best_match deve ser chamado com a lista de candidatos do label e os dados já processados do xml_adapter."""
    
        candidates = self._make_results(1)
        xml_adapter = self._make_xml_adapter(data_to_compare={"title": "Foo"})
    
        mock_get_best_match.return_value = {"unmatched": ["ITEM_DATA"]}
    
        PidProviderXML.select_record(xml_adapter, [("journal", candidates)])
    
>       mock_get_best_match.assert_called_once_with(candidates, {"title": "Foo", "z_partial_body": ANY})

pid_provider/tests/test_select_record.py:216: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/usr/local/lib/python3.11/unittest/mock.py:951: in assert_called_once_with
    return self.assert_called_with(*args, **kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <MagicMock name='get_best_match' id='124830705917456'>, args = ([<MagicMock name='candidate_0' id='124830705915344'>], {'title': 'Foo', 'z_partial_body': <ANY>}), kwargs = {}
expected = call([<MagicMock name='candidate_0' id='124830705915344'>], {'title': 'Foo', 'z_partial_body': <ANY>})
actual = call([<MagicMock name='candidate_0' id='124830705915344'>], {'title': 'Foo', 'body_fragment_fingerprint': <MagicMock name='mock.xml_with_pre.body_fragment_fingerprint' id='124830705961424'>})
_error_message = <function NonCallableMock.assert_called_with.<locals>._error_message at 0x7188691d5d00>, cause = None

    def assert_called_with(self, /, *args, **kwargs):
        """assert that the last call was made with the specified arguments.
    
        Raises an AssertionError if the args and keyword args passed in are
        different to the last call to the mock."""
        if self.call_args is None:
            expected = self._format_mock_call_signature(args, kwargs)
            actual = 'not called.'
            error_message = ('expected call not found.\nExpected: %s\n  Actual: %s'
                    % (expected, actual))
            raise AssertionError(error_message)
    
        def _error_message():
            msg = self._format_mock_failure_message(args, kwargs)
            return msg
        expected = self._call_matcher(_Call((args, kwargs), two=True))
        actual = self._call_matcher(self.call_args)
        if actual != expected:
            cause = expected if isinstance(expected, Exception) else None
>           raise AssertionError(_error_message()) from cause
E           AssertionError: expected call not found.
E           Expected: get_best_match([<MagicMock name='candidate_0' id='124830705915344'>], {'title': 'Foo', 'z_partial_body': <ANY>})
E             Actual: get_best_match([<MagicMock name='candidate_0' id='124830705915344'>], {'title': 'Foo', 'body_fragment_fingerprint': <MagicMock name='mock.xml_with_pre.body_fragment_fingerprint' id='124830705961424'>})

/usr/local/lib/python3.11/unittest/mock.py:939: AssertionError

 pid_provider/tests/test_select_record.py ⨯✓✓✓✓✓                                                                                                                                                                                                 37% ███▋      
 pid_provider/tests/test_select_records.py ✓✓✓                                                                                                                                                                                                   40% ████      
 pid_provider/tests/test_query_params.py ✓✓✓✓                                                                                                                                                                                                    44% ████▌     

――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― ValidateInputDataTests.test_raises_not_enough_parameters_when_all_empty ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

self = <pid_provider.tests.test_query_params.ValidateInputDataTests testMethod=test_raises_not_enough_parameters_when_all_empty>

    def test_raises_not_enough_parameters_when_all_empty(self):
        adapter = make_xml_adapter(
            data={"pub_year": "2026", "issn_electronic": "0000-1111"},
        )
        qbuilder = QueryBuilderPidProviderXML(adapter)
>       with self.assertRaises(exceptions.NotEnoughParametersToGetPidProviderXMLError):
E       AssertionError: NotEnoughParametersToGetPidProviderXMLError not raised

pid_provider/tests/test_query_params.py:181: AssertionError

 pid_provider/tests/test_query_params.py45% ████▌     

―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― ValidateInputDataTests.test_raises_not_enough_parameters_when_body_fragment_is_blank ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

self = <pid_provider.tests.test_query_params.ValidateInputDataTests testMethod=test_raises_not_enough_parameters_when_body_fragment_is_blank>

    def test_raises_not_enough_parameters_when_body_fragment_is_blank(self):
        adapter = make_xml_adapter(
            data={"pub_year": "2026", "issn_electronic": "0000-1111"},
            body_fragment="",
        )
        qbuilder = QueryBuilderPidProviderXML(adapter)
>       with self.assertRaises(exceptions.NotEnoughParametersToGetPidProviderXMLError):
E       AssertionError: NotEnoughParametersToGetPidProviderXMLError not raised

pid_provider/tests/test_query_params.py:200: AssertionError

 pid_provider/tests/test_query_params.py46% ████▋     

――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― ValidateInputDataTests.test_raises_not_enough_parameters_when_titles_are_blank ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

self = <pid_provider.tests.test_query_params.ValidateInputDataTests testMethod=test_raises_not_enough_parameters_when_titles_are_blank>

    def test_raises_not_enough_parameters_when_titles_are_blank(self):
        """Lista de títulos só com valores falsy deve ser tratada como vazia."""
        adapter = make_xml_adapter(
            data={"pub_year": "2026", "issn_electronic": "0000-1111"},
            article_titles=["", None],
        )
        qbuilder = QueryBuilderPidProviderXML(adapter)
>       with self.assertRaises(exceptions.NotEnoughParametersToGetPidProviderXMLError):
E       AssertionError: NotEnoughParametersToGetPidProviderXMLError not raised

pid_provider/tests/test_query_params.py:191: AssertionError

 pid_provider/tests/test_query_params.py ⨯✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓                                                                                                                                                                                       65% ██████▌   

――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― PartialBodyQueryTests.test_deduplicates_when_both_hashes_are_equal ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

self = <pid_provider.tests.test_query_params.PartialBodyQueryTests testMethod=test_deduplicates_when_both_hashes_are_equal>

    def test_deduplicates_when_both_hashes_are_equal(self):
        adapter = make_xml_adapter(
            data={},
            z_partial_body="hash-igual",
            body_fragment_fingerprint="hash-igual",
        )
        qbuilder = QueryBuilderPidProviderXML(adapter)
>       self.assertEqual(
            qbuilder.partial_body_query, Q(z_partial_body__in={"hash-igual"})
        )
E       AssertionError: <Q: ([24 chars]n', {<MagicMock name='mock.z_partial_body' id=[31 chars]'}))> != <Q: ([24 chars]n', {'hash-igual'}))>

pid_provider/tests/test_query_params.py:377: AssertionError

 pid_provider/tests/test_query_params.py66% ██████▋   

――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― PartialBodyQueryTests.test_ignores_body_fingerprint ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

self = <pid_provider.tests.test_query_params.PartialBodyQueryTests testMethod=test_ignores_body_fingerprint>

    def test_ignores_body_fingerprint(self):
        adapter = make_xml_adapter(
            data={},
            z_partial_body="hash-legado",
            body_fragment_fingerprint="hash-fragmento-corpo",
            body_fingerprint="hash-corpo-inteiro-que-nao-deve-ser-usado",
        )
        qbuilder = QueryBuilderPidProviderXML(adapter)
    
        expected = Q(
            z_partial_body__in={"hash-legado", "hash-fragmento-corpo"}
        )
>       self.assertDictEqual(
            dict(qbuilder.partial_body_query.children),
            dict(expected.children),
        )
E       AssertionError: {'z_p[14 chars]n': {<MagicMock name='mock.z_partial_body' id=[39 chars]po'}} != {'z_p[14 chars]n': {'hash-legado', 'hash-fragmento-corpo'}}
E       - {'z_partial_body__in': {'hash-fragmento-corpo',
E       ?                                               ^
E       
E       + {'z_partial_body__in': {'hash-legado', 'hash-fragmento-corpo'}}
E       ?                         +++++++++++++++                      ^^
E       
E       -                         <MagicMock name='mock.z_partial_body' id='124830711058960'>}}

pid_provider/tests/test_query_params.py:410: AssertionError

 pid_provider/tests/test_query_params.py67% ██████▋   

――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― PartialBodyQueryTests.test_uses_in_with_both_hashes_when_both_present_and_different ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

self = <pid_provider.tests.test_query_params.PartialBodyQueryTests testMethod=test_uses_in_with_both_hashes_when_both_present_and_different>

    def test_uses_in_with_both_hashes_when_both_present_and_different(self):
        adapter = make_xml_adapter(
            data={},
            z_partial_body="hash-legado",
            body_fragment_fingerprint="hash-fragmento-corpo",
        )
        qbuilder = QueryBuilderPidProviderXML(adapter)
        expected = Q(
            z_partial_body__in={"hash-legado", "hash-fragmento-corpo"}
        )
>       self.assertDictEqual(
            dict(qbuilder.partial_body_query.children),
            dict(expected.children),
        )
E       AssertionError: {'z_p[20 chars]hash-fragmento-corpo', <MagicMock name='mock.z[33 chars]0'>}} != {'z_p[20 chars]hash-legado', 'hash-fragmento-corpo'}}
E       - {'z_partial_body__in': {'hash-fragmento-corpo',
E       ?                                               ^
E       
E       + {'z_partial_body__in': {'hash-legado', 'hash-fragmento-corpo'}}
E       ?                         +++++++++++++++                      ^^
E       
E       -                         <MagicMock name='mock.z_partial_body' id='124830693212880'>}}

pid_provider/tests/test_query_params.py:365: AssertionError

 pid_provider/tests/test_query_params.py68% ██████▊   

――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― PartialBodyQueryTests.test_uses_in_with_only_body_fragment_fingerprint ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

self = <pid_provider.tests.test_query_params.PartialBodyQueryTests testMethod=test_uses_in_with_only_body_fragment_fingerprint>

    def test_uses_in_with_only_body_fragment_fingerprint(self):
        adapter = make_xml_adapter(
            data={},
            body_fragment_fingerprint="hash-fragmento-corpo",
        )
        qbuilder = QueryBuilderPidProviderXML(adapter)
>       self.assertEqual(
            qbuilder.partial_body_query,
            Q(z_partial_body__in={"hash-fragmento-corpo"}),
        )
E       AssertionError: <Q: ([24 chars]n', {<MagicMock name='mock.z_partial_body' id=[41 chars]'}))> != <Q: ([24 chars]n', {'hash-fragmento-corpo'}))>

pid_provider/tests/test_query_params.py:350: AssertionError

 pid_provider/tests/test_query_params.py69% ██████▉   

―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― PartialBodyQueryTests.test_uses_in_with_only_legacy_partial_body ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

self = <pid_provider.tests.test_query_params.PartialBodyQueryTests testMethod=test_uses_in_with_only_legacy_partial_body>

    def test_uses_in_with_only_legacy_partial_body(self):
        adapter = make_xml_adapter(
            data={},
            z_partial_body="hash-legado",
            body_fragment_fingerprint=None,
        )
        qbuilder = QueryBuilderPidProviderXML(adapter)
>       self.assertEqual(
            qbuilder.partial_body_query, Q(z_partial_body__in={"hash-legado"})
        )
E       AssertionError: <Q: ([24 chars]n', {<MagicMock name='mock.z_partial_body' id=[17 chars]>}))> != <Q: ([24 chars]n', {'hash-legado'}))>

pid_provider/tests/test_query_params.py:340: AssertionError

 pid_provider/tests/test_query_params.py70% ███████   

――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― PartialBodyQueryTests.test_uses_isnull_when_neither_hash_is_present ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

self = <pid_provider.tests.test_query_params.PartialBodyQueryTests testMethod=test_uses_isnull_when_neither_hash_is_present>

    def test_uses_isnull_when_neither_hash_is_present(self):
        """
        Regressão do incidente: quando o XML de entrada não tem nenhum
        hash de corpo, a query deve usar isnull=True (equivalente ao
        antigo Q(z_partial_body=None)), e JAMAIS __in=(None, None), que
        em SQL nunca casaria com candidatos cujo z_partial_body é NULL
        (NULL = NULL é UNKNOWN, não True).
        """
        adapter = make_xml_adapter(
            data={}, z_partial_body=None, body_fragment_fingerprint=None
        )
        qbuilder = QueryBuilderPidProviderXML(adapter)
>       self.assertEqual(qbuilder.partial_body_query, Q(z_partial_body__isnull=True))
E       AssertionError: <Q: ([19 chars]dy__in', {<MagicMock name='mock.z_partial_body[22 chars]>}))> != <Q: ([19 chars]dy__isnull', True))>

pid_provider/tests/test_query_params.py:393: AssertionError

 pid_provider/tests/test_query_params.py71% ███████▏  

――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― ArticleDataQueryTests.test_combines_textual_fields_with_partial_body_query_both_hashes ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

self = <pid_provider.tests.test_query_params.ArticleDataQueryTests testMethod=test_combines_textual_fields_with_partial_body_query_both_hashes>

    def test_combines_textual_fields_with_partial_body_query_both_hashes(self):
        adapter = make_xml_adapter(
            data={
                "z_surnames": "Silva",
                "z_collab": None,
                "z_links": None,
            },
            z_partial_body="hash-legado",
            body_fragment_fingerprint="hash-fragmento-corpo",
        )
        qbuilder = QueryBuilderPidProviderXML(adapter)
        expected = Q(z_surnames="Silva", z_collab=None, z_links=None) & Q(
            z_partial_body__in={"hash-legado", "hash-fragmento-corpo"}
        )
>       self.assertDictEqual(
            dict(qbuilder.article_data_query.children),
            dict(expected.children),
        )
E       AssertionError: {'z_c[78 chars]hash-fragmento-corpo', <MagicMock name='mock.z[33 chars]0'>}} != {'z_c[78 chars]hash-legado', 'hash-fragmento-corpo'}}
E         {'z_collab': None,
E          'z_links': None,
E       -  'z_partial_body__in': {'hash-fragmento-corpo',
E       +  'z_partial_body__in': {'hash-legado', 'hash-fragmento-corpo'},
E       ?                         +++++++++++++++                      +
E       
E       -                         <MagicMock name='mock.z_partial_body' id='124830710734800'>},
E          'z_surnames': 'Silva'}

pid_provider/tests/test_query_params.py:437: AssertionError

 pid_provider/tests/test_query_params.py72% ███████▎  

――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― ArticleDataQueryTests.test_falls_back_to_isnull_when_no_body_hash_available ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

self = <pid_provider.tests.test_query_params.ArticleDataQueryTests testMethod=test_falls_back_to_isnull_when_no_body_hash_available>

    def test_falls_back_to_isnull_when_no_body_hash_available(self):
        adapter = make_xml_adapter(data={}, body_fragment_fingerprint=None)
        qbuilder = QueryBuilderPidProviderXML(adapter)
        expected = Q(z_surnames=None, z_collab=None, z_links=None) & Q(
            z_partial_body__isnull=True
        )
>       self.assertEqual(qbuilder.article_data_query, expected)
E       AssertionError: <Q: ([80 chars]dy__in', {<MagicMock name='mock.z_partial_body[22 chars]>}))> != <Q: ([80 chars]dy__isnull', True))>

pid_provider/tests/test_query_params.py:448: AssertionError

 pid_provider/tests/test_query_params.py ⨯✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓                                                                                                                                                                                        89% ████████▉ 

―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― CompareItemsTests.test_different_scalars_uses_how_similar_and_includes_registered ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

self = <pid_provider.tests.test_query_params.CompareItemsTests testMethod=test_different_scalars_uses_how_similar_and_includes_registered>, mock_how_similar = <MagicMock name='how_similar' id='124830705601680'>

    @patch("pid_provider.query_params.how_similar")
    def test_different_scalars_uses_how_similar_and_includes_registered(
        self, mock_how_similar
    ):
        mock_how_similar.return_value = 0.4
        result = compare_items("z_surnames", "Silva", "Souza")
>       self.assertEqual(
            result, {"label": "z_surnames", "score": 0.4, "registered": "Silva"}
        )
E       AssertionError: {'lab[15 chars]s', 'score': 0.4, 'registered': 'Silva', 'input_data': 'Souza'} != {'lab[15 chars]s', 'score': 0.4, 'registered': 'Silva'}
E       + {'label': 'z_surnames', 'registered': 'Silva', 'score': 0.4}
E       - {'input_data': 'Souza',
E       -  'label': 'z_surnames',
E       -  'registered': 'Silva',
E       -  'score': 0.4}

pid_provider/tests/test_query_params.py:596: AssertionError

 pid_provider/tests/test_query_params.py ⨯✓✓✓                                                                                                                                                                                                    94% █████████▍

―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― CompareItemsTests.test_none_input_data_falls_back_to_empty_string_for_how_similar ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

self = <pid_provider.tests.test_query_params.CompareItemsTests testMethod=test_none_input_data_falls_back_to_empty_string_for_how_similar>, mock_how_similar = <MagicMock name='how_similar' id='124830692963024'>

    @patch("pid_provider.query_params.how_similar")
    def test_none_input_data_falls_back_to_empty_string_for_how_similar(
        self, mock_how_similar
    ):
        mock_how_similar.return_value = 0.2
        result = compare_items("z_links", "algum-link", None)
>       self.assertEqual(
            result, {"label": "z_links", "score": 0.2, "registered": "algum-link"}
        )
E       AssertionError: {'lab[14 chars], 'score': 0.2, 'registered': 'algum-link', 'input_data': None} != {'lab[14 chars], 'score': 0.2, 'registered': 'algum-link'}
E       + {'label': 'z_links', 'registered': 'algum-link', 'score': 0.2}
E       - {'input_data': None,
E       -  'label': 'z_links',
E       -  'registered': 'algum-link',
E       -  'score': 0.2}

pid_provider/tests/test_query_params.py:607: AssertionError

 pid_provider/tests/test_query_params.py95% █████████▌

―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― CompareItemsTests.test_none_registered_falls_back_to_empty_string_for_how_similar ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

self = <pid_provider.tests.test_query_params.CompareItemsTests testMethod=test_none_registered_falls_back_to_empty_string_for_how_similar>, mock_how_similar = <MagicMock name='how_similar' id='124830693055696'>

    @patch("pid_provider.query_params.how_similar")
    def test_none_registered_falls_back_to_empty_string_for_how_similar(
        self, mock_how_similar
    ):
        mock_how_similar.return_value = 0.3
        result = compare_items("z_links", None, "algum-link")
>       self.assertEqual(result, {"label": "z_links", "score": 0.3, "registered": None})
E       AssertionError: {'lab[14 chars], 'score': 0.3, 'registered': None, 'input_data': 'algum-link'} != {'lab[14 chars], 'score': 0.3, 'registered': None}
E       + {'label': 'z_links', 'registered': None, 'score': 0.3}
E       - {'input_data': 'algum-link',
E       -  'label': 'z_links',
E       -  'registered': None,
E       -  'score': 0.3}

pid_provider/tests/test_query_params.py:618: AssertionError

 pid_provider/tests/test_query_params.py ⨯✓✓✓✓                                                                                                                                                                                                  100% ██████████
====================================================================================================================== warnings summary =======================================================================================================================
../usr/local/lib/python3.11/site-packages/django/db/backends/utils.py:98
  /usr/local/lib/python3.11/site-packages/django/db/backends/utils.py:98: RuntimeWarning: Accessing the database during app initialization is discouraged. To fix this warning, avoid executing queries in AppConfig.ready() or when your app modules are imported.
    warnings.warn(self.APPS_NOT_READY_WARNING_MSG, category=RuntimeWarning)

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=================================================================================================================== short test summary info ===================================================================================================================
FAILED pid_provider/tests/test_get_best_match.py::PidProviderXMLBestMatchesTests::test_get_best_match_no_candidates_approved - KeyError: 'id'
FAILED pid_provider/tests/test_get_best_match.py::PidProviderXMLBestMatchesTests::test_get_best_match_single_match_does_not_expose_matched_key - KeyError: 'id'
FAILED pid_provider/tests/test_get_best_match.py::PidProviderXMLBestMatchesTests::test_get_best_match_three_matches_only_secondary_items_in_matched - KeyError: 'id'
FAILED pid_provider/tests/test_get_best_match.py::PidProviderXMLBestMatchesTests::test_get_best_match_two_matches_excludes_registered_from_matched - KeyError: 'id'
FAILED pid_provider/tests/test_select_record.py::PidProviderXMLSelectRecordTests::test_select_record_passes_candidates_and_comparison_data_to_get_best_match - AssertionError: expected call not found.
FAILED pid_provider/tests/test_query_params.py::ValidateInputDataTests::test_raises_not_enough_parameters_when_all_empty - AssertionError: NotEnoughParametersToGetPidProviderXMLError not raised
FAILED pid_provider/tests/test_query_params.py::ValidateInputDataTests::test_raises_not_enough_parameters_when_body_fragment_is_blank - AssertionError: NotEnoughParametersToGetPidProviderXMLError not raised
FAILED pid_provider/tests/test_query_params.py::ValidateInputDataTests::test_raises_not_enough_parameters_when_titles_are_blank - AssertionError: NotEnoughParametersToGetPidProviderXMLError not raised
FAILED pid_provider/tests/test_query_params.py::PartialBodyQueryTests::test_deduplicates_when_both_hashes_are_equal - AssertionError: <Q: ([24 chars]n', {<MagicMock name='mock.z_partial_body' id=[31 chars]'}))> != <Q: ([24 chars]n', {'hash-igual'}))>
FAILED pid_provider/tests/test_query_params.py::PartialBodyQueryTests::test_ignores_body_fingerprint - AssertionError: {'z_p[14 chars]n': {<MagicMock name='mock.z_partial_body' id=[39 chars]po'}} != {'z_p[14 chars]n': {'hash-legado', 'hash-fragmento-corpo'}}
FAILED pid_provider/tests/test_query_params.py::PartialBodyQueryTests::test_uses_in_with_both_hashes_when_both_present_and_different - AssertionError: {'z_p[20 chars]hash-fragmento-corpo', <MagicMock name='mock.z[33 chars]0'>}} != {'z_p[20 chars]hash-legado', 'hash-fragmento-corpo'}}
FAILED pid_provider/tests/test_query_params.py::PartialBodyQueryTests::test_uses_in_with_only_body_fragment_fingerprint - AssertionError: <Q: ([24 chars]n', {<MagicMock name='mock.z_partial_body' id=[41 chars]'}))> != <Q: ([24 chars]n', {'hash-fragmento-corpo'}))>
FAILED pid_provider/tests/test_query_params.py::PartialBodyQueryTests::test_uses_in_with_only_legacy_partial_body - AssertionError: <Q: ([24 chars]n', {<MagicMock name='mock.z_partial_body' id=[17 chars]>}))> != <Q: ([24 chars]n', {'hash-legado'}))>
FAILED pid_provider/tests/test_query_params.py::PartialBodyQueryTests::test_uses_isnull_when_neither_hash_is_present - AssertionError: <Q: ([19 chars]dy__in', {<MagicMock name='mock.z_partial_body[22 chars]>}))> != <Q: ([19 chars]dy__isnull', True))>
FAILED pid_provider/tests/test_query_params.py::ArticleDataQueryTests::test_combines_textual_fields_with_partial_body_query_both_hashes - AssertionError: {'z_c[78 chars]hash-fragmento-corpo', <MagicMock name='mock.z[33 chars]0'>}} != {'z_c[78 chars]hash-legado', 'hash-fragmento-corpo'}}
FAILED pid_provider/tests/test_query_params.py::ArticleDataQueryTests::test_falls_back_to_isnull_when_no_body_hash_available - AssertionError: <Q: ([80 chars]dy__in', {<MagicMock name='mock.z_partial_body[22 chars]>}))> != <Q: ([80 chars]dy__isnull', True))>
FAILED pid_provider/tests/test_query_params.py::CompareItemsTests::test_different_scalars_uses_how_similar_and_includes_registered - AssertionError: {'lab[15 chars]s', 'score': 0.4, 'registered': 'Silva', 'input_data': 'Souza'} != {'lab[15 chars]s', 'score': 0.4, 'registered': 'Silva'}
FAILED pid_provider/tests/test_query_params.py::CompareItemsTests::test_none_input_data_falls_back_to_empty_string_for_how_similar - AssertionError: {'lab[14 chars], 'score': 0.2, 'registered': 'algum-link', 'input_data': None} != {'lab[14 chars], 'score': 0.2, 'registered': 'algum-link'}
FAILED pid_provider/tests/test_query_params.py::CompareItemsTests::test_none_registered_falls_back_to_empty_string_for_how_similar - AssertionError: {'lab[14 chars], 'score': 0.3, 'registered': None, 'input_data': 'algum-link'} != {'lab[14 chars], 'score': 0.3, 'registered': None}

Results (2.93s):
      74 passed
      19 failed
         - pid_provider/tests/test_get_best_match.py:50 PidProviderXMLBestMatchesTests.test_get_best_match_no_candidates_approved
         - pid_provider/tests/test_get_best_match.py:13 PidProviderXMLBestMatchesTests.test_get_best_match_single_match_does_not_expose_matched_key
         - pid_provider/tests/test_get_best_match.py:101 PidProviderXMLBestMatchesTests.test_get_best_match_three_matches_only_secondary_items_in_matched
         - pid_provider/tests/test_get_best_match.py:69 PidProviderXMLBestMatchesTests.test_get_best_match_two_matches_excludes_registered_from_matched
         - pid_provider/tests/test_select_record.py:205 PidProviderXMLSelectRecordTests.test_select_record_passes_candidates_and_comparison_data_to_get_best_match
         - pid_provider/tests/test_query_params.py:176 ValidateInputDataTests.test_raises_not_enough_parameters_when_all_empty
         - pid_provider/tests/test_query_params.py:194 ValidateInputDataTests.test_raises_not_enough_parameters_when_body_fragment_is_blank
         - pid_provider/tests/test_query_params.py:184 ValidateInputDataTests.test_raises_not_enough_parameters_when_titles_are_blank
         - pid_provider/tests/test_query_params.py:370 PartialBodyQueryTests.test_deduplicates_when_both_hashes_are_equal
         - pid_provider/tests/test_query_params.py:398 PartialBodyQueryTests.test_ignores_body_fingerprint
         - pid_provider/tests/test_query_params.py:355 PartialBodyQueryTests.test_uses_in_with_both_hashes_when_both_present_and_different
         - pid_provider/tests/test_query_params.py:344 PartialBodyQueryTests.test_uses_in_with_only_body_fragment_fingerprint
         - pid_provider/tests/test_query_params.py:333 PartialBodyQueryTests.test_uses_in_with_only_legacy_partial_body
         - pid_provider/tests/test_query_params.py:381 PartialBodyQueryTests.test_uses_isnull_when_neither_hash_is_present
         - pid_provider/tests/test_query_params.py:423 ArticleDataQueryTests.test_combines_textual_fields_with_partial_body_query_both_hashes
         - pid_provider/tests/test_query_params.py:442 ArticleDataQueryTests.test_falls_back_to_isnull_when_no_body_hash_available
         - pid_provider/tests/test_query_params.py:590 CompareItemsTests.test_different_scalars_uses_how_similar_and_includes_registered
         - pid_provider/tests/test_query_params.py:601 CompareItemsTests.test_none_input_data_falls_back_to_empty_string_for_how_similar
         - pid_provider/tests/test_query_params.py:612 CompareItemsTests.test_none_registered_falls_back_to_empty_string_for_how_similar

Cobre PidProviderSetting (ex.: record_all_registration_events e
demais opções configuráveis via Wagtail Admin). Mensagem gerada a
partir do nome do arquivo — ajustar detalhes de implementação após
revisão do conteúdo.
Cobre o modelo XMLVersion (versionamento de XML associado a
PidProviderXML). Mensagem gerada a partir do nome do arquivo —
ajustar detalhes de implementação após revisão do conteúdo.
Cobre geração/resolução de URLs associadas ao XML registrado.
Mensagem gerada a partir do nome do arquivo — ajustar detalhes de
implementação após revisão do conteúdo.
Cobre o relacionamento OtherPid/InlinePanel 'Other PID' em
PidProviderXML. Mensagem gerada a partir do nome do arquivo — ajustar
detalhes de implementação após revisão do conteúdo.
Cobre lógica de fix/normalização do PID v2 (ex.: CollectionPidV2 ou
rotina de correção equivalente). Mensagem gerada a partir do nome do
arquivo — ajustar detalhes de implementação após revisão do
conteúdo.
Cobre funções utilitárias usadas por PidProviderXML (possivelmente
os wrappers fix_get_article_data/fix_get_data_to_compare ou
equivalentes). Mensagem gerada a partir do nome do arquivo — ajustar
detalhes de implementação após revisão do conteúdo.
Cobre rotinas de manutenção/limpeza sobre registros existentes de
PidProviderXML. Mensagem gerada a partir do nome do arquivo — ajustar
detalhes de implementação após revisão do conteúdo.
Cobre o modelo PidProviderXMLRegistration e a lógica de quando
gravar eventos de auditoria (erro, ambiguidade ou
record_all_registration_events). Mensagem gerada a partir do nome do
arquivo — ajustar detalhes de implementação após revisão do
conteúdo.
…z_partial_body

Ajusta make_xml_adapter() para configurar explicitamente
adapter.z_partial_body (agora lido como atributo direto pelo
QueryBuilderPidProviderXML, não mais via xml_adapter.data.get()),
evitando MagicMock não configurado no teste.

CompareItemsTests: passa a esperar também a chave 'input_data' no
retorno de compare_items() quando o score é != 1, além de
'registered'.

CompareTests, reescrito para refletir que compare() NÃO pula mais
labels ausentes em input_data (usa .get(label), tratando ausência
como None):
- test_missing_input_key_is_treated_as_none_not_skipped (renomeado de
  ...is_skipped_not_treated_as_none): label ausente com valor
  registrado falsy agora ENTRA em items com score 1, em vez de ser
  descartado.
- test_missing_input_key_with_truthy_registered_value_lowers_score
  (novo): label ausente com valor registrado truthy cai no ramo
  how_similar (score < 1).
- test_empty_registered_items_raises_zero_division_error (renomeado
  de test_all_labels_missing_raises_zero_division_error): único
  cenário que ainda levanta ZeroDivisionError é registered_items
  vazio, não mais 'todos os labels ausentes em input_data'.
…response

get_best_match() passou a envolver cada candidato em
{'data': ..., 'response': ...} nas listas 'matched'/'unmatched', em
vez do dict .data cru. Ajusta as asserções de
result['unmatched'][0]['id'] e result['matched'][N]['id'] para
result[...][N]['data']['id'] nos quatro testes afetados.
select_record() não usa mais xml_adapter.get_data_to_compare() puro:
passa a chamar fix_get_data_to_compare(xml_adapter), que acrescenta a
chave 'body_fragment_fingerprint' (lida de
xml_adapter.xml_with_pre.body_fragment_fingerprint) ao dict retornado
por get_data_to_compare().

_make_xml_adapter() passa a aceitar body_fragment_fingerprint e
configurá-lo no mock, para não ficar um MagicMock não configurado.

test_select_record_passes_candidates_and_comparison_data_to_get_best_match
atualizado para esperar que get_best_match seja chamado com o dict
já acrescido de 'body_fragment_fingerprint', não mais o retorno cru
de get_data_to_compare().
Alinha os testes à mudança de register(), que agora faz
input_data.update(xml_with_pre.readable_data) em vez de usar
get_article_data(). O helper make_xml_with_pre() passa a
configurar readable_data como um dict de verdade (com surnames,
collab, links, article_titles, body_fragment) e
body_fragment_fingerprint, já que um MagicMock não configurado
quebra dict.update() com TypeError — erro que era silenciosamente
capturado pelo except Exception de register() e mascarado como
event_status='error'.

Adiciona patch de PidProviderSetting.load no setUp da classe base
(RegisterTestBase), desativado por padrão, para desacoplar os
testes da configuração persistida e permitir reuso em
RecordAllEventsSettingTest sem duplicar o patch.

Adiciona RegisterResponseSchemaTest como teste de contrato: valida
apenas as chaves presentes em cada nível do response de register()
(sucesso limpo, erro, conflito, skipped), incluindo verificação
explícita de que as chaves de readable_data substituem as antigas
(ex.: ausência de 'partial_body'). Funciona como sentinela contra
mudanças silenciosas de schema que testes anteriores, focados
apenas em event_status/v3, não detectavam.
Remove a chave 'finger_print' solta em record_data() e passa a
aninhar o retorno de get_readable_data() sob a chave
'registered_data', em vez de mesclá-lo (update) diretamente no
nível superior do dict. Isso evita colisão de chaves entre os
dados legíveis e os demais campos do registro.
@robertatakenaka
robertatakenaka merged commit e702cb2 into scieloorg:main Sep 1, 2026
3 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants