import gzip
import hashlib
import json
from pathlib import Path
import sqlite3
import tempfile
import unittest
from paper_atlas.collectors.dblp import catalogue,records
class DblpCatalogueTests(unittest.TestCase):
def setUp(self):
self.tmp=tempfile.TemporaryDirectory();self.addCleanup(self.tmp.cleanup);self.root=Path(self.tmp.name)
self.dtd=self.root/'dblp.dtd';self.dtd.write_bytes(b'')
def archive(self,body,doctype=''):
path=self.root/'dblp.xml.gz'
with gzip.open(path,'wb') as stream:stream.write((''+doctype+''+body+'').encode())
return path
def test_streamed_entities_nested_titles_and_candidate_scope_remain_explicit(self):
path=self.archive('AAAI Workshop proceedings2025RenéLearn deep models2025AAAI Workshopconf/aaai/2025https://doi.org/10.1234/exampleOther2020')
manifest=catalogue(path,self.dtd,self.root/'out',hashlib.sha256(path.read_bytes()).hexdigest(),hashlib.md5(self.dtd.read_bytes()).hexdigest())
self.assertEqual(manifest['records_scanned'],3);self.assertEqual(manifest['candidate_records'],1)
row=json.loads(Path(manifest['file']).read_text())
self.assertEqual(row['fields']['author'],['René']);self.assertEqual(row['fields']['title'],['Learn deep models'])
self.assertNotIn('status',row);self.assertNotIn('venue',row)
with sqlite3.connect(manifest['catalogue']) as connection:
self.assertEqual(connection.execute('SELECT sum(count) FROM volume_counts').fetchone()[0],2)
self.assertEqual(connection.execute('SELECT count(*) FROM proceedings').fetchone()[0],1)
def test_remote_external_dtd_is_rejected_without_fetching(self):
path=self.archive('',doctype='')
with self.assertRaisesRegex(ValueError,'pinned external'):list(records(path,self.dtd.read_bytes()))
def test_real_monthly_dtd_name_and_latin1_encoding(self):
path=self.root/'monthly.xml.gz'
xml='RenéMetadata'
with gzip.open(path,'wb') as stream:stream.write(xml.encode('latin1'))
self.assertEqual(list(records(path,self.dtd.read_bytes()))[0]['fields']['author'],['René'])
def test_multiple_volume_fields_remain_reviewable_without_double_counting(self):
path=self.archive('Joint publication2024First meetingSecond meetingconf/aaai/joint2024')
manifest=catalogue(path,self.dtd,self.root/'out',hashlib.sha256(path.read_bytes()).hexdigest(),hashlib.md5(self.dtd.read_bytes()).hexdigest())
with sqlite3.connect(manifest['catalogue']) as connection:
raw=json.loads(connection.execute('SELECT metadata_json FROM volume_anomalies').fetchone()[0])
self.assertEqual(raw['fields']['booktitle'],['First meeting','Second meeting'])
self.assertEqual(connection.execute('SELECT sum(count) FROM volume_counts').fetchone()[0],1)
def test_internal_entities_are_rejected(self):
path=self.archive('',doctype=']>')
with self.assertRaisesRegex(ValueError,'pinned external'):list(records(path,self.dtd.read_bytes()))
def test_checksum_failure_preserves_existing_output(self):
path=self.archive('');out=self.root/'out';out.mkdir();target=out/'dblp-ai-candidates.jsonl';target.write_text('previous')
with self.assertRaisesRegex(ValueError,'snapshot checksum'):catalogue(path,self.dtd,out,'0'*64)
self.assertEqual(target.read_text(),'previous')
def test_truncated_gzip_cannot_publish_a_catalogue(self):
path=self.archive('One')
path.write_bytes(path.read_bytes()[:-8]);out=self.root/'out'
with self.assertRaises((EOFError,OSError)):
catalogue(path,self.dtd,out,hashlib.sha256(path.read_bytes()).hexdigest(),hashlib.md5(self.dtd.read_bytes()).hexdigest())
self.assertFalse((out/'dblp-ai-candidates.jsonl').exists())
if __name__=='__main__':unittest.main()