160 lines
7.5 KiB
Python
160 lines
7.5 KiB
Python
#!/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-<name>
|
||
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())
|