#!/usr/bin/env python3 """ size_check.py — размерный регресс-тест: сравнение _CODE тестов с эталоном docs/size_baseline.tsv. Использование: make size-check # собрать обязательные тесты и сверить с эталоном make size-baseline # принять свежие размеры, сохранив необязательные Логика: собираем _CODE из свежей .map каждой программы под tests/, сравниваем с эталоном. Карты, оставшиеся от другой цели или более старые, чем входы сборки, пропускаются. Любое отличие печатается; выход 1 — если есть РОСТ (случайное разжирение libc), 0 — если только уменьшения/без изменений. Новые программы (нет в эталоне) — предупреждение, добавить через --update. --update заменяет размеры собранных тестов, сохраняя эталоны необязательных тестов, которые сейчас не собирались. Прямой запуск скрипта рассчитан на уже собранные тесты; Make-цели сами собирают обязательный набор и передают его через --require. ТОЛЬКО tests/, без отдельного репозитория Examples: регресс ловит разжирение libc, а для этого хватает мелких тестов — каждый тянет свой кусок библиотеки и пересобирается за секунды. Крупные приложения (`../Examples/mdview` компилируется минутами) не показывают ничего сверх этого и лишь удлиняют цикл, поэтому и из `make all` они убраны (см. корневой Makefile). """ import argparse import glob import os from pathlib import Path import re ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASELINE = os.path.join(ROOT, "docs", "size_baseline.tsv") def collect(): """Имя программы → размер _CODE (байт) по свежим .map своей цели.""" sizes = {} skipped = set() pats = [ os.path.join(ROOT, "tests", "*", ".sprinter-cc-*", "*.map"), ] for pat in pats: for map_path in glob.glob(pat): name = os.path.splitext(os.path.basename(map_path))[0] # .map должен лежать в своей папке .sprinter-cc- if os.path.basename(os.path.dirname(map_path)) != ".sprinter-cc-" + name: continue project = Path(map_path).parents[1] makefile = project / "Makefile" if not makefile.is_file(): continue declared = re.search(r"^EXAMPLE\s*[:?+]?=\s*(\S+)", makefile.read_text(), re.M) if not declared or declared.group(1) != name: # Например, старый .sprinter-cc-w3bankgfx рядом с w3bgfx. continue flags = makefile.read_text() safe = "--safe" in flags gfx = "--gfx" in flags variant = "_safe" if safe else "" # app.mk сам по себе не является входом линкера: его изменения # учитываются сборкой через sdbg-config и CC_FLAGS. Проверяем # только реальные входы, иначе правка run-цели объявит все карты # устаревшими, хотя размер программы не менялся. dependencies = [makefile, Path(ROOT) / "bin/sprinter-cc", Path(ROOT) / "toolchain/mkexe/mkexe", Path(ROOT) / f"lib/sprinter{variant}.lib"] if gfx: dependencies.append(Path(ROOT) / f"lib/bgi256{variant}.lib") dependencies.extend(project.glob("*.c")) dependencies.extend(project.glob("*.h")) dependencies.extend((Path(ROOT) / "runtime").glob("*.s")) if any(not path.is_file() or os.path.getmtime(map_path) + 1 < path.stat().st_mtime for path in dependencies): skipped.add(name) continue with open(map_path, encoding="utf-8", errors="replace") as f: for line in f: m = re.match(r"^_CODE\s+([0-9A-Fa-f]+)\s+([0-9A-Fa-f]+)", line) if m: sizes[name] = int(m.group(2), 16) break return sizes, skipped def load_baseline(): base = {} if os.path.exists(BASELINE): with open(BASELINE) as f: for line in f: line = line.strip() if not line or line.startswith("#"): continue name, size = line.split("\t") base[name] = int(size) return base def save_baseline(sizes): accepted = load_baseline() accepted.update(sizes) with open(BASELINE, "w") as f: f.write("# Эталон размеров _CODE (байт); обновление: " "make size-baseline\n") for name in sorted(accepted): f.write(f"{name}\t{accepted[name]}\n") def main(): parser = argparse.ArgumentParser(description="Проверка размера _CODE SDK-тестов") parser.add_argument("--update", action="store_true") parser.add_argument("--require", nargs="*", default=[], metavar="TEST") args = parser.parse_args() sizes, skipped = collect() missing = set(args.require) - sizes.keys() if missing: print("size_check: нет свежей карты для обязательных тестов: " + ", ".join(sorted(missing))) return 2 if not sizes: print("size_check: свежие .map не найдены — сначала `make all`") return 2 if args.update: save_baseline(sizes) print(f"size_check: эталон обновлён ({len(sizes)} свежих программ) → " f"{os.path.relpath(BASELINE, ROOT)}") return 0 base = load_baseline() if not base: print("size_check: эталона нет — создать: size_check.py --update") return 2 grew = 0 for name in sorted(sizes): new, old = sizes.get(name), base.get(name) if old is None: print(f" {name}: НОВЫЙ ({new} Б) — добавить в эталон (--update)") elif new != old: mark = "РОСТ" if new > old else "ок, меньше" print(f" {name}: {old} -> {new} ({new - old:+d} Б) [{mark}]") if new > old: grew += 1 if skipped: print(f"size_check: {len(skipped)} устаревших карт пропущено: " + ", ".join(sorted(skipped))) if grew: print(f"size_check: {grew} программ(ы) ВЫРОСЛИ — проверить правки libc") return 1 print(f"size_check: OK ({len(sizes)} программ, роста нет)") return 0 if __name__ == "__main__": raise SystemExit(main())