add info about testing
This commit is contained in:
@@ -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 |
|
||||||
@@ -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,
|
||||||
|
поэтому занимает значительно больше времени, чем случайные проверки.
|
||||||
|
|||||||
@@ -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:]))
|
|
||||||
@@ -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
|
|
||||||
@@ -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"),
|
|
||||||
]
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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"
|
|
||||||
@@ -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()
|
|
||||||
@@ -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)
|
|
||||||
Reference in New Issue
Block a user