Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a925457b0 |
@@ -53,8 +53,10 @@ jobs:
|
||||
subprocess.run([python,'-I','-B','-m','paper_atlas','import','arxiv','--help'],cwd=outside,check=True)
|
||||
subprocess.run([python,'-I','-B','-m','paper_atlas','maintain','backfill','--help'],cwd=outside,check=True)
|
||||
subprocess.run([python,'-I','-B','-m','paper_atlas','audit','public-openreview','--help'],cwd=outside,check=True)
|
||||
subprocess.run([python,'-I','-B','-m','paper_atlas','audit','histories','--help'],cwd=outside,check=True)
|
||||
subprocess.run([python,'-I','-B','-c','from paper_atlas.postgres.writer import PostgresStore, WriteConnection'],cwd=outside,check=True)
|
||||
smoke = "from paper_atlas.db.store import Store; from paper_atlas.config import DEFAULT_REGISTRY; import json; assert {'COLM','MM','MLSys'} <= {v['acronym'] for v in json.loads(DEFAULT_REGISTRY.read_text())['venues']}; s=Store('installed.sqlite'); s.conn.close()"
|
||||
subprocess.run([python,'-I','-B','-c',smoke],cwd=outside,check=True)
|
||||
subprocess.run([python,'-I','-B','-m','paper_atlas','query','--db','installed.sqlite','--stats'],cwd=outside,check=True)
|
||||
subprocess.run([python,'-I','-B','-m','paper_atlas','audit','histories','--db','installed.sqlite','--examples','0'],cwd=outside,check=True)
|
||||
PY
|
||||
|
||||
+21
-5
@@ -79,10 +79,10 @@ paper-atlas maintain merge --db /data/paper-index/samples/example.sqlite \
|
||||
## 4. 查询与分析前检查
|
||||
|
||||
```sh
|
||||
paper-atlas query --db /data/paper-index/papers.sqlite --query '"language model"' --limit 20
|
||||
paper-atlas query --db /data/paper-index/papers.sqlite --tag arxiv:category=cs.AI
|
||||
paper-atlas query --db /data/paper-index/papers.sqlite --venue ICLR --year 2025 --status rejected
|
||||
paper-atlas query --db /data/paper-index/papers.sqlite --paper 123
|
||||
paper-atlas query --pg-service foundry-research --query '"language model"' --limit 20
|
||||
paper-atlas query --pg-service foundry-research --tag arxiv:category=cs.AI
|
||||
paper-atlas query --pg-service foundry-research --venue ICLR --year 2025 --status rejected
|
||||
paper-atlas query --pg-service foundry-research --paper 123
|
||||
paper-atlas coverage --db /data/paper-index/papers.sqlite --out /data/paper-index/collected/coverage.json
|
||||
```
|
||||
|
||||
@@ -91,10 +91,26 @@ paper-atlas coverage --db /data/paper-index/papers.sqlite --out /data/paper-inde
|
||||
例如,查找在 ICLR 有拒稿记录、但在其他已知投稿中已有录用的论文:
|
||||
|
||||
```sh
|
||||
paper-atlas query --db /data/paper-index/papers.sqlite \\
|
||||
paper-atlas query --pg-service foundry-research \
|
||||
--venue ICLR --status rejected --outcome accepted
|
||||
```
|
||||
|
||||
`known_acceptances` 列出目前仍为录用状态的各次投稿及会议、年份、track 和决定证据,`publication_outcome` 汇总为 accepted 或 no_known_acceptance。汇总会检查所有会议和 track,不把某次拒稿覆盖到整篇论文;同一次投稿的后续撤稿也会更新该次状态并保留原决定。no_known_acceptance 只表示现有来源中没有查到录用,不能称为“从未录用”。
|
||||
|
||||
`--stats` 中 papers 是当前规则下的论文实体数;source_records 是来源记录数;decisions 是决定观察数。一个投稿可能有多个来源和多条观察,不能相加当作论文数。
|
||||
|
||||
## 5. 投稿历史关系审计
|
||||
|
||||
```sh
|
||||
PGSERVICEFILE=/etc/paper-atlas/pg_service.conf paper-atlas audit histories \
|
||||
--pg-service foundry-research --pg-schema research --examples 20 \
|
||||
--out /data/paper-index/collected/history-audit.json
|
||||
```
|
||||
|
||||
使用一个只读一致性快照,区分论文实体、投稿和决定观察;分别统计同次投稿曾出现负面与录用观察、不同投稿共存负面与当前已知录用、不同会议之间的共存。workshop/main 是不同 track,属于同一会议时不算跨会议;会议别名也不会增加一个会议。当前已知录用使用与 `query --outcome` 相同的“每次投稿最新观察”规则,后续撤稿的该次投稿不再算当前录用,但旧录用决定仍保留。
|
||||
|
||||
报告不凭观察时间、届次年份或行顺序断言“拒稿发生在录用之前”。样例保留真实 `decision_date`、`observed_at`、原始标签、证据 URL 和决定来源,供进一步核查;日期缺失仍保持未知。`negative` 统计包含 rejected、desk_rejected 和 withdrawn,其原始状态不互相替换。
|
||||
|
||||
总数覆盖快照内全部投稿,样例由 `--examples` 限定为 0–100 篇;每篇最多展示 20 次投稿、每次最多 50 条决定、每条决定最多 100 个来源,并明确标记截断。样例优先保留负面/录用的不同投稿对。四项来源关系检查报告缺失关联和来源论文归属不一致,不自动修复。
|
||||
|
||||
这些统计依赖现有论文身份关联,不证明实体合并正确,也不证明原始决定签名、公开拒稿全集、标签类型或最终全库去重已完成。SQLite 独立样本可使用 `--db`;冻结备份不能代表当前 PostgreSQL 的历史情况。
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
"""Audit linked submission histories without inferring event chronology."""
|
||||
import argparse
|
||||
from collections import Counter
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
|
||||
from paper_atlas.db.backend import database_arguments, open_read_connection
|
||||
from paper_atlas.db.submissions import timestamp
|
||||
from paper_atlas.querying import LATEST_DECISION
|
||||
|
||||
|
||||
ATTEMPTS = '''SELECT s.paper_id,s.id AS submission_id,e.venue_id,v.acronym AS venue,e.year,e.track,
|
||||
count(d.id) AS decision_observations,
|
||||
MAX(CASE WHEN d.status IN ('rejected','desk_rejected','withdrawn') THEN 1 ELSE 0 END)
|
||||
AS historical_negative,
|
||||
MAX(CASE WHEN d.status='accepted' THEN 1 ELSE 0 END) AS historical_acceptance,
|
||||
COALESCE(MAX(CASE WHEN ''' + LATEST_DECISION + ''' THEN d.status END),'unknown')
|
||||
AS latest_observed_status
|
||||
FROM submissions s JOIN editions e ON e.id=s.edition_id JOIN venues v ON v.id=e.venue_id
|
||||
LEFT JOIN decisions d ON d.submission_id=s.id
|
||||
GROUP BY s.paper_id,s.id,e.venue_id,v.acronym,e.year,e.track
|
||||
ORDER BY s.paper_id,e.venue_id,s.id'''
|
||||
|
||||
|
||||
def _paper_counts(attempts):
|
||||
negative = sum(row['historical_negative'] for row in attempts)
|
||||
accepted = sum(row['latest_observed_status'] == 'accepted' for row in attempts)
|
||||
overlap = sum(row['historical_negative'] and row['latest_observed_status'] == 'accepted'
|
||||
for row in attempts)
|
||||
venues = {}
|
||||
for row in attempts:
|
||||
counts = venues.setdefault(row['venue_id'], [0, 0])
|
||||
counts[0] += row['historical_negative']
|
||||
counts[1] += row['latest_observed_status'] == 'accepted'
|
||||
return {
|
||||
'papers_with_submissions': 1,
|
||||
'papers_with_multiple_submissions': int(len(attempts) > 1),
|
||||
'papers_with_historical_negative': int(negative > 0),
|
||||
'papers_with_current_known_acceptance': int(accepted > 0),
|
||||
'papers_with_negative_and_acceptance_in_distinct_attempts': int(negative * accepted > overlap),
|
||||
'papers_with_negative_and_acceptance_in_distinct_venues':
|
||||
int(negative * accepted > sum(n * a for n, a in venues.values())),
|
||||
}
|
||||
|
||||
|
||||
def _example(connection, attempts):
|
||||
# Keep a negative/accepted pair visible even when a paper has many attempts.
|
||||
accepted = [row for row in attempts if row['latest_observed_status'] == 'accepted']
|
||||
first = accepted[0] if accepted else None
|
||||
other_venue = next((row for row in accepted if row['venue_id'] != first['venue_id']), None)
|
||||
pair = None
|
||||
for negative in attempts:
|
||||
if negative['historical_negative'] and first:
|
||||
candidate = first if first['venue_id'] != negative['venue_id'] else other_venue
|
||||
if candidate:
|
||||
pair = (negative, candidate)
|
||||
break
|
||||
if pair is None:
|
||||
for negative in attempts:
|
||||
if negative['historical_negative'] and first:
|
||||
candidate = first if first['submission_id'] != negative['submission_id'] else (
|
||||
accepted[1] if len(accepted) > 1 else None)
|
||||
if candidate:
|
||||
pair = (negative, candidate)
|
||||
break
|
||||
selected = {}
|
||||
for row in list(pair or ()) + attempts:
|
||||
if len(selected) < 20:
|
||||
selected.setdefault(row['submission_id'], dict(row))
|
||||
for row in selected.values():
|
||||
row['decisions'] = []
|
||||
for decision in connection.execute('''SELECT id,status,raw_value,evidence_url,
|
||||
decision_date,observed_at FROM decisions WHERE submission_id=?
|
||||
ORDER BY observed_at DESC,COALESCE(decision_date,observed_at) DESC,id DESC LIMIT 50''',
|
||||
(row['submission_id'],)):
|
||||
entry = dict(decision)
|
||||
sources = [dict(source) for source in connection.execute('''
|
||||
SELECT so.name,r.external_id,ds.source_decision_id FROM decision_sources ds
|
||||
JOIN source_records r ON r.id=ds.source_record_id JOIN sources so ON so.id=r.source_id
|
||||
WHERE ds.decision_id=? ORDER BY so.name,r.external_id LIMIT 101''', (entry['id'],))]
|
||||
entry['sources'] = sources[:100]
|
||||
entry['sources_truncated'] = len(sources) > 100
|
||||
row['decisions'].append(entry)
|
||||
row['decisions_truncated'] = row['decision_observations'] > len(row['decisions'])
|
||||
paper_id = attempts[0]['paper_id']
|
||||
return {'paper_id': paper_id,
|
||||
'title': connection.execute('SELECT title FROM papers WHERE id=?', (paper_id,)).fetchone()[0],
|
||||
'counts': _paper_counts(attempts), 'attempt_count': len(attempts),
|
||||
'attempts': list(selected.values()), 'attempts_truncated': len(attempts) > len(selected),
|
||||
'chronology': 'Not inferred from observed_at, edition year, or row order.'}
|
||||
|
||||
|
||||
def report(db=None, *, pg_service=None, pg_schema=None, examples=20):
|
||||
if not 0 <= examples <= 100:
|
||||
raise ValueError('Examples must be between 0 and 100')
|
||||
started_at = timestamp()
|
||||
connection = open_read_connection(db, pg_service=pg_service, pg_schema=pg_schema)
|
||||
if not pg_service:
|
||||
connection.row_factory = sqlite3.Row
|
||||
try:
|
||||
if pg_service:
|
||||
identity = connection.execute('SELECT current_database(),current_schema()').fetchone()
|
||||
database, schema = identity[0], identity[1]
|
||||
else:
|
||||
database, schema = str(Path(db).resolve()), None
|
||||
names = ['papers_with_submissions', 'papers_with_multiple_submissions',
|
||||
'papers_with_historical_negative', 'papers_with_current_known_acceptance',
|
||||
'papers_with_negative_and_acceptance_in_distinct_attempts',
|
||||
'papers_with_negative_and_acceptance_in_distinct_venues']
|
||||
totals = Counter({key: 0 for key in names})
|
||||
totals.update(submissions=0, decision_observations=0,
|
||||
attempts_with_historical_negative=0,
|
||||
attempts_with_historical_negative_and_acceptance=0)
|
||||
latest = Counter()
|
||||
sample_attempts = []
|
||||
|
||||
def consume(rows):
|
||||
if not rows:
|
||||
return
|
||||
counts = _paper_counts(rows)
|
||||
totals.update(counts)
|
||||
if counts['papers_with_negative_and_acceptance_in_distinct_attempts'] and len(sample_attempts) < examples:
|
||||
sample_attempts.append(rows)
|
||||
|
||||
current = []
|
||||
for raw in connection.execute(ATTEMPTS):
|
||||
row = dict(raw)
|
||||
if current and row['paper_id'] != current[0]['paper_id']:
|
||||
consume(current)
|
||||
current = []
|
||||
current.append(row)
|
||||
totals['submissions'] += 1
|
||||
totals['decision_observations'] += row['decision_observations']
|
||||
totals['attempts_with_historical_negative'] += row['historical_negative']
|
||||
totals['attempts_with_historical_negative_and_acceptance'] += int(
|
||||
row['historical_negative'] and row['historical_acceptance'])
|
||||
latest[row['latest_observed_status']] += 1
|
||||
consume(current)
|
||||
# These are relationship checks, not a substitute for the final entity/FK audit.
|
||||
integrity = {}
|
||||
checks = {
|
||||
'submissions_without_source_links': '''SELECT count(*) FROM submissions s
|
||||
WHERE NOT EXISTS(SELECT 1 FROM submission_sources ss WHERE ss.submission_id=s.id)''',
|
||||
'decisions_without_source_links': '''SELECT count(*) FROM decisions d
|
||||
WHERE NOT EXISTS(SELECT 1 FROM decision_sources ds WHERE ds.decision_id=d.id)''',
|
||||
'submission_source_paper_mismatches': '''SELECT count(*) FROM submission_sources ss
|
||||
JOIN submissions s ON s.id=ss.submission_id JOIN source_records r ON r.id=ss.source_record_id
|
||||
WHERE s.paper_id<>r.paper_id''',
|
||||
'decision_source_paper_mismatches': '''SELECT count(*) FROM decision_sources ds
|
||||
JOIN decisions d ON d.id=ds.decision_id JOIN submissions s ON s.id=d.submission_id
|
||||
JOIN source_records r ON r.id=ds.source_record_id WHERE s.paper_id<>r.paper_id''',
|
||||
}
|
||||
for name, query in checks.items():
|
||||
integrity[name] = connection.execute(query).fetchone()[0]
|
||||
return {'report_started_at': started_at, 'checked_at': timestamp(),
|
||||
'engine': 'postgresql' if pg_service else 'sqlite', 'database': database,
|
||||
'schema': schema, 'pg_service': pg_service,
|
||||
'status': 'observed_history_audit_not_final_dedup', 'counts': dict(totals),
|
||||
'latest_attempt_status_counts': dict(sorted(latest.items())),
|
||||
'evidence_relationship_checks': integrity,
|
||||
'examples': [_example(connection, rows) for rows in sample_attempts],
|
||||
'example_limit': examples,
|
||||
'interpretation': [
|
||||
'Counts use existing canonical paper links, not independently verified real-world identity.',
|
||||
'Negative includes rejected, desk_rejected and withdrawn observations; these remain distinct decisions.',
|
||||
'Known acceptance follows the latest observed decision per attempt, matching query --outcome.',
|
||||
'Distinct attempts/venues require separate submission/venue IDs; aliases do not create new venues.',
|
||||
'Coexisting evidence does not prove rejection happened before acceptance. observed_at is collection time.',
|
||||
'No known acceptance is not proof of never accepted. Source signatures and completeness are not established here.',
|
||||
'Examples are bounded; counts include all attempts and observations in one read-only snapshot.',
|
||||
'This does not perform entity merging, final deduplication, or label namespace/type validation.',
|
||||
]}
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
def main(argv=None, *, prog=None):
|
||||
parser = argparse.ArgumentParser(prog=prog)
|
||||
database_arguments(parser)
|
||||
parser.add_argument('--examples', type=int, default=20)
|
||||
parser.add_argument('--out')
|
||||
args = parser.parse_args(argv)
|
||||
result = report(args.db, pg_service=args.pg_service, pg_schema=args.pg_schema, examples=args.examples)
|
||||
text = json.dumps(result, ensure_ascii=False, indent=2)
|
||||
if args.out:
|
||||
path = Path(args.out)
|
||||
stage = path.with_suffix('.part')
|
||||
stage.write_text(text + '\n')
|
||||
stage.replace(path)
|
||||
else:
|
||||
print(text)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -35,6 +35,7 @@ COMMANDS = {
|
||||
('audit','arxiv'): ('paper_atlas.audits.arxiv', {}, '核验 arXiv 原始编号、重复行与 ZIP 完整性'),
|
||||
('audit','arxiv-tags'): ('paper_atlas.audits.arxiv_tags', {}, '逐记录核验 arXiv 类别及出处完整性'),
|
||||
('audit','public-openreview'): ('paper_atlas.audits.public_openreview', {}, '核对公开负面决定与已有投稿历史'),
|
||||
('audit','histories'): ('paper_atlas.audits.histories', {}, '只读核对跨会议投稿历史及决定来源关系'),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from paper_atlas.audits.histories import report
|
||||
from paper_atlas.db.store import Store
|
||||
|
||||
|
||||
def add_history_fixture(store):
|
||||
def event(work, forum, venue, year, track, status, observed):
|
||||
paper, record = store.record('history_fixture', work + '/' + forum,
|
||||
'https://example.org/' + forum, work + ' title\x00 preserved', 'Abstract', ['Jane Doe'],
|
||||
{'arxiv': work})
|
||||
store.decision(paper, record, venue, year, track, forum,
|
||||
'https://example.org/' + forum + '/' + str(status), status, status or '',
|
||||
identities=[('openreview', forum)], observed_at=observed)
|
||||
return paper
|
||||
|
||||
# The negative observation was collected later: observation order is not chronology.
|
||||
event('2501.00001', 'HistoryA01', 'ICLR', 2024, 'main', 'rejected', '2026-02-01T00:00:00Z')
|
||||
event('2501.00001', 'HistoryA02', 'ICML', 2025, 'main', 'accepted', '2026-01-01T00:00:00Z')
|
||||
event('2501.00002', 'HistoryB01', 'ICLR', 2024, 'main', 'rejected', '2026-01-01T00:00:00Z')
|
||||
event('2501.00002', 'HistoryB01', 'ICLR', 2024, 'main', 'accepted', '2026-02-01T00:00:00Z')
|
||||
event('2501.00003', 'HistoryC01', 'ICLR', 2024, 'main', 'desk_rejected', '2026-01-01T00:00:00Z')
|
||||
event('2501.00003', 'HistoryC02', 'ICLR', 2024, 'workshop_example', 'accepted', '2026-02-01T00:00:00Z')
|
||||
event('2501.00004', 'HistoryD01', 'ICML', 2025, 'main', 'accepted', '2026-01-01T00:00:00Z')
|
||||
event('2501.00004', 'HistoryD01', 'ICML', 2025, 'main', 'withdrawn', '2026-02-01T00:00:00Z')
|
||||
event('2501.00004', 'HistoryD02', 'ICLR', 2024, 'main', 'rejected', '2026-01-01T00:00:00Z')
|
||||
event('2501.00005', 'HistoryE01', 'COLM', 2026, 'main', None, '2026-01-01T00:00:00Z')
|
||||
store.conn.commit()
|
||||
|
||||
|
||||
class HistoryTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.db = Path(self.tmp.name) / 'sample.sqlite'
|
||||
self.store = Store(self.db)
|
||||
self.addCleanup(self.store.conn.close)
|
||||
|
||||
def test_distinct_attempts_venues_and_latest_acceptances_are_not_observation_counts(self):
|
||||
add_history_fixture(self.store)
|
||||
result = report(self.db)
|
||||
self.assertEqual(result['counts'], {
|
||||
'papers_with_submissions': 5, 'papers_with_multiple_submissions': 3,
|
||||
'papers_with_historical_negative': 4, 'papers_with_current_known_acceptance': 3,
|
||||
'papers_with_negative_and_acceptance_in_distinct_attempts': 2,
|
||||
'papers_with_negative_and_acceptance_in_distinct_venues': 1,
|
||||
'submissions': 8, 'decision_observations': 9,
|
||||
'attempts_with_historical_negative': 5,
|
||||
'attempts_with_historical_negative_and_acceptance': 2,
|
||||
})
|
||||
self.assertEqual(result['latest_attempt_status_counts'],
|
||||
{'accepted': 3, 'desk_rejected': 1, 'rejected': 2, 'unknown': 1, 'withdrawn': 1})
|
||||
self.assertEqual(len(result['examples']), 2)
|
||||
first = result['examples'][0]
|
||||
self.assertIn('\x00', first['title'])
|
||||
self.assertEqual({r['venue'] for r in first['attempts']}, {'ICLR', 'ICML'})
|
||||
self.assertTrue(all(r['decisions'][0]['sources'] for r in first['attempts']))
|
||||
self.assertTrue(all(r['decisions'][0]['decision_date'] is None for r in first['attempts']))
|
||||
self.assertIn('Not inferred', first['chronology'])
|
||||
self.assertEqual(result['status'], 'observed_history_audit_not_final_dedup')
|
||||
self.assertTrue(all(v == 0 for v in result['evidence_relationship_checks'].values()))
|
||||
|
||||
def test_missing_and_mislinked_source_evidence_is_reported_without_repair(self):
|
||||
add_history_fixture(self.store)
|
||||
self.store.conn.execute('DELETE FROM decision_sources WHERE decision_id=1')
|
||||
self.store.conn.execute('DELETE FROM submission_sources WHERE submission_id=1')
|
||||
# Record3 belongs to paper2; move it to paper1 to create a real mismatch.
|
||||
self.store.conn.execute('UPDATE source_records SET paper_id=1 WHERE id=3')
|
||||
self.store.conn.commit()
|
||||
result = report(self.db, examples=0)
|
||||
checks = result['evidence_relationship_checks']
|
||||
self.assertEqual(checks['decisions_without_source_links'], 1)
|
||||
self.assertEqual(checks['submissions_without_source_links'], 1)
|
||||
self.assertEqual(checks['submission_source_paper_mismatches'], 1)
|
||||
self.assertEqual(checks['decision_source_paper_mismatches'], 2)
|
||||
self.assertEqual(self.store.conn.execute('SELECT paper_id FROM source_records WHERE id=3').fetchone()[0], 1)
|
||||
|
||||
def test_example_limit_does_not_change_global_counts(self):
|
||||
add_history_fixture(self.store)
|
||||
none, one = report(self.db, examples=0), report(self.db, examples=1)
|
||||
self.assertEqual(none['counts'], one['counts'])
|
||||
self.assertEqual(none['examples'], [])
|
||||
self.assertEqual(len(one['examples']), 1)
|
||||
|
||||
def test_empty_snapshot_and_invalid_limit_do_not_create_missing_database(self):
|
||||
empty = report(self.db)
|
||||
self.assertTrue(all(v == 0 for v in empty['counts'].values()))
|
||||
self.assertEqual(empty['latest_attempt_status_counts'], {})
|
||||
absent = self.db.parent / 'absent.sqlite'
|
||||
with self.assertRaisesRegex(ValueError, 'Examples'):
|
||||
report(absent, examples=-1)
|
||||
with self.assertRaisesRegex(ValueError, 'Existing database'):
|
||||
report(absent)
|
||||
self.assertFalse(absent.exists())
|
||||
|
||||
def test_truncated_example_keeps_the_cross_venue_acceptance_visible(self):
|
||||
paper, record = self.store.record('fixture', 'many', 'https://example.org/many',
|
||||
'Many attempts', 'Abstract', [])
|
||||
for index in range(26):
|
||||
accepted = index == 25
|
||||
forum = f'BoundedForum{index}'
|
||||
self.store.decision(paper, record, 'ICML' if accepted else 'ICLR', 2025, 'main', forum,
|
||||
'https://example.org/' + forum, 'accepted' if accepted else 'rejected',
|
||||
'Accept' if accepted else 'Reject', identities=[('openreview', forum)])
|
||||
self.store.conn.commit()
|
||||
result = report(self.db)
|
||||
example, = result['examples']
|
||||
self.assertEqual((example['attempt_count'], len(example['attempts'])), (26, 20))
|
||||
self.assertTrue(example['attempts_truncated'])
|
||||
self.assertEqual({row['latest_observed_status'] for row in example['attempts']},
|
||||
{'accepted', 'rejected'})
|
||||
self.assertEqual(result['counts']['submissions'], 26)
|
||||
@@ -175,6 +175,22 @@ class PostgreSQLIntegrationTests(unittest.TestCase):
|
||||
finally:
|
||||
reader.close()
|
||||
|
||||
def test_history_audit_matches_sqlite_for_actual_postgresql_snapshot(self):
|
||||
from paper_atlas.audits.histories import report
|
||||
from tests.test_histories import add_history_fixture
|
||||
store = Store(self.db)
|
||||
try:
|
||||
add_history_fixture(store)
|
||||
finally:
|
||||
store.conn.close()
|
||||
expected = report(self.db)
|
||||
self.assertEqual(self.migrate()['status'], 'complete')
|
||||
actual = report(pg_service=self.service, pg_schema=self.target)
|
||||
self.assertEqual(actual['engine'], 'postgresql')
|
||||
self.assertEqual(actual['schema'], self.target)
|
||||
for key in ('counts', 'latest_attempt_status_counts', 'evidence_relationship_checks', 'examples'):
|
||||
self.assertEqual(actual[key], expected[key], key)
|
||||
|
||||
def interrupt_after_first_table(self):
|
||||
calls = 0
|
||||
def interrupt(*args):
|
||||
|
||||
Reference in New Issue
Block a user