Отличное форматирование
This commit is contained in:
25
Report/scripts/build.sh
Executable file
25
Report/scripts/build.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPORT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
ROOT="$(cd "$REPORT_DIR/.." && pwd)"
|
||||
|
||||
export JAVA_HOME="${JAVA_HOME:-/usr/lib/jvm/java-21-openjdk}"
|
||||
|
||||
echo "== gen_listings =="
|
||||
python3 "$SCRIPT_DIR/gen_listings.py"
|
||||
|
||||
echo "== gen_test_tables =="
|
||||
python3 "$SCRIPT_DIR/gen_test_tables.py"
|
||||
|
||||
echo "== render_puml =="
|
||||
"$SCRIPT_DIR/render_puml.sh"
|
||||
|
||||
echo "== check_images =="
|
||||
python3 "$SCRIPT_DIR/check_images.py"
|
||||
|
||||
echo "== typst compile =="
|
||||
cd "$REPORT_DIR"
|
||||
typst compile --root "$ROOT" "Пояснительная_записка_ПытковРЕ.typ"
|
||||
|
||||
echo "Done: $REPORT_DIR/Пояснительная_записка_ПытковРЕ.pdf"
|
||||
72
Report/scripts/check_images.py
Normal file → Executable file
72
Report/scripts/check_images.py
Normal file → Executable file
@@ -1,51 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify IMAGES_REGISTRY.md entries exist on disk and match .typ references."""
|
||||
"""Verify Report/images files exist; fail on missing diagram PNGs."""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REQUIRED_READY = {
|
||||
f"fig_{i:02d}_" for i in range(1, 5)
|
||||
} | {f"fig_11_"} | {f"fig_{i:02d}_" for i in range(14, 24)}
|
||||
|
||||
WARN_PLACEHOLDER = {f"fig_{i:02d}_" for i in range(5, 11)} | {f"fig_12_"} | {f"fig_13_"}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
report = Path(__file__).resolve().parent.parent
|
||||
repo = report.parent
|
||||
report = Path(__file__).resolve().parents[1]
|
||||
images = report / "images"
|
||||
registry = images / "IMAGES_REGISTRY.md"
|
||||
typ = report / "Пояснительная_записка_ПытковРЕ.typ"
|
||||
errors = 0
|
||||
text = registry.read_text(encoding="utf-8")
|
||||
errors: list[str] = []
|
||||
warns: list[str] = []
|
||||
|
||||
if not registry.exists():
|
||||
print("Missing IMAGES_REGISTRY.md", file=sys.stderr)
|
||||
return 1
|
||||
for line in text.splitlines():
|
||||
if "|" not in line or "fig_" not in line:
|
||||
continue
|
||||
cols = [c.strip() for c in line.split("|")]
|
||||
if len(cols) < 4:
|
||||
continue
|
||||
fname, status = cols[2], cols[3]
|
||||
if not fname.startswith("fig_"):
|
||||
continue
|
||||
path = images / fname
|
||||
if not path.is_file():
|
||||
errors.append(f"missing file: {fname}")
|
||||
continue
|
||||
if status == "placeholder":
|
||||
for prefix in WARN_PLACEHOLDER:
|
||||
if fname.startswith(prefix):
|
||||
warns.append(f"placeholder: {fname}")
|
||||
break
|
||||
elif status == "ready":
|
||||
for prefix in REQUIRED_READY:
|
||||
if fname.startswith(prefix):
|
||||
if path.stat().st_size < 500:
|
||||
errors.append(f"too small (placeholder?): {fname}")
|
||||
break
|
||||
|
||||
rows = re.findall(
|
||||
r"\|\s*\d+\s*\|\s*([^\|]+?)\s*\|\s*(\w+)\s*\|",
|
||||
registry.read_text(encoding="utf-8"),
|
||||
)
|
||||
rows = [(f.strip(), s.strip()) for f, s in rows if f.startswith("fig_")]
|
||||
for fname, status in rows:
|
||||
p = images / fname
|
||||
if not p.exists():
|
||||
print(f"MISSING file: {fname} (status {status})", file=sys.stderr)
|
||||
errors += 1
|
||||
elif status == "ready" and p.stat().st_size < 1000:
|
||||
print(f"WARN small file: {fname}", file=sys.stderr)
|
||||
|
||||
if typ.exists():
|
||||
text = typ.read_text(encoding="utf-8")
|
||||
for m in re.finditer(r'image\("(?:Report/)?images/([^"]+)"', text):
|
||||
f = images / m.group(1)
|
||||
if not f.exists():
|
||||
print(f"MISSING in typ: images/{m.group(1)}", file=sys.stderr)
|
||||
errors += 1
|
||||
for w in warns:
|
||||
print(f"WARN {w}", file=sys.stderr)
|
||||
for e in errors:
|
||||
print(f"ERROR {e}", file=sys.stderr)
|
||||
|
||||
if errors:
|
||||
print(f"check_images: {errors} error(s)", file=sys.stderr)
|
||||
return 1
|
||||
print("check_images: OK", file=sys.stderr)
|
||||
print(f"OK: images check passed ({len(list(images.glob('fig_*')))} files)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
sys.exit(main())
|
||||
|
||||
@@ -96,6 +96,13 @@ def typst_escape_path(rel: str) -> str:
|
||||
return rel.replace("\\", "/")
|
||||
|
||||
|
||||
def typst_escape_caption(text: str) -> str:
|
||||
"""Экранирование для текста в [...]; без обратных кавычек (inline raw ловит raw.line)."""
|
||||
for ch in "#$\\[]":
|
||||
text = text.replace(ch, "\\" + ch)
|
||||
return text
|
||||
|
||||
|
||||
def write_listing(
|
||||
out_path: Path,
|
||||
rel_from_report: str,
|
||||
@@ -104,19 +111,15 @@ def write_listing(
|
||||
lang: str,
|
||||
) -> None:
|
||||
rel_typ = typst_escape_path(rel_from_report)
|
||||
# Подпись перед кодом: figure.caption(position: top) в теле (placement: top — про float, не порядок).
|
||||
content = (
|
||||
f'#let lst-body-{label} = read("{rel_typ}")\n'
|
||||
f"#figure(\n"
|
||||
f" [\n"
|
||||
f" #figure.caption(position: top)[{caption}]\n"
|
||||
f" #block(breakable: true)[\n"
|
||||
f" #raw(lst-body-{label}, lang: \"{lang}\", block: true)\n"
|
||||
f" ]\n"
|
||||
f" #raw(read(\"{rel_typ}\"), lang: \"{lang}\", block: true, tab-size: 4, align: left)\n"
|
||||
f" ],\n"
|
||||
f" supplement: [Листинг],\n"
|
||||
f" gap: 0.4em,\n"
|
||||
f") <lst-{label}>\n\n"
|
||||
f") <lst-{label}>\n"
|
||||
f"#pagebreak(weak: true)\n\n"
|
||||
)
|
||||
out_path.write_text(content, encoding="utf-8")
|
||||
|
||||
@@ -159,7 +162,7 @@ def main() -> int:
|
||||
rel_report_str = rel_report.as_posix()
|
||||
hid = path_hash(rel_repo)
|
||||
label = hid
|
||||
cap = f"Исходный файл `{rel_repo}`"
|
||||
cap = f"Исходный файл {typst_escape_caption(rel_repo)}"
|
||||
lang = lang_for(f, ext_map, name_map)
|
||||
listing_name = f"listing-{hid}.typ"
|
||||
listing_path = generated / listing_name
|
||||
@@ -182,9 +185,13 @@ def main() -> int:
|
||||
ix.write(f"// Generated listings: {len(files)} files\n\n")
|
||||
|
||||
appendix = generated / "appendix-a.typ"
|
||||
styles_path = report_dir / "includes" / "listings-appendix.typ"
|
||||
styles = styles_path.read_text(encoding="utf-8")
|
||||
with appendix.open("w", encoding="utf-8") as ap:
|
||||
ap.write("// AUTO-GENERATED by gen_listings.py — do not edit\n\n")
|
||||
ap.write("#set figure(gap: 0.4em)\n\n")
|
||||
ap.write("// Стили листингов (вставлены напрямую: #include не распространяет show-правила)\n")
|
||||
ap.write(styles)
|
||||
ap.write("\n")
|
||||
for mi in module_includes:
|
||||
ap.write(f'#include "{mi}"\n')
|
||||
|
||||
|
||||
@@ -1,95 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate Typst tables of unit tests for chapter 5."""
|
||||
"""Generate a single Typst unit-test table for chapter 5."""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
yaml = None # type: ignore
|
||||
|
||||
|
||||
def typst_escape(s: str) -> str:
|
||||
return s.replace("\\", "\\\\").replace("`", "\\`")
|
||||
|
||||
|
||||
def emit_table(
|
||||
lines: list[str],
|
||||
caption: str,
|
||||
ncol: int,
|
||||
headers: list[str],
|
||||
data_rows: list[list[str]],
|
||||
label: str,
|
||||
) -> None:
|
||||
lines.append("#pz-test-table(\n")
|
||||
lines.append(f" [{caption}],\n")
|
||||
lines.append(f" {ncol},\n")
|
||||
lines.append(" table.header(\n")
|
||||
for h in headers:
|
||||
lines.append(f" [{typst_escape(h)}],\n")
|
||||
lines.append(" ),\n")
|
||||
for row in data_rows:
|
||||
cells = ", ".join(f"[{typst_escape(c)}]" for c in row)
|
||||
lines.append(f" {cells},\n")
|
||||
lines.append(f") <{label}>\n\n")
|
||||
def load_overrides(script_dir: Path) -> dict[str, str]:
|
||||
path = script_dir / "test_descriptions.yaml"
|
||||
if not path.is_file() or yaml is None:
|
||||
return {}
|
||||
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
||||
return {str(k): str(v) for k, v in data.items()}
|
||||
|
||||
|
||||
def _split_camel(name: str) -> list[str]:
|
||||
parts: list[str] = []
|
||||
buf: list[str] = []
|
||||
for ch in name:
|
||||
if ch.isupper() and buf and buf[-1].islower():
|
||||
parts.append("".join(buf))
|
||||
buf = [ch]
|
||||
else:
|
||||
buf.append(ch)
|
||||
if buf:
|
||||
parts.append("".join(buf))
|
||||
return parts
|
||||
|
||||
|
||||
def describe_method(name: str, overrides: dict[str, str]) -> str:
|
||||
if name in overrides:
|
||||
return overrides[name]
|
||||
low = name.lower()
|
||||
if "encryption" in low and "wrong" in low:
|
||||
return "дешифрование с неверным ключом завершается ошибкой"
|
||||
if "encryption" in low and "same" in low:
|
||||
return "симметрия шифрования и дешифрования при верном ключе"
|
||||
if "correct key" in low:
|
||||
return "верный ключ проходит проверку checkKey"
|
||||
if "incorrect key" in low:
|
||||
return "неверный ключ не проходит проверку checkKey"
|
||||
if name.startswith("maps"):
|
||||
return "исключение преобразуется в типизированную ошибку Wallenc"
|
||||
if name.startswith("syncGroup"):
|
||||
rest = _split_camel(name[9:])
|
||||
return "синхронизация группы: " + " ".join(w.lower() for w in rest)
|
||||
if name.startswith("sync"):
|
||||
return "сценарий синхронизации: " + " ".join(w.lower() for w in _split_camel(name[4:]))
|
||||
if "Totp" in name or "totp" in name or "Otp" in name or "otp" in name:
|
||||
return "корректность TOTP/OTP: " + " ".join(w.lower() for w in _split_camel(name))
|
||||
if name.endswith("Works") or "Crud" in name:
|
||||
return "CRUD-операции и сохранение данных"
|
||||
if "parses" in low or "rejects" in low:
|
||||
return "разбор и валидация входных данных"
|
||||
if "Route" in name or "Intent" in name or "mapsTo" in name:
|
||||
return "маршрутизация, deep link или подписи UI"
|
||||
if "enqueue" in low or "cancel" in low or "fail" in low or "progress" in low:
|
||||
return "жизненный цикл фоновой задачи"
|
||||
words = _split_camel(name)
|
||||
return " ".join(w.lower() for w in words)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
out = Path(__file__).resolve().parents[1] / "includes" / "ch05-tests-generated.typ"
|
||||
overrides = load_overrides(script_dir)
|
||||
rows: list[tuple[str, str, str, str]] = []
|
||||
|
||||
for p in sorted(root.rglob("*.kt")):
|
||||
if "/src/test/" not in p.as_posix():
|
||||
continue
|
||||
text = p.read_text(encoding="utf-8", errors="replace")
|
||||
cls_m = re.search(r"class\s+(\w+)", text)
|
||||
if not cls_m:
|
||||
continue
|
||||
cls = cls_m.group(1)
|
||||
mod = p.parts[p.parts.index("src") - 1]
|
||||
rel = p.relative_to(root).as_posix()
|
||||
for m in re.finditer(r"@Test[\s\S]*?fun\s+(?:`([^`]+)`|(\w+))\s*\(", text):
|
||||
name = m.group(1) or m.group(2)
|
||||
rows.append((mod, cls, name, rel))
|
||||
desc = describe_method(name, overrides)
|
||||
rows.append((mod, name, desc))
|
||||
|
||||
by_mod: dict[str, list] = defaultdict(list)
|
||||
for mod, cls, name, rel in rows:
|
||||
by_mod[mod].append((cls, name, rel))
|
||||
rows.sort(key=lambda r: (r[0], r[1]))
|
||||
|
||||
lines = [
|
||||
"// AUTO-GENERATED by gen_test_tables.py — include from ch05.typ\n",
|
||||
'#import "common.typ": pz-test-table\n\n',
|
||||
]
|
||||
|
||||
summary_rows = []
|
||||
for mod in sorted(by_mod):
|
||||
for cls, name, rel in by_mod[mod]:
|
||||
short = rel.split("/")[-1]
|
||||
summary_rows.append([mod, cls, name, short])
|
||||
data_rows = []
|
||||
for i, (mod, name, desc) in enumerate(rows, start=1):
|
||||
data_rows.append([str(i), mod, name, desc])
|
||||
|
||||
emit_table(
|
||||
lines,
|
||||
"Сводка модульных unit-тестов (src/test)",
|
||||
4,
|
||||
["Модуль", "Класс", "Метод", "Файл"],
|
||||
summary_rows,
|
||||
"tbl-unit-all",
|
||||
)
|
||||
|
||||
for mod in sorted(by_mod):
|
||||
safe = mod.replace("-", "_")
|
||||
lines.append(f"=== Реестр тестов модуля :{mod}\n\n")
|
||||
mod_rows = [
|
||||
[cls, name, rel.split("/")[-1]]
|
||||
for cls, name, rel in by_mod[mod]
|
||||
]
|
||||
emit_table(
|
||||
lines,
|
||||
f"Unit-тесты модуля :{mod}",
|
||||
3,
|
||||
["Класс", "Метод", "Файл"],
|
||||
mod_rows,
|
||||
f"tbl-unit-{safe}",
|
||||
)
|
||||
lines.append("#pz-test-table(\n")
|
||||
lines.append(" [Реестр модульных unit-тестов],\n")
|
||||
lines.append(" 4,\n")
|
||||
lines.append(" table.header(\n")
|
||||
for h in ["№", "Модуль", "Метод", "Проверяемое поведение"]:
|
||||
lines.append(f" [{typst_escape(h)}],\n")
|
||||
lines.append(" ),\n")
|
||||
for row in data_rows:
|
||||
cells = ", ".join(f"[{typst_escape(c)}]" for c in row)
|
||||
lines.append(f" {cells},\n")
|
||||
lines.append(") <tbl-unit-all>\n\n")
|
||||
|
||||
out.write_text("".join(lines), encoding="utf-8")
|
||||
print(f"Wrote {out} ({len(rows)} tests)", flush=True)
|
||||
|
||||
40
Report/scripts/render_puml.sh
Executable file
40
Report/scripts/render_puml.sh
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
# Render Report/puml/fig_*.puml → Report/images/fig_*.png (имя PNG = @startuml в файле).
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPORT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
PUML_DIR="$REPORT_DIR/puml"
|
||||
IMG_DIR="$REPORT_DIR/images"
|
||||
|
||||
mkdir -p "$IMG_DIR"
|
||||
|
||||
if [[ -z "${JAVA_HOME:-}" ]]; then
|
||||
for j in /usr/lib/jvm/java-21-openjdk /usr/lib/jvm/java-17-openjdk; do
|
||||
if [[ -x "$j/bin/java" ]]; then
|
||||
export JAVA_HOME="$j"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
export PATH="${JAVA_HOME:+$JAVA_HOME/bin:}$PATH"
|
||||
|
||||
JAR="${PLANTUML_JAR:-/usr/share/java/plantuml/plantuml.jar}"
|
||||
JAVA_BIN="${JAVA_HOME:+$JAVA_HOME/bin/}java"
|
||||
if [[ ! -f "$JAR" ]]; then
|
||||
echo "plantuml.jar not found at $JAR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
shopt -s nullglob
|
||||
PUML_FILES=("$PUML_DIR"/fig_*.puml)
|
||||
if ((${#PUML_FILES[@]} == 0)); then
|
||||
echo "No fig_*.puml in $PUML_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export PLANTUML_LIMIT_SIZE=8192
|
||||
"$JAVA_BIN" -Djava.awt.headless=true -jar "$JAR" \
|
||||
-charset UTF-8 -tpng -o "$IMG_DIR" "${PUML_FILES[@]}"
|
||||
|
||||
echo "Done. $(ls -1 "$IMG_DIR"/fig_*.png 2>/dev/null | wc -l) PNG in images/"
|
||||
23
Report/scripts/test_descriptions.yaml
Normal file
23
Report/scripts/test_descriptions.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
# Ручные описания для неочевидных имён тестов (ключ — имя метода)
|
||||
syncGroupCopiesFileFromSourceToTarget: копирование файла с источника на целевое хранилище в группе
|
||||
syncGroupSkippedWhenFewerThanTwoStorages: синхронизация пропускается, если в группе меньше двух хранилищ
|
||||
syncGroupDeleteRemovesFileOnTarget: удаление файла на целевом хранилище при синхронизации
|
||||
syncSkipsWhenTargetRevisionAlreadyWinner: пропуск синхронизации, если ревизия цели уже новее
|
||||
openReadDoesNotChangeJournal: чтение без записи не изменяет журнал синхронизации
|
||||
deleteWithRecordSyncJournalFalseDoesNotBumpSequence: удаление без записи в журнал не увеличивает sequence
|
||||
syncGroupTrashSoftDeletesOnTarget: мягкое удаление (trash) на целевом хранилище
|
||||
syncGroupStopsWhenLockCannotBeAcquired: остановка при невозможности захватить блокировку группы
|
||||
syncGroupReleasesLocksAfterSuccessfulSync: снятие блокировок после успешной синхронизации
|
||||
syncGroupReleasesLocksWhenJournalReadFails: снятие блокировок при ошибке чтения журнала
|
||||
syncGroupCooperativeCancellationReleasesLocks: снятие блокировок при отмене задачи пользователем
|
||||
syncGroupReleasesLocksWhenJournalEmpty: снятие блокировок при пустом журнале
|
||||
mergeKeepsSingleEntryPerPath: слияние журнала оставляет одну запись на путь
|
||||
isSyncableUserPathExcludesEncDirAndJournal: пользовательский путь исключает служебные каталоги
|
||||
storageWithoutEncInfoIsCompatible: хранилище без метаданных шифрования совместимо с синхронизацией
|
||||
storageWithEncInfoIsIncompatible: хранилище с шифрованием несовместимо в одной группе sync
|
||||
flushRestoresPendingOnWriteFailure: откат буфера журнала при сбое записи
|
||||
diskInfoParsesResponse: разбор ответа API diskInfo
|
||||
listReturnsEmptyEmbeddedOn404: пустой список при HTTP 404
|
||||
diskInfoThrowsAuthExceptionOn401: AuthException при HTTP 401
|
||||
preservesWallencException: сохранение уже типизированного WallencException
|
||||
startTestTaskEnqueuesWork: постановка тестовой задачи в очередь orchestrator
|
||||
Reference in New Issue
Block a user