diff --git a/Docs/test_result.md b/Docs/test_result.md new file mode 100644 index 0000000..8e6fe79 --- /dev/null +++ b/Docs/test_result.md @@ -0,0 +1,35 @@ +## original set.c + +| command | average_seconds | run1_seconds | run2_seconds | run3_seconds | exit_status | +| :-------------------------- | :-------------: | :----------: | :----------: | :----------: | ----------: | +| -s check | 1.570747 | 1.621496 | 1.545001 | 1.545744 | 0 | +| -s autoremove | 2.294502 | 2.312574 | 2.287133 | 2.283798 | 0 | +| -s install rpm-build | 2.108121 | 2.142784 | 2.093474 | 2.088105 | 0 | +| -s install openuds-server | 8.028098 | 8.147214 | 8.001140 | 7.935940 | 0 | +| -s install password-store | 3.373377 | 3.396943 | 3.361290 | 3.361899 | 0 | +| --enable-upgrade -s upgrade | 33.915979 | 33.895262 | 33.770192 | 34.082484 | 100 | +| -s dist-upgrade | 88.722136 | 95.484250 | 85.808085 | 84.874073 | 100 | + +## original set.c w/out optimizations + +| command | average_seconds | run1_seconds | run2_seconds | run3_seconds | exit_status | +| :-------------------------- | :-------------: | :----------: | :----------: | :----------: | ----------: | +| -s check | 4.217343 | 4.310882 | 4.179343 | 4.161804 | 0 | +| -s autoremove | 4.884413 | 4.876135 | 4.882347 | 4.894757 | 0 | +| -s install rpm-build | 4.634789 | 4.652499 | 4.623077 | 4.628792 | 0 | +| -s install openuds-server | 12.704538 | 12.640266 | 12.975482 | 12.497867 | 0 | +| -s install password-store | 8.560463 | 8.561515 | 8.557596 | 8.562277 | 0 | +| --enable-upgrade -s upgrade | 36.271838 | 35.511443 | 35.908641 | 37.395430 | 100 | +| -s dist-upgrade | 90.550086 | 94.702198 | 92.174160 | 84.773901 | 100 | + +## set9.c + +| command | average_seconds | run1_seconds | run2_seconds | run3_seconds | exit_status | +| :-------------------------- | :-------------: | :----------: | :----------: | :----------: | ----------: | +| -s check | 1.550342 | 1.596307 | 1.532768 | 1.521950 | 0 | +| -s autoremove | 2.200188 | 2.237645 | 2.185378 | 2.177540 | 0 | +| -s install rpm-build | 2.031055 | 2.045757 | 2.023173 | 2.024234 | 0 | +| -s install openuds-server | 7.580058 | 7.572994 | 7.582557 | 7.584622 | 0 | +| -s install password-store | 3.337012 | 3.367696 | 3.309826 | 3.333514 | 0 | +| --enable-upgrade -s upgrade | 31.494775 | 31.535714 | 31.585522 | 31.363090 | 100 | +| -s dist-upgrade | 85.326580 | 85.389775 | 85.604566 | 84.985398 | 100 | diff --git a/README.md b/README.md index 397d2d2..6720136 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,111 @@ Research on ALT Linux RPM set.c and dependency comparison using encoded symbol sets. - [Docs about current set:version implementation](Docs/set:version.md) + +# Compare results + +| command | original set.c | orig set (w/out optim) | set9.c | +| :-------------------------- | :------------: | :--------------------: | :----: | +| -s check | x1 | x3.134 | x1.041 | +| -s autoremove | x1 | x2.564 | x1.053 | +| -s install rpm-build | x1 | x2.578 | x1.036 | +| -s install openuds-server | x1 | x1.946 | x1.094 | +| -s install password-store | x1 | x2.941 | x1.073 | +| --enable-upgrade -s upgrade | x1 | x1.351 | x1.100 | +| -s dist-upgrade | x1 | x1.204 | x1.044 | + +Более подробные данные сравнения приведены [здесь](Docs/test_result.md) + +# Testing + +Для проверки используются четыре скрипта из каталога `scripts`. + +## Случайные наборы символов + +### `scripts/rpmsetinit/compare_random_sets.py` + +Проверяет совместимость кодировщиков: компилирует `set.c` и +`reimplement/newset.c`, генерирует случайные списки символов и сравнивает +полученные set-строки при случайном `bpp` от 10 до 32. + +Пути до наобходимых файлов приведены в коде скрипта. +Минимальный запуск из корня репозитория: + +```sh +python3 scripts/rpmsetinit/compare_random_sets.py +``` + +Нужны Python 3 и C-компилятор `cc`. Скрипт работает непрерывно до `Ctrl-C`; +входы, на которых результаты различаются, сохраняются в +`scripts/rpmsetinit/error/`. + +### `scripts/rpmsetcmp/compare_random_sets.py` + +Проверяет совместимость `rpmsetcmp()` из `set.c` и +`reimplement/newset.c`. Скрипт генерирует равные, вложенные, несравнимые и +некорректные множества, проверяет оба направления сравнения и сопоставляет +коды результатов двух реализаций. + +Пути до наобходимых файлов приведены в коде скрипта. +Минимальный запуск: + +```sh +python3 scripts/rpmsetcmp/compare_random_sets.py +``` + +Нужны Python 3 и `cc`. Тест также работает до `Ctrl-C`, а найденные +расхождения сохраняет в `scripts/rpmsetcmp/error/`. + +## Проверка зависимостей Sisyphus + +### `scripts/rpmsetcmp/check_sisyphus_set_relations.sh` + +Скрипт загружает в изолированный временный APT-каталог метаданные Sisyphus, +извлекает set:version `Provides` и `Requires`, компилирует компаратор из +`reimplement/newset.c` и проверяет, что требования пакетов удовлетворяются +хотя бы одним провайдером. По умолчанию проверяются первые 100 уникальных +требований; лимит задаётся переменной `REQUIRE_LIMIT` в начале файла. +`REQUIRE_LIMIT=0` для снятия лимита + +Минимальный запуск на ALT Linux с доступом к зеркалу Sisyphus: + +```sh +bash scripts/rpmsetcmp/check_sisyphus_set_relations.sh +``` + +Нужны `apt-get`, `apt-cache`, `cc`. В конце выводятся числа совместимых требований, +требований без провайдера, несовместимых пар и ошибок декодирования. + +## Сравнение производительности + +### `scripts/run-setc-bench.sh` + +Скрипт берёт все `*.c` из `~/setc`, для каждого варианта собирает отдельные +`rpm-build` и `librpm7` в hasher, подставляя файл как `lib/set.c`. Затем он +запускает с этой `librpm` одинаковый набор симуляций APT (`check`, +`autoremove`, `install`, `upgrade`, `dist-upgrade`), записывает коды возврата и +время трёх прогонов в TSV. При включённых `COLLECT_PERF` и +`PERF_RECORD` дополнительно создаются результаты `perf stat`, `perf record`, +`perf report` и `perf annotate`. + +Минимальная подготовка: + +1. Запускать на ALT Linux p11 с настроенными `gear-hsh`/`hasher` и разрешённым + монтированием `/proc`. +2. Скопировать сравниваемые реализации в `~/setc/`, по одному файлу `*.c` на + вариант. +3. Проверить настройки в начале скрипта: `PACKAGER`, `APT_SOURCE`, `CPU` и + каталоги `SETC_DIR`, `WORK_ROOT`, `RESULT_DIR`. +4. Для запуска без perf установить `COLLECT_PERF=0`; для профилирования + оставить `COLLECT_PERF=1`, установить пакет `perf` и разрешить доступ к + счётчикам производительности. + +Запуск: + +```sh +bash scripts/run-setc-bench.sh +``` + +По умолчанию рабочие каталоги создаются в `~/setc-bench`, а итоговые TSV и +perf-артефакты — в `~/res`. Первый запуск скачивает исходники и собирает RPM, +поэтому занимает значительно больше времени, чем случайные проверки. diff --git a/other/extract_alt_set_symbols.py b/other/extract_alt_set_symbols.py deleted file mode 100755 index 83ef5ef..0000000 --- a/other/extract_alt_set_symbols.py +++ /dev/null @@ -1,384 +0,0 @@ -#!/usr/bin/env python3 -"""Extract ALT Linux set:version strings and optional ELF nm symbol stats. - -The script uses the public rdb.altlinux.org API to read package dependency -metadata. With --download-nm it also downloads binary RPMs, extracts ELF files -with bsdtar, and runs nm on dynamic symbols. -""" - -from __future__ import annotations - -import argparse -import json -import os -import re -import shutil -import statistics -import subprocess -import sys -import tempfile -import urllib.error -import urllib.parse -import urllib.request -from collections import Counter -from dataclasses import dataclass -from pathlib import Path -from typing import Iterable - -API_BASE = "https://rdb.altlinux.org/api" -DEFAULT_PACKAGES = [ - "glibc-core", - "libcrypto3", - "libssl3", - "zlib", - "libgcc1", - "libcurl", - "libsystemd", - "libqt6-core", - "libgtk+3", - "libsqlite3", - "libxml2", - "libX11", - "libxcb", - "coreutils", - "curl", - "openssl", - "systemd", - "python3-base", -] - -SET_PREFIX = "set:" -SET_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" -IDENTISH_SYMBOL_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(?:@@?[A-Za-z0-9_.]+)?$") - - -@dataclass(frozen=True) -class SetDependency: - package: str - dep_type: str - name: str - set_string: str - - @property - def payload(self) -> str: - return self.set_string[len(SET_PREFIX) :] - - @property - def bpp(self) -> int | None: - return set_char_to_int(self.payload[0]) if self.payload else None - - @property - def mshift(self) -> int | None: - return set_char_to_int(self.payload[1]) if len(self.payload) > 1 else None - - -@dataclass -class NmReport: - package: str - mode: str - files_seen: int - symbols: list[str] - file_examples: list[str] - - -def set_char_to_int(char: str) -> int | None: - try: - return SET_ALPHABET.index(char) - except ValueError: - return None - - -def api_json(path: str, params: dict[str, object] | None = None) -> dict: - url = API_BASE + path - if params: - url += "?" + urllib.parse.urlencode(params, doseq=True) - req = urllib.request.Request(url, headers={"User-Agent": "arsv-set-symbol-extractor/1.0"}) - with urllib.request.urlopen(req, timeout=60) as response: - return json.load(response) - - -def get_pkghash(package: str, branch: str, arch: str) -> str: - data = api_json( - "/site/pkghash_by_binary_name", - {"branch": branch, "name": package, "arch": arch}, - ) - return str(data["pkghash"]) - - -def get_set_dependencies(package: str, branch: str, arch: str) -> tuple[str, list[SetDependency]]: - pkghash = get_pkghash(package, branch, arch) - deps = api_json(f"/dependencies/binary_package_dependencies/{pkghash}")["dependencies"] - set_deps = [] - for dep in deps: - version = dep.get("version") or "" - if version.startswith(SET_PREFIX): - set_deps.append( - SetDependency( - package=package, - dep_type=dep.get("type", ""), - name=dep.get("name", ""), - set_string=version, - ) - ) - return pkghash, set_deps - - -def package_download_url(pkghash: str, branch: str, arch: str) -> str: - data = api_json( - f"/site/package_downloads_bin/{pkghash}", - {"branch": branch, "arch": arch}, - ) - downloads = data.get("downloads") or [] - if not downloads or not downloads[0].get("packages"): - raise RuntimeError(f"no download URL for pkghash={pkghash}") - return downloads[0]["packages"][0]["url"] - - -def download_file(url: str, destination: Path) -> None: - req = urllib.request.Request(url, headers={"User-Agent": "arsv-set-symbol-extractor/1.0"}) - with urllib.request.urlopen(req, timeout=120) as response, destination.open("wb") as out: - shutil.copyfileobj(response, out) - - -def run_text(command: list[str], cwd: Path | None = None, check: bool = True) -> str: - proc = subprocess.run(command, cwd=cwd, text=True, capture_output=True) - if check and proc.returncode != 0: - joined = " ".join(command) - raise RuntimeError(f"{joined} failed with {proc.returncode}: {proc.stderr.strip()}") - return proc.stdout - - -def rpm_members(rpm_path: Path) -> list[str]: - return [line for line in run_text(["bsdtar", "-tf", str(rpm_path)]).splitlines() if line] - - -def select_library_members(members: Iterable[str], max_files: int) -> list[str]: - selected = [ - member - for member in members - if not member.endswith("/") and re.search(r"(^|/)lib[^/]*\.so(?:\.|$)", member) - ] - return selected[:max_files] - - -def select_executable_members(members: Iterable[str], max_files: int) -> list[str]: - prefixes = ("./bin/", "./usr/bin/", "./sbin/", "./usr/sbin/", "./usr/lib/systemd/") - selected = [member for member in members if not member.endswith("/") and member.startswith(prefixes)] - return selected[:max_files] - - -def nm_symbols(path: Path, mode: str) -> list[str]: - flag = "-U" if mode == "defined" else "-u" - proc = subprocess.run( - ["nm", "--dynamic", "-j", flag, str(path)], - text=True, - capture_output=True, - ) - if proc.returncode != 0: - return [] - return [line.strip() for line in proc.stdout.splitlines() if line.strip()] - - -def extract_nm_report( - package: str, - pkghash: str, - branch: str, - arch: str, - mode: str, - max_files: int, - workdir: Path, -) -> NmReport: - rpm_url = package_download_url(pkghash, branch, arch) - rpm_path = workdir / Path(urllib.parse.urlparse(rpm_url).path).name - download_file(rpm_url, rpm_path) - members = rpm_members(rpm_path) - selected = ( - select_library_members(members, max_files) - if mode == "defined" - else select_executable_members(members, max_files) - ) - extract_dir = workdir / f"extract-{package.replace('/', '_').replace('+', '_')}-{mode}" - extract_dir.mkdir(parents=True, exist_ok=True) - if selected: - run_text(["bsdtar", "-xf", str(rpm_path), "-C", str(extract_dir), *selected]) - - symbols: list[str] = [] - for member in selected: - member_path = extract_dir / member - if member_path.exists(): - symbols.extend(nm_symbols(member_path, mode)) - - return NmReport( - package=package, - mode=mode, - files_seen=len(selected), - symbols=symbols, - file_examples=selected[:3], - ) - - -def char_classes(chars: Iterable[str]) -> dict[str, object]: - char_set = sorted(set(chars)) - return { - "alphabet": "".join(char_set), - "upper": sum(ch.isupper() for ch in char_set), - "lower": sum(ch.islower() for ch in char_set), - "digit": sum(ch.isdigit() for ch in char_set), - "underscore": "_" in char_set, - "at": "@" in char_set, - "dot": "." in char_set, - "dollar": "$" in char_set, - "other": "".join(ch for ch in char_set if not (ch.isalnum() or ch in "_.@$")), - } - - -def print_set_report(set_deps: list[SetDependency], prefix_len: int) -> None: - print("# set:version dependency strings") - if not set_deps: - print("No set: dependencies found.") - return - - lengths = [len(dep.payload) for dep in set_deps] - alphabet = "".join(sorted(set("".join(dep.payload for dep in set_deps)))) - print(f"count: {len(set_deps)}") - print(f"payload length min/median/max: {min(lengths)}/{statistics.median(lengths)}/{max(lengths)}") - print(f"observed encoded alphabet: {alphabet}") - print(f"bpp counts: {dict(sorted(Counter(dep.bpp for dep in set_deps).items()))}") - print(f"Mshift counts: {dict(sorted(Counter(dep.mshift for dep in set_deps).items()))}") - print() - print("package\ttype\tbpp\tMshift\tlen\tdependency\tset-prefix") - for dep in sorted(set_deps, key=lambda item: (item.package, item.dep_type, item.name)): - print( - f"{dep.package}\t{dep.dep_type}\t{dep.bpp}\t{dep.mshift}\t" - f"{len(dep.payload)}\t{dep.name}\t{dep.payload[:prefix_len]}" - ) - - -def print_nm_report(reports: list[NmReport], sample_limit: int) -> None: - print("\n# nm dynamic symbol strings") - if not reports: - print("Skipped. Pass --download-nm to download RPMs and run bsdtar/nm.") - return - - for mode in ("defined", "undefined"): - mode_reports = [report for report in reports if report.mode == mode] - if not mode_reports: - continue - all_symbols = [symbol for report in mode_reports for symbol in report.symbols] - print(f"\n## {mode} symbols") - print(f"packages: {len(mode_reports)}") - print(f"symbols total/unique: {len(all_symbols)}/{len(set(all_symbols))}") - if all_symbols: - print(f"max symbol length: {max(len(symbol) for symbol in all_symbols)}") - print(f"character classes: {char_classes(''.join(all_symbols))}") - odd = sorted({symbol for symbol in all_symbols if not IDENTISH_SYMBOL_RE.match(symbol)}) - mangled = sorted({symbol for symbol in all_symbols if symbol.startswith("_Z")}) - print(f"non identifier-ish examples: {odd[:sample_limit]}") - print(f"C++ mangled examples: {mangled[:sample_limit]}") - print("package\tfiles\tsymbols\tfile-examples\tsymbol-examples") - for report in mode_reports: - print( - f"{report.package}\t{report.files_seen}\t{len(report.symbols)}\t" - f"{', '.join(report.file_examples)}\t{', '.join(report.symbols[:sample_limit])}" - ) - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Print ALT package set:version strings and optional nm symbol statistics." - ) - parser.add_argument("packages", nargs="*", default=DEFAULT_PACKAGES, help="binary package names") - parser.add_argument("--branch", default="sisyphus", help="ALT repository branch/packageset") - parser.add_argument("--arch", default="x86_64", help="binary package architecture") - parser.add_argument("--download-nm", action="store_true", help="download RPMs and run nm on ELF files") - parser.add_argument("--nm-defined", action="store_true", help="with --download-nm, inspect defined symbols from libraries") - parser.add_argument("--nm-undefined", action="store_true", help="with --download-nm, inspect undefined symbols from executables") - parser.add_argument("--max-libs", type=int, default=8, help="max library files per package for defined-symbol nm") - parser.add_argument("--max-bins", type=int, default=20, help="max executable files per package for undefined-symbol nm") - parser.add_argument("--prefix-len", type=int, default=48, help="number of set payload chars to print per row") - parser.add_argument("--sample-limit", type=int, default=12, help="number of symbol examples to print") - return parser.parse_args(argv) - - -def main(argv: list[str]) -> int: - args = parse_args(argv) - if args.download_nm and not args.nm_defined and not args.nm_undefined: - args.nm_defined = True - args.nm_undefined = True - - if args.download_nm: - missing = [cmd for cmd in ("bsdtar", "nm") if shutil.which(cmd) is None] - if missing: - print(f"missing required command(s) for --download-nm: {', '.join(missing)}", file=sys.stderr) - return 2 - - package_hashes: dict[str, str] = {} - set_deps: list[SetDependency] = [] - errors: list[str] = [] - - for package in args.packages: - try: - pkghash, deps = get_set_dependencies(package, args.branch, args.arch) - package_hashes[package] = pkghash - set_deps.extend(deps) - except (urllib.error.URLError, urllib.error.HTTPError, KeyError, RuntimeError) as exc: - errors.append(f"{package}: {exc}") - - print(f"branch: {args.branch}") - print(f"arch: {args.arch}") - print(f"packages requested: {', '.join(args.packages)}") - if errors: - print("\n# lookup errors", file=sys.stderr) - for error in errors: - print(error, file=sys.stderr) - print() - print_set_report(set_deps, args.prefix_len) - - nm_reports: list[NmReport] = [] - if args.download_nm: - with tempfile.TemporaryDirectory(prefix="arsv-alt-rpms-") as tmp: - workdir = Path(tmp) - for package, pkghash in package_hashes.items(): - if args.nm_defined: - try: - nm_reports.append( - extract_nm_report( - package, - pkghash, - args.branch, - args.arch, - "defined", - args.max_libs, - workdir, - ) - ) - except Exception as exc: # keep processing other packages - errors.append(f"{package} defined nm: {exc}") - if args.nm_undefined: - try: - nm_reports.append( - extract_nm_report( - package, - pkghash, - args.branch, - args.arch, - "undefined", - args.max_bins, - workdir, - ) - ) - except Exception as exc: # keep processing other packages - errors.append(f"{package} undefined nm: {exc}") - - print_nm_report(nm_reports, args.sample_limit) - - if errors: - print("\n# errors", file=sys.stderr) - for error in errors: - print(error, file=sys.stderr) - return 1 - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/scripts/run-setc-bench-no-perf.sh b/scripts/run-setc-bench-no-perf.sh deleted file mode 100755 index 9ed224a..0000000 --- a/scripts/run-setc-bench-no-perf.sh +++ /dev/null @@ -1,312 +0,0 @@ -#!/bin/bash -set -euo pipefail -export LC_ALL=C - -# Настройки. -SETC_DIR="$HOME/setc" -WORK_ROOT="$HOME/setc-bench" -RESULT_DIR="$HOME/res" -RUNS=3 -CPU=0 -RESET_WORK=1 # 0 — продолжить готовые сборки, 1 — начать всё заново. -PACKAGER='krosh ' -APT_SOURCE=/etc/apt/sources.list.d/alt.list -APT_GET=/usr/lib/apt/apt-get -RPM_BUILD_GIT=https://git.altlinux.org/gears/r/rpm-build.git -RPM_GIT=https://git.altlinux.org/gears/r/rpm.git - -fail() -{ - printf 'error: %s\n' "$*" >&2 - exit 1 -} - -prepare_spec() -{ - local spec=$1 suffix=$2 version release new_release date - - version=$(sed -n 's/^Version:[[:space:]]*//p' "$spec" | sed -n '1p') - release=$(sed -n 's/^Release:[[:space:]]*//p' "$spec" | sed -n '1p') - [[ -n $version && -n $release ]] || fail "cannot read Version/Release from $spec" - - new_release="$release.$suffix" - sed -i "0,/^Release:[[:space:]]*$release$/s//Release: $new_release/" "$spec" - - date=$(date '+%a %b %d %Y') - sed -i "/^%changelog/a\\ -* $date $PACKAGER $version-$new_release\\ -- Local set.c benchmark build.\\ -" "$spec" -} - -apt_options() -{ - local root=$1 - - APT_OPTIONS=( - -o 'Dir::Etc::main=-' - -o 'Dir::Etc::parts=-' - -o "Dir::Etc::sourcelist=$COMMON/sources.list" - -o "Dir::Etc::sourceparts=$COMMON/sources.list.d" - -o 'Dir::Etc::preferences=-' - -o 'Dir::Etc::preferencesparts=-' - -o "Dir::State::lists=$COMMON/lists/" - -o "Dir::State::status=$COMMON/status" - -o "Dir::Cache=$COMMON/cache/" - -o "Dir::Cache::archives=$COMMON/cache/archives" - -o "Dir::Cache::pkgcache=$COMMON/cache/pkgcache.bin" - -o "Dir::Cache::srcpkgcache=$COMMON/cache/srcpkgcache.bin" - -o "RPM::RootDir=$root" - ) -} - -operation_args() -{ - case $1 in - check) ARGS=(-s check) ;; - autoremove) ARGS=(-s autoremove) ;; - install-rpm-build) ARGS=(-s install rpm-build) ;; - install-openuds-server) ARGS=(-s install openuds-server) ;; - install-password-store) ARGS=(-s install password-store) ;; - upgrade) ARGS=(-o APT::Get::EnableUpgrade=true -s upgrade) ;; - dist-upgrade) ARGS=(-s dist-upgrade) ;; - *) fail "unknown operation: $1" ;; - esac -} - -operation_label() -{ - case $1 in - check) printf '%s' '-s check' ;; - autoremove) printf '%s' '-s autoremove' ;; - install-rpm-build) printf '%s' '-s install rpm-build' ;; - install-openuds-server) printf '%s' '-s install openuds-server' ;; - install-password-store) printf '%s' '-s install password-store' ;; - upgrade) printf '%s' '--enable-upgrade -s upgrade' ;; - dist-upgrade) printf '%s' '-s dist-upgrade' ;; - esac -} - -run_once() -{ - local operation=$1 variant=$2 run=$3 start end status - local libdir="$variant/lib/usr/lib64" - local root="$COMMON/root" - local raw="$variant/raw" - - operation_args "$operation" - apt_options "$root" - - start=$(date +%s%N) - if env LC_ALL=C APT_CONFIG="$COMMON/apt.conf" \ - LD_LIBRARY_PATH="$libdir" \ - taskset -c "$CPU" "$APT_GET" -q \ - "${APT_OPTIONS[@]}" "${ARGS[@]}" \ - >"$raw/$operation.$run.stdout" \ - 2>"$raw/$operation.$run.stderr"; then - status=0 - else - status=$? - fi - end=$(date +%s%N) - - RUN_TIME=$(awk -v start="$start" -v end="$end" \ - 'BEGIN { printf "%.6f", (end - start) / 1000000000 }') - RUN_STATUS=$status -} - -benchmark_variant() -{ - local variant=$1 result=$2 operation run average label status_text - local -a times statuses - local -a operations=( - check - autoremove - install-rpm-build - install-openuds-server - install-password-store - upgrade - dist-upgrade - ) - - mkdir -p "$variant/raw" - printf 'command\taverage_seconds\trun1_seconds\trun2_seconds\trun3_seconds\texit_status\n' \ - >"$result" - - for operation in "${operations[@]}"; do - times=() - statuses=() - - for ((run = 1; run <= RUNS; ++run)); do - run_once "$operation" "$variant" "$run" - times+=("$RUN_TIME") - statuses+=("$RUN_STATUS") - printf '%s: run %d/%d: %ss, status=%s\n' \ - "$operation" "$run" "$RUNS" "$RUN_TIME" "$RUN_STATUS" - done - - average=$(printf '%s\n' "${times[@]}" | \ - awk '{ total += $1 } END { printf "%.6f", total / NR }') - - status_text=${statuses[0]} - for status in "${statuses[@]}"; do - if [[ $status != "$status_text" ]]; then - status_text=$(IFS=,; printf 'mixed:%s' "${statuses[*]}") - break - fi - done - - label=$(operation_label "$operation") - printf '%s\t%s\t%s\t%s\t%s\t%s\n' \ - "$label" "$average" \ - "${times[0]}" "${times[1]}" "${times[2]}" "$status_text" \ - >>"$result" - done -} - -for command in git gear-hsh hsh rpm rpmquery rpm2cpio cpio apt-get apt-cache \ - taskset awk sed date sha256sum ldd; do - command -v "$command" >/dev/null || fail "required command not found: $command" -done -[[ -x $APT_GET ]] || fail "not found: $APT_GET" -[[ $RUNS -eq 3 ]] || fail 'RUNS must stay equal to 3 for the TSV format below' - -shopt -s nullglob -setc_files=("$SETC_DIR"/*.c) -((${#setc_files[@]} > 0)) || fail "no .c files in $SETC_DIR" - -if ((RESET_WORK)); then - # После прерванной сборки часть chroot принадлежит hasher-псевдопользователю; - # обычный rm её не удалит. - old_hashers=("$WORK_ROOT"/variants/*/hasher) - for old_hasher in "${old_hashers[@]}"; do - [[ -d $old_hasher ]] || continue - hsh --cleanup-only --workdir="$old_hasher" >/dev/null 2>&1 || true - done - rm -rf "$WORK_ROOT" -fi -mkdir -p "$WORK_ROOT/src" "$WORK_ROOT/variants" "$RESULT_DIR" -COMMON="$WORK_ROOT/common" -if [[ ! -s $COMMON/cache/pkgcache.bin ]]; then - rm -rf "$COMMON" - mkdir -p \ - "$COMMON/lists/partial" \ - "$COMMON/cache/archives/partial" \ - "$COMMON/sources.list.d" \ - "$COMMON/root/var/lib/rpm" - : >"$COMMON/apt.conf" - : >"$COMMON/status" - - # Один официальный p11 snapshot без hasher/stuff. Он создаётся до сборок - # и затем одинаково используется всеми вариантами. - awk '$1 == "rpm" && /p11\/branch/ && $0 !~ /(hasher|stuff)/ { print }' \ - "$APT_SOURCE" >"$COMMON/sources.list" - [[ -s $COMMON/sources.list ]] || fail "no p11 sources in $APT_SOURCE" - - cp -a /var/lib/rpm/. "$COMMON/root/var/lib/rpm/" - apt_options "$COMMON/root" - APT_CONFIG="$COMMON/apt.conf" apt-get -qq "${APT_OPTIONS[@]}" update - APT_CONFIG="$COMMON/apt.conf" apt-cache -q "${APT_OPTIONS[@]}" gencaches >/dev/null - sha256sum "$COMMON/cache/pkgcache.bin" "$COMMON/cache/srcpkgcache.bin" \ - >"$COMMON/cache.sha256" -fi - -# Чистые p11 gear-деревья клонируются один раз. -if [[ ! -d $WORK_ROOT/src/rpm-build-base/.git ]]; then - git clone --branch p11 --single-branch "$RPM_BUILD_GIT" \ - "$WORK_ROOT/src/rpm-build-base" -fi -if [[ ! -d $WORK_ROOT/src/rpm-base/.git ]]; then - git clone --branch p11 --single-branch "$RPM_GIT" \ - "$WORK_ROOT/src/rpm-base" -fi - -index=0 -for setc in "${setc_files[@]}"; do - index=$((index + 1)) - id=$(printf 'v%03d' "$index") - suffix="setc$id" - filename=$(basename "$setc") - result_name=${filename%.c}.tsv - variant="$WORK_ROOT/variants/$id" - source="$variant/src" - hasher="$variant/hasher" - repo="$hasher/repo/x86_64/RPMS.hasher" - - printf '\n===== %s: %s =====\n' "$id" "$filename" - mkdir -p "$source" "$hasher" "$variant/logs" "$variant/lib" - - if [[ ! -d $source/rpm-build/.git ]]; then - git clone --local "$WORK_ROOT/src/rpm-build-base" "$source/rpm-build" - git clone --local "$WORK_ROOT/src/rpm-base" "$source/rpm" - - # По условию один и тот же входной файл кладётся как lib/set.c в оба пакета. - cp "$setc" "$source/rpm-build/lib/set.c" - cp "$setc" "$source/rpm/lib/set.c" - - prepare_spec "$source/rpm-build/rpm-4_0.spec" "$suffix" - prepare_spec "$source/rpm/alt/rpm.spec" "$suffix" - else - cmp -s "$setc" "$source/rpm-build/lib/set.c" && \ - cmp -s "$setc" "$source/rpm/lib/set.c" || \ - fail "$filename changed; set RESET_WORK=1 and restart" - fi - - rpm_build_rpms=("$repo"/rpm-build-[0-9]*".$suffix".x86_64.rpm) - if ((${#rpm_build_rpms[@]} == 0)); then - ( - cd "$source/rpm-build" - gear-hsh \ - --commit \ - --with-stuff \ - --packager="$PACKAGER" \ - --no-sisyphus-check=changelog,packager,gpg \ - --mountpoints=/proc \ - --workdir="$hasher" \ - --target=x86_64 - ) 2>&1 | tee "$variant/logs/rpm-build.log" - else - printf 'rpm-build already built: %s\n' "${rpm_build_rpms[0]}" - fi - - librpm_rpms=("$repo"/librpm7-[0-9]*".$suffix".x86_64.rpm) - if ((${#librpm_rpms[@]} == 0)); then - ( - cd "$source/rpm" - gear-hsh \ - --commit \ - --with-stuff \ - --packager="$PACKAGER" \ - --no-sisyphus-check=changelog,packager,gpg \ - --mountpoints=/proc \ - --workdir="$hasher" \ - --target=x86_64 - ) 2>&1 | tee "$variant/logs/rpm.log" - else - printf 'rpm/librpm7 already built: %s\n' "${librpm_rpms[0]}" - fi - - # [0-9]* исключает отдельный пакет librpm7-debuginfo. - librpm_rpms=("$repo"/librpm7-[0-9]*".$suffix".x86_64.rpm) - ((${#librpm_rpms[@]} == 1)) || \ - fail "expected one librpm7 package for $filename, got ${#librpm_rpms[@]}" - - rm -rf "$variant/lib" - mkdir -p "$variant/lib" - ( - cd "$variant/lib" - rpm2cpio "${librpm_rpms[0]}" | cpio -idm --quiet - ) - - libdir="$variant/lib/usr/lib64" - [[ -e $libdir/librpm.so.7 && -e $libdir/librpmio.so.7 ]] || \ - fail "librpm libraries were not extracted for $filename" - LD_LIBRARY_PATH="$libdir" ldd "$APT_GET" | \ - grep -F "$libdir/librpm.so.7" >/dev/null || \ - fail "apt-get does not load the built librpm for $filename" - - benchmark_variant "$variant" "$RESULT_DIR/$result_name" -done - -printf '\nResults:\n' -printf ' %s\n' "$RESULT_DIR"/*.tsv diff --git a/tests/test_check_alt_set_impl.py b/tests/test_check_alt_set_impl.py deleted file mode 100644 index 9a0be6e..0000000 --- a/tests/test_check_alt_set_impl.py +++ /dev/null @@ -1,100 +0,0 @@ -import importlib.util -import sys -from pathlib import Path - -SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "check_alt_set_impl.py" - - -def load_script(): - spec = importlib.util.spec_from_file_location("check_alt_set_impl", SCRIPT) - module = importlib.util.module_from_spec(spec) - assert spec.loader is not None - sys.modules[spec.name] = module - spec.loader.exec_module(module) - return module - - -def test_select_elf_members_separates_providers_and_requirers(): - check = load_script() - members = [ - "./usr/lib64/libfoo.so.1", - "./usr/lib64/libfoo.so.1.2.3", - "./usr/lib64/libfoo.a", - "./usr/bin/tool", - "./usr/share/doc/readme", - ] - - assert check.select_provider_members(members) == [ - "./usr/lib64/libfoo.so.1", - "./usr/lib64/libfoo.so.1.2.3", - ] - assert check.select_requirer_members(members) == [ - "./usr/lib64/libfoo.so.1", - "./usr/lib64/libfoo.so.1.2.3", - "./usr/bin/tool", - ] - - -def test_generate_label_set_and_compare_uses_only_local_set_py(): - check = load_script() - provided = check.generate_label_set("provider", "libfoo.so.1", ["foo", "bar", "baz"], bpp=16) - required = check.generate_label_set("requirer", "tool", ["foo", "bar"], bpp=16) - missing = check.generate_label_set("requirer", "badtool", ["foo", "quux"], bpp=16) - - ok = check.compare_label_sets(provided, required) - bad = check.compare_label_sets(provided, missing) - - assert ok.status == "compatible" - assert ok.cmp_result == 1 - assert bad.status == "incompatible" - assert bad.cmp_result == -2 - - -def test_normalize_required_symbol_version_can_match_provided_symbol_version(): - check = load_script() - - assert check.normalize_required_symbol("foo@LIB_1") == "foo@@LIB_1" - assert check.normalize_required_symbol("foo@@LIB_1") == "foo@@LIB_1" - assert check.normalize_required_symbol("foo") == "foo" - - -def test_parse_required_nm_output_filters_weak_undefined_symbols(): - check = load_script() - nm_output = """ - w __gmon_start__ - w _ITM_deregisterTMCloneTable - U close@GLIBC_2.2.5 - U memcpy@GLIBC_2.14 - W optional_hook - """ - - assert check.parse_required_nm_output(nm_output) == [ - "close@GLIBC_2.2.5", - "memcpy@GLIBC_2.14", - ] - - -def test_default_bpp_is_large_enough_for_real_alt_symbol_sets(): - check = load_script() - - assert check.parse_args([]).bpp == 32 - - -def test_build_dependency_results_splits_required_labels_by_provider_library(): - check = load_script() - libc = check.generate_label_set( - "provided", "libc.so.6", ["close@@GLIBC_2.2.5", "read@@GLIBC_2.2.5"], bpp=16, package="glibc-core" - ) - libz = check.generate_label_set( - "provided", "libz.so.1", ["inflate", "deflate"], bpp=16, package="zlib" - ) - tool = check.generate_label_set( - "required", "tool", ["close@@GLIBC_2.2.5", "inflate"], bpp=16, package="consumer" - ) - - results = check.build_dependency_results([libc, libz], [tool], bpp=16) - - assert [(result.provider_member, result.required_labels, result.status) for result in results] == [ - ("libc.so.6", 1, "compatible"), - ("libz.so.1", 1, "compatible"), - ] diff --git a/tests/test_check_sisyphus_set_relations.py b/tests/test_check_sisyphus_set_relations.py deleted file mode 100644 index 3fb38d1..0000000 --- a/tests/test_check_sisyphus_set_relations.py +++ /dev/null @@ -1,135 +0,0 @@ -from __future__ import annotations - -import os -import subprocess -from pathlib import Path - - -REPO_ROOT = Path(__file__).resolve().parents[1] -SCRIPT = REPO_ROOT / "scripts" / "rpmsetcmp" / "check_sisyphus_set_relations.sh" -PROVIDER_SET = "set:jg2ZwFjdTD2B0" -REQUIRED_SET = "set:jiXMb" - - -def run_with_metadata( - tmp_path: Path, metadata: str, *, locale: str | None = None -) -> subprocess.CompletedProcess[str]: - fake_bin = tmp_path / "bin" - fake_bin.mkdir() - metadata_file = tmp_path / "available.txt" - metadata_file.write_text(metadata, encoding="utf-8") - - (fake_bin / "apt-get").write_text( - """#!/bin/sh -archive= -rpm_root= -for argument in "$@"; do - case $argument in - Dir::Cache::archives=*) archive=${argument#*=} ;; - RPM::RootDir=*) rpm_root=${argument#*=} ;; - esac -done -[ -n "$archive" ] && [ -d "$archive/partial" ] -[ -n "$rpm_root" ] && [ -d "$rpm_root/var/lib/rpm" ] -""", - encoding="utf-8", - ) - (fake_bin / "apt-cache").write_text( - '#!/bin/sh\ncat "$FAKE_APT_DUMP"\n', encoding="utf-8" - ) - (fake_bin / "apt-get").chmod(0o755) - (fake_bin / "apt-cache").chmod(0o755) - - environment = os.environ.copy() - environment["PATH"] = f"{fake_bin}:{environment['PATH']}" - environment["FAKE_APT_DUMP"] = str(metadata_file) - if locale is not None: - environment["LC_ALL"] = locale - return subprocess.run( - ["bash", str(SCRIPT)], - cwd=REPO_ROOT, - text=True, - capture_output=True, - env=environment, - ) - - -def test_accepts_a_real_provider_subset_relation_and_continuation_line(tmp_path: Path) -> None: - metadata = f"""\ -Package: libexample -Provides: libexample.so.1()(64bit) (= {PROVIDER_SET}) - -Package: example -Depends: plain-dependency, - libexample.so.1()(64bit) (>= {REQUIRED_SET}) - -""" - - completed = run_with_metadata(tmp_path, metadata) - - assert completed.returncode == 0, completed.stderr - assert "requirements: 1" in completed.stdout - assert "compatible: 1" in completed.stdout - assert "incompatible: 0" in completed.stdout - - -def test_rejects_a_provider_that_does_not_contain_the_required_set(tmp_path: Path) -> None: - metadata = f"""\ -Package: libexample -Provides: libexample.so.1()(64bit) (= {REQUIRED_SET}) - -Package: example -Depends: libexample.so.1()(64bit) (>= {PROVIDER_SET}) - -""" - - completed = run_with_metadata(tmp_path, metadata) - - assert completed.returncode == 1 - assert "requirements: 1" in completed.stdout - assert "compatible: 0" in completed.stdout - assert "incompatible: 1" in completed.stdout - assert "example" in completed.stdout - - -def test_uses_one_collation_order_for_sort_and_join(tmp_path: Path) -> None: - metadata = f"""\ -Package: userdb-provider -Provides: /usr/lib64/courier-authlib/libauthuserdb.so.0()(64bit) (= {PROVIDER_SET}) - -Package: auth-provider -Provides: /usr/lib64/courier-authlib/libcourierauth.so.0()(64bit) (= {PROVIDER_SET}) - -Package: auth-consumer -Depends: /usr/lib64/courier-authlib/libcourierauth.so.0()(64bit) (>= {REQUIRED_SET}) - -Package: common-consumer -Depends: /usr/lib64/courier-authlib/libcourierauthcommon.so.0()(64bit) (>= {REQUIRED_SET}) - -""" - - completed = run_with_metadata(tmp_path, metadata, locale="ru_RU.utf8") - - assert completed.returncode == 1 - assert "is not sorted" not in completed.stderr - assert "requirements: 2" in completed.stdout - assert "compatible: 1" in completed.stdout - assert "no provider: 1" in completed.stdout - - -def test_large_set_is_not_passed_as_a_command_line_argument(tmp_path: Path) -> None: - oversized_set = "set:" + "a" * 140_000 - metadata = f"""\ -Package: libexample -Provides: libexample.so.1()(64bit) (= {oversized_set}) - -Package: example -Depends: libexample.so.1()(64bit) (>= {REQUIRED_SET}) - -""" - - completed = run_with_metadata(tmp_path, metadata) - - assert completed.returncode == 1 - assert "Argument list too long" not in completed.stderr - assert "decode errors:1" in completed.stdout diff --git a/tests/test_compare_cmp_realization.py b/tests/test_compare_cmp_realization.py deleted file mode 100644 index 1f33adf..0000000 --- a/tests/test_compare_cmp_realization.py +++ /dev/null @@ -1,174 +0,0 @@ -from __future__ import annotations - -import csv -import importlib.util -import sys -from pathlib import Path - - -REPO_ROOT = Path(__file__).resolve().parents[1] -SCRIPT = REPO_ROOT / "scripts" / "compare_cmp_realization.py" - - -def make_fake_rpm_tree(tmp_path: Path) -> tuple[Path, Path]: - rpm_root = tmp_path / "rpm" - (rpm_root / "rpmio").mkdir(parents=True) - (rpm_root / "lib").mkdir() - (rpm_root / "system.h").write_text("#pragma once\n", encoding="utf-8") - (rpm_root / "rpmio" / "rpmmalloc.c").write_text("/* test stub */\n", encoding="utf-8") - (rpm_root / "lib" / "placeholder.h").write_text("#pragma once\n", encoding="utf-8") - - set_source = tmp_path / "fake_set.c" - set_source.write_text( - r''' -#include -#include - -struct set { - size_t count; -}; - -struct set *set_new(void) -{ - return calloc(1, sizeof(struct set)); -} - -void set_add(struct set *set, const char *symbol) -{ - (void) symbol; - ++set->count; -} - -const char *set_fini(struct set *set, int bpp) -{ - (void) set; - (void) bpp; - return strdup("fake-payload"); -} - -struct set *set_free(struct set *set) -{ - free(set); - return NULL; -} - -int rpmsetcmp(const char *set1, const char *set2) -{ - return strcmp(set1, set2) == 0 ? 0 : -2; -} -''', - encoding="utf-8", - ) - return rpm_root, set_source - - -def make_elf_fixture(tmp_path: Path) -> tuple[Path, Path]: - import subprocess - - provider_source = tmp_path / "provider.c" - provider_source.write_text("int cmp_benchmark_label(void) { return 7; }\n", encoding="utf-8") - provider = tmp_path / "libcmp-provider.so.1" - subprocess.run( - [ - "cc", - "-shared", - "-fPIC", - "-Wl,-soname,libcmp-provider.so.1", - str(provider_source), - "-o", - str(provider), - ], - check=True, - ) - - consumer_source = tmp_path / "consumer.c" - consumer_source.write_text( - "extern int cmp_benchmark_label(void); int main(void) { return cmp_benchmark_label() == 7 ? 0 : 1; }\n", - encoding="utf-8", - ) - consumer = tmp_path / "consumer" - subprocess.run( - [ - "cc", - str(consumer_source), - str(provider), - f"-Wl,-rpath,{tmp_path}", - "-o", - str(consumer), - ], - check=True, - ) - return consumer, provider - - -def load_module(): - sys.path.insert(0, str(REPO_ROOT / "scripts")) - spec = importlib.util.spec_from_file_location("compare_cmp_realization_tested", SCRIPT) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - sys.modules[spec.name] = module - spec.loader.exec_module(module) - return module - - -def test_compiles_cmp_runner_runs_hardcoded_case_n_times_and_writes_median( - tmp_path: Path, capsys -) -> None: - module = load_module() - rpm_root, set_source = make_fake_rpm_tree(tmp_path) - consumer, provider = make_elf_fixture(tmp_path) - result_root = tmp_path / "res_cmp" - setattr( - module, - "HARDCODED_CASES", - ( - module.ComparisonCase( - name="consumer-provider", - binary_candidates=(str(consumer),), - library_candidates=(str(provider),), - ), - ), - ) - - return_code = module.main( - [ - str(set_source), - "-n", - "3", - "--rpm-root", - str(rpm_root), - "--res-dir", - str(result_root), - "--name", - "fake", - ] - ) - - assert return_code == 0 - implementation_dir = result_root / "fake" - assert (implementation_dir / "build" / "run_set").is_file() - assert (implementation_dir / "build" / "run_cmp").is_file() - assert (implementation_dir / "build" / "build.log").is_file() - - set_outputs = sorted((implementation_dir / "sets").glob("*.tsv")) - assert len(set_outputs) == 2 - raw_outputs = sorted((implementation_dir / "runs" / "consumer-provider").glob("run-*.tsv")) - assert len(raw_outputs) == 3 - assert all("cmp_result\t0" in path.read_text(encoding="utf-8") for path in raw_outputs) - assert all("rpmsetcmp_ns\t" in path.read_text(encoding="utf-8") for path in raw_outputs) - - with (implementation_dir / "summary.tsv").open(newline="", encoding="utf-8") as stream: - rows = list(csv.DictReader(stream, delimiter="\t")) - assert len(rows) == 1 - row = rows[0] - assert row["case"] == "consumer-provider" - assert row["binary"] == str(consumer.resolve()) - assert row["library"] == str(provider.resolve()) - assert row["runs"] == "3" - assert row["cmp_result"] == "0" - assert row["outputs_consistent"] == "yes" - assert int(row["median_rpmsetcmp_ns"]) >= 0 - - captured = capsys.readouterr() - assert "median_rpmsetcmp_ns" in captured.out - assert "consumer-provider" in captured.out diff --git a/tests/test_compare_realization.py b/tests/test_compare_realization.py deleted file mode 100644 index b32c28a..0000000 --- a/tests/test_compare_realization.py +++ /dev/null @@ -1,132 +0,0 @@ -from __future__ import annotations - -import csv -import subprocess -import sys -from pathlib import Path - - -REPO_ROOT = Path(__file__).resolve().parents[1] -SCRIPT = REPO_ROOT / "scripts" / "compare_realization.py" - - -def run(command: list[str]) -> subprocess.CompletedProcess[str]: - return subprocess.run(command, text=True, capture_output=True, check=True) - - -def make_fake_rpm_tree(tmp_path: Path) -> tuple[Path, Path]: - rpm_root = tmp_path / "rpm" - (rpm_root / "rpmio").mkdir(parents=True) - (rpm_root / "lib").mkdir() - (rpm_root / "system.h").write_text("#pragma once\n", encoding="utf-8") - (rpm_root / "rpmio" / "rpmmalloc.c").write_text("/* test stub */\n", encoding="utf-8") - (rpm_root / "lib" / "placeholder.h").write_text("#pragma once\n", encoding="utf-8") - - set_source = tmp_path / "fake_set.c" - set_source.write_text( - r''' -#include -#include - -struct set { - size_t count; -}; - -struct set *set_new(void) -{ - return calloc(1, sizeof(struct set)); -} - -void set_add(struct set *set, const char *symbol) -{ - (void) symbol; - ++set->count; -} - -const char *set_fini(struct set *set, int bpp) -{ - (void) set; - (void) bpp; - return strdup("fake-payload"); -} - -struct set *set_free(struct set *set) -{ - free(set); - return NULL; -} -''', - encoding="utf-8", - ) - return rpm_root, set_source - - -def make_provider(tmp_path: Path) -> Path: - source = tmp_path / "provider.c" - source.write_text("int benchmark_label(void) { return 7; }\n", encoding="utf-8") - provider = tmp_path / "libprovider.so.1" - run( - [ - "cc", - "-shared", - "-fPIC", - "-Wl,-soname,libprovider.so.1", - str(source), - "-o", - str(provider), - ] - ) - return provider - - -def test_compiles_selected_set_runs_n_times_and_writes_median_results(tmp_path: Path) -> None: - rpm_root, set_source = make_fake_rpm_tree(tmp_path) - provider = make_provider(tmp_path) - result_root = tmp_path / "res" - - completed = run( - [ - sys.executable, - str(SCRIPT), - str(set_source), - "-n", - "3", - "--rpm-root", - str(rpm_root), - "--res-dir", - str(result_root), - "--name", - "fake", - "--target", - str(provider), - ] - ) - - implementation_dir = result_root / "fake" - assert (implementation_dir / "build" / "run_set").is_file() - assert (implementation_dir / "build" / "build.log").is_file() - - raw_outputs = sorted((implementation_dir / "runs" / "libprovider.so.1").glob("run-*.tsv")) - assert len(raw_outputs) == 3 - assert all("set:fake-payload" in output.read_text(encoding="utf-8") for output in raw_outputs) - - with (implementation_dir / "summary.tsv").open(newline="", encoding="utf-8") as stream: - rows = list(csv.DictReader(stream, delimiter="\t")) - assert len(rows) == 1 - row = rows[0] - assert row["target"] == str(provider.resolve()) - assert row["kind"] == "shared-library" - assert row["runs"] == "3" - assert row["sets_per_run"] == "1" - assert row["outputs_consistent"] == "yes" - for field in ( - "median_set_new_ns", - "median_set_add_total_ns", - "median_set_fini_ns", - "median_set_free_ns", - "median_set_api_total_ns", - ): - assert int(row[field]) >= 0 - - assert "libprovider.so.1" in completed.stdout - assert "median_set_api_total_ns" in completed.stdout diff --git a/tests/test_compare_sisyphus_set_versions.py b/tests/test_compare_sisyphus_set_versions.py deleted file mode 100644 index de2a704..0000000 --- a/tests/test_compare_sisyphus_set_versions.py +++ /dev/null @@ -1,319 +0,0 @@ -from __future__ import annotations - -import os -import subprocess -from pathlib import Path - - -REPO_ROOT = Path(__file__).resolve().parents[1] -SCRIPT = REPO_ROOT / "scripts" / "compare_sisyphus_set_versions.sh" -PACKAGE_PARSER = REPO_ROOT / "scripts" / "parse_sisyphus_packages.awk" -RESUME_PARSER = REPO_ROOT / "scripts" / "completed_set_versions.awk" -PAYLOAD_HELPERS = REPO_ROOT / "scripts" / "rpm_payload_safety.sh" - - -def run_script(*arguments: str, input_text: str | None = None) -> subprocess.CompletedProcess[str]: - return subprocess.run( - ["bash", str(SCRIPT), *arguments], - cwd=REPO_ROOT, - text=True, - input=input_text, - capture_output=True, - ) - - -def test_requires_explicit_scope_before_touching_repository() -> None: - completed = run_script() - - assert completed.returncode == 2 - assert "use --all, --limit, or --package" in completed.stderr - - -def test_rejects_source_list_and_package_name_injection_before_network() -> None: - bad_mirror = run_script("--package", "strace", "--mirror", "https://example.test/\nrpm evil") - assert bad_mirror.returncode == 2 - assert "mirror must not contain whitespace" in bad_mirror.stderr - - bad_package = run_script("--package", "--option") - assert bad_package.returncode == 2 - assert "invalid package name" in bad_package.stderr - - -def test_build_mkset_wraps_newset_and_matches_known_alt_payload(tmp_path: Path) -> None: - output = tmp_path / "new-mkset" - completed = run_script("--build-mkset", str(output)) - - assert completed.returncode == 0, completed.stderr - assert output.is_file() - assert os.access(output, os.X_OK) - - encoded = subprocess.run( - [str(output), "16"], - input="sym0\nsym1\nsym2\nsym3\nsym0\n", - text=True, - capture_output=True, - check=True, - ) - assert encoded.stdout == "set:jg2ZwFjdTD2B0\n" - - bad_bpp = subprocess.run( - [str(output), "16junk"], - input="sym0\n", - text=True, - capture_output=True, - ) - assert bad_bpp.returncode == 2 - assert "invalid BPP" in bad_bpp.stderr - - empty = subprocess.run( - [str(output), "16"], - input="", - text=True, - capture_output=True, - ) - assert empty.returncode == 2 - assert "no symbols" in empty.stderr - - -def test_package_parser_preserves_empty_md5_field() -> None: - metadata = """\ -Package: libexample -Architecture: x86_64 -Version: 1.2-alt1 -Filename: libexample-1.2-alt1.x86_64.rpm -Provides: libexample.so.1 = set:abc -Depends: libc.so.6 - libc.so.6(GLIBC_2.34)(64bit) >= set:def - -""" - completed = subprocess.run( - ["awk", "-f", str(PACKAGE_PARSER)], - input=metadata, - text=True, - capture_output=True, - check=True, - ) - - assert completed.stdout.rstrip("\n").split("\x1c") == [ - "libexample", - "x86_64", - "1.2-alt1", - "libexample-1.2-alt1.x86_64.rpm", - "", - "1", - "1", - ] - - -def test_resume_parser_ignores_partial_summary_rows() -> None: - report = "\n".join( - [ - "timestamp\trecord\tpackage\tarchitecture\tstatus\tside\tcapability\toperator\texpected\tgenerated\tdetail", - "t\tSTART\tbroken\tx86_64\tprocessing\t-\t-\t-\t-\t-\t1-alt1", - "t\tSUMMARY\tbroken\tx86_64\tmatch", - "t\tSTART\tcomplete\tnoarch\tprocessing\t-\t-\t-\t-\t-\t2-alt1", - "t\tSUMMARY\tcomplete\tnoarch\tmatch\t-\t-\t-\t-\t-\tdone; complete=1", - "", - ] - ) - completed = subprocess.run( - ["awk", "-f", str(RESUME_PARSER)], - input=report, - text=True, - capture_output=True, - check=True, - ) - - assert completed.stdout == "complete\tnoarch\t2-alt1\n" - - -def test_absolute_rpm_symlink_is_rewritten_inside_temporary_root(tmp_path: Path) -> None: - root = tmp_path / "root" - (root / "usr" / "sbin").mkdir(parents=True) - completed = subprocess.run( - [ - "bash", - "-c", - 'source "$1"; rpm_safe_symlink_target "$2" "$3" "$4"', - "bash", - str(PAYLOAD_HELPERS), - str(root), - "usr/sbin/update-alternatives", - "/bin/true", - ], - text=True, - capture_output=True, - ) - - assert completed.returncode == 0, completed.stderr - assert completed.stdout == "../../bin/true\n" - - -def test_escaping_relative_rpm_symlink_is_rejected(tmp_path: Path) -> None: - root = tmp_path / "root" - root.mkdir() - completed = subprocess.run( - [ - "bash", - "-c", - 'source "$1"; rpm_safe_symlink_target "$2" "$3" "$4"', - "bash", - str(PAYLOAD_HELPERS), - str(root), - "usr/link", - "../../../outside", - ], - text=True, - capture_output=True, - ) - - assert completed.returncode != 0 - - -def test_cpio_member_pattern_quotes_glob_characters() -> None: - completed = subprocess.run( - [ - "bash", - "-c", - 'source "$1"; rpm_cpio_literal_pattern "$2"', - "bash", - str(PAYLOAD_HELPERS), - "./usr/share/doc/a[1]*?.txt", - ], - text=True, - capture_output=True, - ) - - assert completed.returncode == 0, completed.stderr - assert completed.stdout == "./usr/share/doc/a[[]1[]][*][?].txt\n" - - -def test_directory_symlinks_are_installed_before_children(tmp_path: Path) -> None: - root = tmp_path / "root" - root.mkdir() - manifest = tmp_path / "symlinks.tsv" - manifest.write_text("bin/sh\tbash\nbin\tusr/bin\n") - completed = subprocess.run( - [ - "bash", - "-c", - 'source "$1"; rpm_install_symlink_manifest "$2" "$3" "$4"', - "bash", - str(PAYLOAD_HELPERS), - str(root), - str(manifest), - str(tmp_path / "extract.log"), - ], - text=True, - capture_output=True, - ) - - assert completed.returncode == 0, completed.stderr - assert (root / "bin").is_symlink() - assert os.readlink(root / "bin") == "usr/bin" - assert (root / "usr" / "bin" / "sh").is_symlink() - assert os.readlink(root / "usr" / "bin" / "sh") == "bash" - - -def test_child_target_is_revalidated_after_parent_alias(tmp_path: Path) -> None: - root = tmp_path / "root" - root.mkdir() - manifest = tmp_path / "symlinks.tsv" - manifest.write_text("deep/a\t..\ndeep/a/link\t../outside\n") - completed = subprocess.run( - [ - "bash", - "-c", - 'source "$1"; rpm_install_symlink_manifest "$2" "$3" "$4"', - "bash", - str(PAYLOAD_HELPERS), - str(root), - str(manifest), - str(tmp_path / "extract.log"), - ], - text=True, - capture_output=True, - ) - - assert completed.returncode != 0 - assert not (tmp_path / "outside").exists() - - -def test_absolute_target_is_rewritten_after_parent_alias(tmp_path: Path) -> None: - root = tmp_path / "root" - root.mkdir() - manifest = tmp_path / "symlinks.tsv" - manifest.write_text("bin\tusr/bin\nbin/tool\t/opt/tool\n") - completed = subprocess.run( - [ - "bash", - "-c", - 'source "$1"; rpm_install_symlink_manifest "$2" "$3" "$4"', - "bash", - str(PAYLOAD_HELPERS), - str(root), - str(manifest), - str(tmp_path / "extract.log"), - ], - text=True, - capture_output=True, - ) - - assert completed.returncode == 0, completed.stderr - assert os.readlink(root / "usr" / "bin" / "tool") == "../../opt/tool" - assert (root / "usr" / "bin" / "tool").resolve(strict=False) == root / "opt" / "tool" - - -def test_symlinked_temporary_root_is_canonicalized(tmp_path: Path) -> None: - real_root = tmp_path / "real-root" - real_root.mkdir() - root_alias = tmp_path / "root-alias" - root_alias.symlink_to(real_root, target_is_directory=True) - manifest = tmp_path / "symlinks.tsv" - manifest.write_text("bin\tusr/bin\n") - completed = subprocess.run( - [ - "bash", - "-c", - 'source "$1"; rpm_install_symlink_manifest "$2" "$3" "$4"', - "bash", - str(PAYLOAD_HELPERS), - str(root_alias), - str(manifest), - str(tmp_path / "extract.log"), - ], - text=True, - capture_output=True, - ) - - assert completed.returncode == 0, completed.stderr - assert (real_root / "bin").is_symlink() - - -def test_rpm_file_record_with_tab_in_filename_is_rejected() -> None: - record = "/name\tpart\x1c" - completed = subprocess.run( - [ - "bash", - "-c", - 'source "$1"; rpm_split_file_record "$2"', - "bash", - str(PAYLOAD_HELPERS), - record, - ], - text=True, - capture_output=True, - ) - - assert completed.returncode != 0 - - -def test_help_documents_streaming_report_and_test_scope() -> None: - completed = run_script("--help") - - assert completed.returncode == 0 - assert "--report FILE" in completed.stdout - assert "--package NAME" in completed.stdout - assert "--limit N" in completed.stdout - assert "--all" in completed.stdout - assert "appended after every package" in completed.stdout diff --git a/tests/test_newset_decode.py b/tests/test_newset_decode.py deleted file mode 100644 index 7bc7c70..0000000 --- a/tests/test_newset_decode.py +++ /dev/null @@ -1,56 +0,0 @@ -from __future__ import annotations - -import subprocess -import importlib.util -import sys -from pathlib import Path - - -REPO_ROOT = Path(__file__).resolve().parents[1] -SCRIPTS_DIR = REPO_ROOT / "scripts" -if str(SCRIPTS_DIR) not in sys.path: - sys.path.insert(0, str(SCRIPTS_DIR)) - - -def load_script(name: str): - path = SCRIPTS_DIR / f"{name}.py" - spec = importlib.util.spec_from_file_location(name, path) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - sys.modules[name] = module - spec.loader.exec_module(module) - return module - - -# Produced by the original ALT mkset at bpp=16 from sym0..sym3 and sym0. -# The provider payload includes a base62 Z escape; the required payload does not. -PROVIDER_SET = "set:jg2ZwFjdTD2B0" -REQUIRED_SET = "set:jiXMb" - - -def test_newset_decodes_base62_escape_before_comparing_subset(tmp_path: Path) -> None: - compare_set = load_script("compare_realization") - compare_cmp = load_script("compare_cmp_realization") - build_dir = tmp_path / "build" - build_dir.mkdir() - run_set = compare_set.compile_runner( - REPO_ROOT / "reimplement" / "newset.c", - REPO_ROOT / "rpm-build", - build_dir, - "cc", - "g++", - ) - assert run_set.is_file() - run_cmp = compare_cmp.compile_cmp_runner(build_dir, REPO_ROOT / "rpm-build", "g++") - - completed = subprocess.run( - [str(run_cmp), PROVIDER_SET, REQUIRED_SET], - text=True, - capture_output=True, - check=True, - ) - values = dict( - line.split("\t", 1) for line in completed.stdout.splitlines() if "\t" in line - ) - - assert values["cmp_result"] == "1" diff --git a/tests/test_reimplement_set.py b/tests/test_reimplement_set.py deleted file mode 100644 index 14686c7..0000000 --- a/tests/test_reimplement_set.py +++ /dev/null @@ -1,127 +0,0 @@ -import contextlib -import io -import runpy -import unittest - -from reimplement import set as rpmset - - -class Base64AlphabetTest(unittest.TestCase): - def test_int_to_char_and_char_to_int_roundtrip_all_values(self): - for value in range(64): - self.assertEqual(rpmset.char_to_int(rpmset.int_to_char(value)), value) - - def test_int_to_char_rejects_out_of_range_values(self): - with self.assertRaises(ValueError): - rpmset.int_to_char(64) - - def test_char_to_int_rejects_non_base64_character(self): - with self.assertRaises(ValueError): - rpmset.char_to_int("=") - - -class GolombEncodingTest(unittest.TestCase): - def test_delta_roundtrip(self): - values = [1, 3, 6, 10, 31] - self.assertEqual(rpmset.decode_delta(rpmset.encode_delta(values)), values) - - def test_golomb_roundtrip(self): - values = [0, 1, 2, 7, 8, 15] - bits = rpmset.encode_golomb(values, Mshift=3) - self.assertEqual(rpmset.decode_golomb(bits, Mshift=3), values) - - def test_base64_roundtrip_preserves_bits_with_zero_padding(self): - bits = [1, 0, 1, 1, 0, 0, 1] - encoded = rpmset.encode_base64(bits) - self.assertEqual(rpmset.decode_base64(encoded)[: len(bits)], bits) - - -class SetStringTest(unittest.TestCase): - def test_encode_decode_set_roundtrip(self): - values = [1, 3, 6, 10] - encoded = rpmset.encode_set(values, bpp=8) - bpp, mshift = rpmset.decode_set_init(encoded) - self.assertEqual(bpp, 8) - self.assertEqual(rpmset.decode_set(encoded, mshift)[: len(values)], values) - - def test_set_add_fini_and_free(self): - item_set = rpmset.set_new() - rpmset.set_add(item_set, "label1") - rpmset.set_add(item_set, "label2") - - encoded = rpmset.set_fini(item_set, bpp=8) - self.assertIsNotNone(encoded) - self.assertEqual(item_set.cnt, 2) - self.assertEqual( - item_set.symbols, sorted(item_set.symbols, key=lambda item: item[1]) - ) - - rpmset.set_free(item_set) - self.assertEqual(item_set.cnt, 0) - self.assertEqual(item_set.symbols, []) - - def test_hash_is_stable_64_bit_ascii_integer(self): - self.assertEqual(rpmset.hash("ascii_symbol"), 10827468943333989194) - self.assertLessEqual(rpmset.hash("ascii_symbol"), 2**64 - 1) - - def test_hash_rejects_non_ascii_labels(self): - with self.assertRaises(UnicodeEncodeError): - rpmset.hash("юникод") - - -class DownsampleSetTest(unittest.TestCase): - def test_masks_high_half_and_keeps_sorted_unique_values(self): - self.assertEqual( - rpmset.downsample_set([1, 3, 6, 8, 10, 14], 3), [0, 1, 2, 3, 6] - ) - - def test_removes_duplicates_created_by_masking(self): - self.assertEqual(rpmset.downsample_set([1, 6, 14], 3), [1, 6]) - - def test_keeps_low_only_set_unchanged(self): - self.assertEqual(rpmset.downsample_set([1, 3, 6], 3), [1, 3, 6]) - - def test_masks_high_only_set(self): - self.assertEqual(rpmset.downsample_set([8, 10, 14], 3), [0, 2, 6]) - - -class RpmSetCmpTest(unittest.TestCase): - def encode_values(self, values, bpp=8): - return rpmset.encode_set(values, bpp) - - def test_returns_zero_for_equal_sets(self): - set1 = self.encode_values([1, 3, 5]) - set2 = self.encode_values([1, 3, 5]) - self.assertEqual(rpmset.rpmsetcmp(set1, set2), 0) - - def test_accepts_set_prefix(self): - set1 = "set:" + self.encode_values([1, 3, 5]) - set2 = self.encode_values([1, 3, 5]) - self.assertEqual(rpmset.rpmsetcmp(set1, set2), 0) - - def test_returns_one_when_first_set_is_superset(self): - set1 = self.encode_values([1, 3, 5]) - set2 = self.encode_values([1, 3]) - self.assertEqual(rpmset.rpmsetcmp(set1, set2), 1) - - def test_returns_minus_one_when_first_set_is_subset(self): - set1 = self.encode_values([1, 3]) - set2 = self.encode_values([1, 3, 5]) - self.assertEqual(rpmset.rpmsetcmp(set1, set2), -1) - - def test_returns_minus_two_for_incomparable_sets(self): - set1 = self.encode_values([1, 3, 7]) - set2 = self.encode_values([1, 3, 5]) - self.assertEqual(rpmset.rpmsetcmp(set1, set2), -2) - - -class ModuleEntrypointTest(unittest.TestCase): - def test_module_has_no_main_side_effects(self): - output = io.StringIO() - with contextlib.redirect_stdout(output): - runpy.run_path("reimplement/set.py", run_name="__main__") - self.assertEqual(output.getvalue(), "") - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_run_set.py b/tests/test_run_set.py deleted file mode 100644 index d5aae37..0000000 --- a/tests/test_run_set.py +++ /dev/null @@ -1,170 +0,0 @@ -from __future__ import annotations - -import csv -import io -import subprocess -from pathlib import Path - - -REPO_ROOT = Path(__file__).resolve().parents[1] -SOURCE = REPO_ROOT / "scripts" / "run_set.cpp" - - -def run(command: list[str], *, cwd: Path | None = None) -> subprocess.CompletedProcess[str]: - return subprocess.run(command, cwd=cwd, text=True, capture_output=True, check=True) - - -def build_run_set(tmp_path: Path) -> Path: - stub = tmp_path / "set_stub.c" - stub.write_text( - r''' -#include -#include - -struct set { - char **items; - size_t count; -}; - -struct set *set_new(void) -{ - return calloc(1, sizeof(struct set)); -} - -void set_add(struct set *set, const char *sym) -{ - set->items = realloc(set->items, sizeof(*set->items) * (set->count + 1)); - set->items[set->count++] = strdup(sym); -} - -const char *set_fini(struct set *set, int bpp) -{ - (void) set; - (void) bpp; - return strdup("stub-payload"); -} - -struct set *set_free(struct set *set) -{ - if (set) { - for (size_t i = 0; i < set->count; ++i) - free(set->items[i]); - free(set->items); - free(set); - } - return NULL; -} -''', - encoding="utf-8", - ) - stub_object = tmp_path / "set_stub.o" - executable = tmp_path / "run_set" - run(["cc", "-std=gnu11", "-O2", "-c", str(stub), "-o", str(stub_object)]) - run( - [ - "g++", - "-std=c++17", - "-O2", - "-Wall", - "-Wextra", - "-Werror", - str(SOURCE), - str(stub_object), - "-o", - str(executable), - ] - ) - return executable - - -def build_elf_fixture(tmp_path: Path) -> tuple[Path, Path]: - provider_source = tmp_path / "provider.c" - provider_source.write_text("int provided_label(void) { return 42; }\n", encoding="utf-8") - provider = tmp_path / "libprovider.so.1" - run( - [ - "cc", - "-shared", - "-fPIC", - "-Wl,-soname,libprovider.so.1", - str(provider_source), - "-o", - str(provider), - ] - ) - - consumer_source = tmp_path / "consumer.c" - consumer_source.write_text( - "extern int provided_label(void); int main(void) { return provided_label() == 42 ? 0 : 1; }\n", - encoding="utf-8", - ) - consumer = tmp_path / "consumer" - run( - [ - "cc", - str(consumer_source), - str(provider), - f"-Wl,-rpath,{tmp_path}", - "-o", - str(consumer), - ] - ) - return provider, consumer - - -def parse_output(output: str) -> tuple[dict[str, str], list[dict[str, str]]]: - lines = output.splitlines() - metadata: dict[str, str] = {} - table_start = None - for index, line in enumerate(lines): - if line.startswith("role\t"): - table_start = index - break - key, value = line.split("\t", 1) - metadata[key] = value - assert table_start is not None - rows = list(csv.DictReader(io.StringIO("\n".join(lines[table_start:])), delimiter="\t")) - return metadata, rows - - -def assert_timing_fields(row: dict[str, str]) -> None: - fields = ["set_new_ns", "set_add_total_ns", "set_fini_ns", "set_free_ns", "set_api_total_ns"] - values = {field: int(row[field]) for field in fields} - assert all(value >= 0 for value in values.values()) - assert values["set_api_total_ns"] == sum(values[field] for field in fields[:-1]) - - -def test_shared_library_builds_provided_set_and_reports_set_api_timings(tmp_path: Path) -> None: - run_set = build_run_set(tmp_path) - provider, _ = build_elf_fixture(tmp_path) - - completed = run([str(run_set), "--bpp", "16", str(provider)]) - metadata, rows = parse_output(completed.stdout) - - assert metadata["kind"] == "shared-library" - assert len(rows) == 1 - row = rows[0] - assert row["role"] == "provided" - assert row["object"] == str(provider.resolve()) - assert int(row["labels"]) >= 1 - assert row["bpp"] == "16" - assert row["set"] == "set:stub-payload" - assert_timing_fields(row) - - -def test_executable_builds_required_sets_grouped_by_provider(tmp_path: Path) -> None: - run_set = build_run_set(tmp_path) - provider, consumer = build_elf_fixture(tmp_path) - - completed = run([str(run_set), "--bpp", "16", str(consumer)]) - metadata, rows = parse_output(completed.stdout) - - assert metadata["kind"] == "executable" - provider_rows = [row for row in rows if Path(row["object"]).name == provider.name] - assert len(provider_rows) == 1 - row = provider_rows[0] - assert row["role"] == "required" - assert int(row["labels"]) >= 1 - assert row["bpp"] == "16" - assert row["set"] == "set:stub-payload" - assert_timing_fields(row)