Files
paper-atlas/tests/test_dblp_records.py
T
codex-bot 3f3dc3f7ad
Metadata integrity checks / metadata (push) Successful in 1m9s
Distinguish historical primary editions from affiliated workshops
2026-11-08 15:12:18 +08:00

209 lines
16 KiB
Python

import hashlib
import json
from contextlib import closing
from pathlib import Path
import sqlite3
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import patch
from paper_atlas.collectors.dblp import XML_SHA
from paper_atlas.collectors.dblp_records import collect,identifiers,normalize_row,material_reason
from paper_atlas.collectors.dblp_scope import scope, HISTORICAL_WORKSHOP_TITLES
from paper_atlas.db.store import Store
from paper_atlas.ingestion.records import ingest_file
class DblpRecordTests(unittest.TestCase):
def setUp(self):
# Tiny fixtures may live on a small tmpfs; production reserve stays 8 GiB.
budget=patch('paper_atlas.collectors.dblp_records.shutil.disk_usage',return_value=SimpleNamespace(free=16*1024**3))
budget.start();self.addCleanup(budget.stop)
def row(self,family='aaai',book='AAAI',year=2024,key='one'):
return {'record_type':'inproceedings','key':f'conf/{family}/{key}','publtype':None,
'fields':{'title':['A research publication'],'author':['Jane Doe 0001','John Smith'],
'booktitle':[book],'year':[str(year)],'crossref':[f'conf/{family}/{year}'],'ee':['https://doi.org/10.1234/'+key]}}
def volume(self,family,year,title=None):
return {'record_type':'proceedings','key':f'conf/{family}/{year}','fields':{'title':[title or f'{family.upper()} {year}'],'year':[str(year)]}}
def test_real_directory_aliases_do_not_confuse_other_conferences(self):
row=self.row('clear2','CLeaR');volume=self.volume('clear2',2024,'Causal Learning and Reasoning, 2024')
self.assertEqual(scope(row,volume)[0]['venue'],'CLeaR')
self.assertIsNone(scope(self.row('clear','CLEAR'),self.volume('clear',2024))[0])
self.assertIsNone(scope(self.row('aamas','Adaptive Agents and Multi-Agent Systems'),self.volume('aamas',2024))[0])
self.assertEqual(scope(self.row('atal','AAMAS'),self.volume('atal',2024))[0]['venue'],'AAMAS')
self.assertEqual(scope(self.row('aips','ICAPS'),self.volume('aips',2024))[0]['venue'],'ICAPS')
def test_conference_year_is_distinct_from_publication_year(self):
row=self.row('nips','NIPS',1988);row['fields']['crossref']=['conf/nips/1987']
volume=self.volume('nips',1987,'NIPS Conference, Denver, 1987; published 1988')
result,_=scope(row,volume);self.assertEqual(result['year'],1987);self.assertEqual(result['bibliographic_year'],1988)
volume['fields']['title']=['NIPS collected papers']
self.assertEqual(scope(row,volume)[1],'unverified_year_difference')
def test_joint_events_and_tracks_are_explicit(self):
result,_=scope(self.row('ijcai','IJCAI/ECAI',2022),self.volume('ijcai',2022))
self.assertEqual(result['venue'],'ECAI+IJCAI');self.assertEqual(result['venue_aliases'],['ECAI','IJCAI'])
result,_=scope(self.row('coling','LREC/COLING'),self.volume('coling',2024))
self.assertEqual(result['venue'],'COLING+LREC')
for book,track in [('ACL (Findings)','findings'),('ACL (Student Research Workshop)','workshop'),('ACL (System Demonstrations)','demo')]:
self.assertEqual(scope(self.row('acl',book),self.volume('acl',2024))[0]['track'],track)
for suffix,track in [('Special interest tracks and posters','special_interest'),('Alternate Track Papers & Posters','alternate')]:
self.assertEqual(scope(self.row('www','WWW ('+suffix+')'),self.volume('www',2024))[0]['track'],track)
volume=self.volume('pkdd',2026,'ECML PKDD 2026, Applied Data Science Track, Demo Track and Industrial Track')
self.assertEqual(scope(self.row('pkdd','ECML PKDD (10)',2026),volume)[0]['track'],'proceedings_unspecified')
def test_short_year_keys_require_a_unique_explicit_year_not_a_guessed_century(self):
row=self.row('sigir','SIGIR',1980);volume=self.volume('sigir',1980,'Information Retrieval Research, Cambridge, June 1980')
volume['key']='conf/sigir/80';volume['fields']['year']=['1981'];row['fields']['crossref']=[volume['key']]
edition,_=scope(row,volume);self.assertEqual(edition['year'],1980)
self.assertIn('two-digit',edition['edition_year_basis'])
volume['fields']['title']=['SIGIR 1980, published 1981']
self.assertEqual(scope(row,volume)[1],'unverified_edition_year')
volume['fields']['title']=['SIGIR 1989']
self.assertEqual(scope(row,volume)[1],'unverified_edition_year')
def test_crossref_mismatch_and_future_editions_are_held_for_review(self):
self.assertEqual(scope(self.row(),self.volume('kdd',2024))[1],'crossref_venue_conflict')
self.assertEqual(scope(self.row(year=2027),self.volume('aaai',2027))[1],'future_edition')
def test_historical_primary_series_workshop_names_require_exact_editions(self):
self.assertEqual(set(HISTORICAL_WORKSHOP_TITLES),
{('aistats',year) for year in [1995,1997,1999,2001,2003,2005]} |
{('colt',year) for year in [1988,1989,1990,1991]})
for (family,year),prefix in HISTORICAL_WORKSHOP_TITLES.items():
with self.subTest(family=family,year=year):
title=prefix+f', {family.upper()} {year}, Proceedings.'
volume=self.volume(family,year,title)
row=self.row(family,family.upper(),year)
if year==1995:
row['fields']['year']=['1996'];volume['fields']['year']=['1996']
edition,reason=scope(row,volume)
self.assertIsNone(reason);self.assertEqual(edition['track'],'main')
self.assertEqual(edition['year'],year)
self.assertIn('Historical primary series',edition['track_scope_note'])
record,reason=normalize_row(row,{volume['key']:volume},{family.upper()})
self.assertIsNone(reason);self.assertEqual(record['metadata']['track_scope_evidence_url'],edition['track_scope_evidence_url'])
self.assertEqual(record['metadata']['proceedings_title'],title)
if year==1995:self.assertEqual(record['metadata']['bibliographic_year'],1996)
def test_historical_exception_does_not_promote_affiliated_or_wrong_scope_workshops(self):
title=HISTORICAL_WORKSHOP_TITLES[('aistats',1995)]+', AISTATS 1995, Proceedings.'
for book,key,changed in [
('AISTATS','conf/aistats/1995w',title),
('AISTATS (Workshops)','conf/aistats/1995',title),
('AISTATS','conf/aistats/1995',title.replace('Fifth','Sixth')),
('AISTATS','conf/aistats/1995',title.replace('1995','1997'))]:
volume=self.volume('aistats',1995,changed);volume['key']=key
row=self.row('aistats',book,1995);row['fields']['crossref']=[key]
self.assertEqual(scope(row,volume)[0]['track'],'workshop')
volume=self.volume('aistats',2010,'Active Learning and Experimental Design workshop, In conjunction with AISTATS 2010')
volume['key']='conf/aistats/2010w'
row=self.row('aistats','AISTATS',2011);row['fields']['crossref']=[volume['key']]
self.assertEqual(scope(row,volume)[0]['track'],'workshop')
def test_combined_colt_kernel_volume_keeps_unknown_per_paper_track(self):
title='Computational Learning Theory and Kernel Machines, 16th Annual Conference on Computational Learning Theory and 7th Kernel Workshop, COLT/Kernel 2003, Washington, DC, USA, August 24-27, 2003, Proceedings'
volume=self.volume('colt',2003,title);row=self.row('colt','COLT',2003)
record,reason=normalize_row(row,{volume['key']:volume},{'COLT'})
self.assertIsNone(reason);self.assertEqual(record['track'],'proceedings_unspecified')
self.assertEqual(record['status'],'accepted')
self.assertIn('per-paper section absent',record['metadata']['track_scope_note'])
self.assertEqual(record['metadata']['track_scope_evidence_url'],'https://doi.org/10.1007/b12006')
volume['key']='conf/colt/2003w';row['fields']['crossref']=[volume['key']]
self.assertEqual(scope(row,volume)[0]['track'],'workshop')
def test_historical_scope_provenance_survives_file_ingestion(self):
title=HISTORICAL_WORKSHOP_TITLES[('colt',1988)]+', COLT 1988, Proceedings.'
row=self.row('colt','COLT',1988);volume=self.volume('colt',1988,title)
record,reason=normalize_row(row,{volume['key']:volume},{'COLT'})
self.assertIsNone(reason)
with tempfile.TemporaryDirectory() as tmp:
root=Path(tmp);path=root/'history.jsonl'
path.write_text(json.dumps(record)+'\n')
path.with_suffix('.manifest.json').write_text(json.dumps({'source':'dblp','count':1,'list_url':'https://doi.org/10.4230/dblp.xml.2026-11-01','file_sha256':hashlib.sha256(path.read_bytes()).hexdigest()}))
ingest_file(root/'sample.sqlite',path)
with closing(sqlite3.connect(root/'sample.sqlite')) as connection:
track=connection.execute('SELECT track FROM editions').fetchone()[0]
metadata=json.loads(connection.execute('SELECT metadata_json FROM source_records').fetchone()[0])
self.assertEqual(track,'main')
self.assertEqual(metadata['track_scope_evidence_url'],'https://www.learningtheory.org/')
self.assertEqual(metadata['proceedings_title'],title)
def test_only_own_identifier_links_are_used_and_ambiguity_is_preserved(self):
own,ambiguous=identifiers({'ee':['https://doi.org/10.1234/published','https://doi.org/10.48550/arXiv.2401.00001','https://openreview.net/forum?id=realForum'],
'cite':['https://doi.org/10.9999/reference']})
self.assertEqual(own,{'doi':'10.1234/published','arxiv':'2401.00001','openreview':'realForum'});self.assertFalse(ambiguous)
own,ambiguous=identifiers({'ee':['https://doi.org/10.1234/a','https://doi.org/10.1234/b','https://openreview.net/forum?id=2026-Oral--99-placeholder']})
self.assertEqual(own,{});self.assertEqual(len(ambiguous['doi']),2)
def test_frontmatter_and_anonymous_event_headers_are_not_paper_acceptances(self):
for title in ('Title, Preface.','Title, Conference Committee, Table of Contents, Author Index.','Front Matter.','Backmatter.','Foreword.'):
self.assertEqual(material_reason(title,['An Editor'],'conf/sigir/C78',1978,'main'),'frontmatter')
for title in ('Sponsors.','IJCAI-09 Awards and Distinguished Papers.','2013 Conference Organization.','Area Chairs and Program Committee.','Welcome to ICDM 2005.','Workshop Summaries.'):
self.assertIsNotNone(material_reason(title,[],'conf/ijcai/X09',2009,'main'))
self.assertEqual(material_reason("Wiki Workshop Chairs' Welcome.",['An Organizer'],'conf/www/X19t',2019,'companion'),'frontmatter')
self.assertEqual(material_reason('Attention - from Neuroscience to the Web and Wellbeing.',[],'conf/www/X19b',2019,'companion'),'unverified_proceedings_material')
row=self.row();row['fields']['title']=['Sponsors.'];row['fields']['author']=[]
self.assertEqual(normalize_row(row,{'conf/aaai/2024':self.volume('aaai',2024)},{'AAAI'})[1],'proceedings_heading')
def test_missing_authors_alone_do_not_remove_real_research_records(self):
self.assertIsNone(material_reason('The Onomastica interlanguage pronunciation lexicon.',[],'conf/interspeech/X95',1995,'main'))
self.assertIsNone(material_reason('Learning Models of Relational Stochastic Processes.',[],'conf/ecml/SanghaiDW05',2005,'main'))
self.assertIsNone(material_reason('Sponsors.', ['A Researcher'],'conf/aaai/research',2024,'main'))
def fixture(self,root,rows):
catalogue=root/'dblp-catalogue.sqlite';volume=self.volume('aaai',2024)
with closing(sqlite3.connect(catalogue)) as c, c:
c.execute('CREATE TABLE proceedings(key TEXT PRIMARY KEY,metadata_json TEXT)')
c.execute('INSERT INTO proceedings VALUES(?,?)',(volume['key'],json.dumps(volume)))
candidates=root/'candidates.jsonl';candidates.write_text(''.join(json.dumps(row)+'\n' for row in rows))
manifest={'snapshot_sha256':XML_SHA,'candidate_sha256':hashlib.sha256(candidates.read_bytes()).hexdigest(),
'candidate_records':len(rows),'record_types':{'proceedings':1,'inproceedings':len(rows)}}
catalogue.with_name('dblp-ai-candidates.manifest.json').write_text(json.dumps(manifest))
return catalogue,candidates
def test_normalized_files_import_with_unknown_abstracts_and_no_invented_retracted_acceptance(self):
with tempfile.TemporaryDirectory() as tmp:
root=Path(tmp);withdrawn=self.row(key='two');withdrawn['publtype']='withdrawn'
catalogue,candidates=self.fixture(root,[self.row(),withdrawn,self.row(book='AAAI Workshops',key='workshop')])
result=collect(catalogue,root/'out',['AAAI'],candidates=candidates)
self.assertEqual(result['normalized_records'],2);self.assertEqual(result['excluded_by_reason']['separate_workshop'],1)
manifest=result['files'][0];self.assertEqual(manifest['missing_abstract_count'],2)
rows=[json.loads(line) for line in Path(manifest['file']).read_text().splitlines()]
self.assertEqual(rows[0]['authors'],['Jane Doe','John Smith']);self.assertIsNone(rows[0]['abstract'])
self.assertEqual(rows[1]['status'],'unknown');self.assertEqual(rows[1]['decision_events'],[])
db=root/'papers.sqlite';ingest_file(db,manifest['file'])
with closing(sqlite3.connect(db)) as c:self.assertEqual(c.execute('SELECT count(*) FROM decisions').fetchone()[0],1)
def test_checksum_failure_and_duplicate_keys_do_not_publish_files(self):
with tempfile.TemporaryDirectory() as tmp:
root=Path(tmp);catalogue,candidates=self.fixture(root,[self.row(),self.row()])
with self.assertRaisesRegex(ValueError,'Repeated DBLP'):collect(catalogue,root/'out',['AAAI'],candidates=candidates)
self.assertFalse(list((root/'out').glob('*.jsonl')))
candidates.write_text('tampered')
with self.assertRaisesRegex(ValueError,'checksum mismatch'):collect(catalogue,root/'out',['AAAI'],candidates=candidates)
def test_case_variants_use_one_conference_edition_and_submission(self):
with tempfile.TemporaryDirectory() as tmp:
store=Store(Path(tmp)/'papers.sqlite');self.addCleanup(store.conn.close)
paper,one=store.record('pmlr','one','https://example.org/one','A paper','Abstract',[],{'doi':'10.1234/one'})
store.decision(paper,one,'CORL',2024,'main','one','https://example.org/one','accepted','Accept',identities=[('publication_doi','10.1234/one')])
same,two=store.record('dblp','two','https://example.org/two','A paper',None,[],{'doi':'10.1234/one'})
store.decision(same,two,'CoRL',2024,'main','two','https://example.org/two','accepted','Published',identities=[('publication_doi','10.1234/one')])
self.assertEqual(store.conn.execute('SELECT count(*) FROM venues').fetchone()[0],1)
self.assertEqual(store.conn.execute('SELECT count(*) FROM submissions').fetchone()[0],1)
def test_production_disk_reserve_stops_before_publishing(self):
with tempfile.TemporaryDirectory() as tmp:
root=Path(tmp);catalogue,candidates=self.fixture(root,[self.row()])
with patch('paper_atlas.collectors.dblp_records.shutil.disk_usage',return_value=SimpleNamespace(free=1024)):
with self.assertRaisesRegex(RuntimeError,'8 GiB'):collect(catalogue,root/'out',['AAAI'],candidates=candidates)
self.assertFalse(list((root/'out').glob('*.jsonl')))
if __name__=='__main__':unittest.main()