add info about testing

This commit is contained in:
2026-07-28 06:24:42 +03:00
parent cea9f6b72a
commit 7518ed0b2f
12 changed files with 143 additions and 1909 deletions
+35
View File
@@ -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 |
+108
View File
@@ -3,3 +3,111 @@
Research on ALT Linux RPM set.c and dependency comparison using encoded symbol sets. Research on ALT Linux RPM set.c and dependency comparison using encoded symbol sets.
- [Docs about current set:version implementation](Docs/set:version.md) - [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,
поэтому занимает значительно больше времени, чем случайные проверки.
-384
View File
@@ -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:]))
-312
View File
@@ -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 <gudovdo@my.msu.ru>'
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
-100
View File
@@ -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"),
]
-135
View File
@@ -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
-174
View File
@@ -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 <stdlib.h>
#include <string.h>
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
-132
View File
@@ -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 <stdlib.h>
#include <string.h>
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
-319
View File
@@ -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
-56
View File
@@ -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"
-127
View File
@@ -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()
-170
View File
@@ -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 <stdlib.h>
#include <string.h>
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)