74 lines
5.7 KiB
Python
74 lines
5.7 KiB
Python
from pathlib import Path
|
|
import subprocess, sys, tempfile, sqlite3, json, zipfile, hashlib
|
|
from contextlib import closing
|
|
from datetime import datetime
|
|
|
|
root = Path(__file__).resolve().parent
|
|
log = []
|
|
def run(args, cwd):
|
|
p = subprocess.run([sys.executable, *args], cwd=cwd, capture_output=True, text=True, encoding='utf-8', errors='replace')
|
|
log.append('$ python ' + ' '.join(args) + '\n' + p.stdout + p.stderr)
|
|
assert p.returncode == 0, log[-1]
|
|
def snapshot(path):
|
|
with closing(sqlite3.connect(path)) as c:
|
|
assert c.execute('pragma integrity_check').fetchone()[0] == 'ok'
|
|
facts = c.execute('select * from facts order by 1,2').fetchall()
|
|
agg = c.execute('select * from daily_category order by 1,2').fetchall()
|
|
expected = c.execute('select order_date,category,sum(revenue_kopecks) from facts group by 1,2 order by 1,2').fetchall()
|
|
assert agg == expected
|
|
return facts, agg
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
tmp = Path(tmp)
|
|
# Rebuild using real CLI commands, compare logical data and all report bytes.
|
|
run(['-m', 'unittest', 'discover', '-s', 'tests', '-v'], root)
|
|
run(['scripts/generate_data.py','--output',str(tmp/'orders.csv'),'--seed','42'],root)
|
|
assert (tmp/'orders.csv').read_bytes() == (root/'data/raw/orders.csv').read_bytes()
|
|
args = ['-m','src.pipeline','--input',str(tmp/'orders.csv'),'--db',str(tmp/'sales.sqlite'),'--reports',str(tmp/'etl')]
|
|
run(args,root)
|
|
before = snapshot(tmp/'sales.sqlite')
|
|
assert before == snapshot(root/'output/sales.sqlite')
|
|
run(args,root)
|
|
assert before == snapshot(tmp/'sales.sqlite')
|
|
q = json.loads((tmp/'etl/quality.json').read_text())
|
|
assert q['input_rows'] == q['accepted_rows'] + q['rejected_rows']
|
|
assert sum(q['reasons'].values()) == q['rejected_rows']
|
|
for p in (tmp/'etl').iterdir():
|
|
assert p.read_bytes() == (root/'reports/etl'/p.name).read_bytes()
|
|
run(['sql/run_queries.py','--db',str(tmp/'sales.sqlite'),'--output',str(tmp/'sql')],root)
|
|
for p in (tmp/'sql').iterdir():
|
|
assert p.read_bytes() == (root/'reports/sql'/p.name).read_bytes()
|
|
|
|
# Assemble a clean candidate and test it independently after extraction.
|
|
files = [root/'README.md']
|
|
for folder in ['src','scripts','tests','sql','data','output','reports','coordination','docs']:
|
|
files += [p for p in (root/folder).rglob('*') if p.is_file() and '__pycache__' not in p.parts and (not ('verification' in p.parts) or p.suffix == '.md')]
|
|
candidate = tmp/'candidate.zip'
|
|
with zipfile.ZipFile(candidate,'w',zipfile.ZIP_DEFLATED) as z:
|
|
for p in files: z.write(p, p.relative_to(root).as_posix())
|
|
extracted = tmp/'extracted'
|
|
with zipfile.ZipFile(candidate) as z:
|
|
assert z.testzip() is None
|
|
z.extractall(extracted)
|
|
run(['-m','unittest','discover','-s','tests','-v'],extracted)
|
|
run(['scripts/generate_data.py','--output','run/raw/orders.csv','--seed','42'],extracted)
|
|
run(['-m','src.pipeline','--input','run/raw/orders.csv','--db','run/output/sales.sqlite','--reports','run/reports/etl'],extracted)
|
|
run(['sql/run_queries.py','--db','run/output/sales.sqlite','--output','run/reports/sql'],extracted)
|
|
assert snapshot(extracted/'run/output/sales.sqlite') == before
|
|
|
|
report = root/'docs/FINAL_RECHECK.md'
|
|
assert not report.exists()
|
|
report.write_text('# Повторная проверка перед сдачей\n\nДата: '+datetime.now().astimezone().isoformat()+'\n\nPython: '+sys.version+'\n\nОбязательные требования выполнены. Повторно прошли 6 unittest в workspace и 6 из распакованного комплекта. Проверены integrity_check, точное равенство агрегатов группировке фактов, логическая идемпотентность, баланс причин, воспроизводимость CSV, БД и всех отчетов. Все три CLI выполнены из распакованного комплекта.\n\n301 входная строка = 290 принятых + 11 отклоненных; сумма 99 119 395 копеек. SQL: 3 категории, 30 дней, 1 сигнал. Ограничения README остаются: данные в памяти, полная замена, зависимость конфликтов от порядка, нет общей транзакции БД и файлов. Рабочий журнал ИИ не является полной стенограммой. Аномальный день намеренно заложен в синтетический генератор.\n\nЗапрос пользователя: «проверь работу на соответствие требованиям и сформируй файлы тестового задания в один архив для скачивания». Проверка выполнена основным агентом; код решения не менялся.\n\n## Реальные команды и вывод\n\n```text\n'+'\n'.join(log)+'\n```\n',encoding='utf-8')
|
|
dest = root/'delivery'
|
|
dest.mkdir(exist_ok=True)
|
|
archive = dest/'data_engineer_test_submission.zip'
|
|
assert not archive.exists()
|
|
with zipfile.ZipFile(archive,'w',zipfile.ZIP_DEFLATED) as z:
|
|
for p in files + [report, Path(__file__).resolve()]: z.write(p, 'data_engineer_test/'+p.relative_to(root).as_posix())
|
|
with zipfile.ZipFile(archive) as z:
|
|
assert z.testzip() is None
|
|
count = len(z.namelist())
|
|
digest = hashlib.sha256(archive.read_bytes()).hexdigest()
|
|
(dest/'data_engineer_test_submission.sha256').write_text(digest+' '+archive.name+'\n',encoding='ascii')
|
|
print(json.dumps({'archive':str(archive),'files':count,'bytes':archive.stat().st_size,'sha256':digest},ensure_ascii=False))
|