This commit is contained in:
lovinervy committed 2026-10-07 13:06:47 +05:00
commit c394677d07
59 files changed
+5149

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
"""Общие фикстуры и помощники для автотестов курса.
Запуск из корня курса: python -m pytest tests/ -k lab01 -v
"""
import importlib.util
import math
import os
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
def load_solution(lab_dir: str):
"""Импортирует solution.py из папки лабораторной как отдельный модуль."""
path = ROOT / lab_dir / "solution.py"
if not path.exists():
raise FileNotFoundError(f"не найден {path}")
spec = importlib.util.spec_from_file_location(f"solution_{lab_dir}", path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def close(a, b, rel=1e-6, abs_tol=1e-6):
"""Сравнение с допуском: для float-результатов (π, интеграл, checksum).
Принимает как значения, так и кортежи (value, seconds), которые
возвращают функции-заготовки студентов.
"""
if isinstance(a, tuple):
a = a[0]
if isinstance(b, tuple):
b = b[0]
if isinstance(a, int) and isinstance(b, int):
return a == b
return math.isclose(float(a), float(b), rel_tol=rel, abs_tol=abs_tol)
def source_of(lab_dir: str) -> str:
"""Исходный код solution.py — для проверки использованных механизмов."""
return (ROOT / lab_dir / "solution.py").read_text(encoding="utf-8")
+53
View File
@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
"""Автотесты лабораторной 1 (последовательное вычисление)."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from conftest import close, load_solution # noqa: E402
from common.tasks import TASKS, run_sequential, smoke_params # noqa: E402
def test_variant_number_set():
mod = load_solution("lab01_sequential")
assert 1 <= getattr(mod, "VARIANT_NUMBER", 0) <= 20, (
"Укажите свой VARIANT_NUMBER (1..20) в solution.py")
def test_cpu_sequential_correct():
mod = load_solution("lab01_sequential")
# имя задачи берём из варианта студента
from common.tasks import get_variant
name = get_variant(mod.VARIANT_NUMBER)["task_name"]
sp = smoke_params(name)
# эталон: последовательный запуск из common
expected, _ = run_sequential(name, sp, parts=1)
# последовательный запуск из 3 кусков тоже обязан совпасть
expected3, _ = run_sequential(name, sp, parts=3)
got1 = mod.run_cpu_sequential(name, sp, parts=1)
got3 = mod.run_cpu_sequential(name, sp, parts=3)
assert close(got1, expected), f"checksum(parts=1)={got1}, ожидалось {expected}"
assert close(got3, expected3), (
f"checksum зависит от разбиения: parts=3 -> {got3}, parts=1 -> {expected}")
def test_cpu_sequential_returns_time():
mod = load_solution("lab01_sequential")
from common.tasks import get_variant
name = get_variant(mod.VARIANT_NUMBER)["task_name"]
res = mod.run_cpu_sequential(name, smoke_params(name), parts=2)
assert isinstance(res, tuple) and len(res) == 2, (
"run_cpu_sequential должна возвращать (checksum, секунды)")
checksum, seconds = res
assert isinstance(seconds, (int, float)) and seconds >= 0
def test_io_sequential_correct():
mod = load_solution("lab01_sequential")
results, seconds = mod.run_io_sequential(6, 0.0)
assert results == [(i, i * i) for i in range(6)]
assert seconds >= 0
+57
View File
@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
"""Автотесты лабораторной 2 (многопоточный код)."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from conftest import load_solution, source_of # noqa: E402
def test_variant_number_set():
mod = load_solution("lab02_threads")
assert 1 <= getattr(mod, "VARIANT_NUMBER", 0) <= 20, (
"Укажите свой VARIANT_NUMBER (1..20) в solution.py")
def test_manual_threads_correct():
mod = load_solution("lab02_threads")
results, seconds = mod.run_io_threads_manual(8, 0.0, p=3)
assert [r[0] for r in results] == list(range(8)), (
"результаты должны быть упорядочены по item")
assert seconds >= 0
def test_manual_threads_parallel():
"""8 запросов по 0.1 c на 4 потоках должны занять ~0.2 c, не ~0.8 c."""
mod = load_solution("lab02_threads")
_, seconds = mod.run_io_threads_manual(8, 0.1, p=4)
assert seconds < 0.6, (
f"выполнено за {seconds:.2f} c — похоже, потоки работают по очереди "
"(start() до join()!)")
def test_pool_threads_correct():
mod = load_solution("lab02_threads")
results, seconds = mod.run_io_threads_pool(8, 0.0, p=3)
assert [r[0] for r in results] == list(range(8))
assert seconds >= 0
def test_pool_threads_parallel():
mod = load_solution("lab02_threads")
_, seconds = mod.run_io_threads_pool(8, 0.1, p=4)
assert seconds < 0.6, (
f"выполнено за {seconds:.2f} c — пул не даёт параллельности")
def test_uses_threading():
src = source_of("lab02_threads")
assert "threading" in src or "ThreadPoolExecutor" in src, (
"в решении должны использоваться потоки")
def test_manual_uses_thread_class():
src = source_of("lab02_threads")
assert "threading.Thread" in src, (
"run_io_threads_manual должна использовать threading.Thread")
+62
View File
@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
"""Автотесты лабораторной 3 (GIL)."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from conftest import close, load_solution # noqa: E402
from common.tasks import run_sequential, smoke_params # noqa: E402
def test_variant_number_set():
mod = load_solution("lab03_gil")
assert 1 <= getattr(mod, "VARIANT_NUMBER", 0) <= 20, (
"Укажите свой VARIANT_NUMBER (1..20) в solution.py")
def test_run_task_on_threads_correct():
mod = load_solution("lab03_gil")
from common.tasks import get_variant
name = get_variant(mod.VARIANT_NUMBER)["task_name"]
sp = smoke_params(name)
expected, _ = run_sequential(name, sp, parts=1)
got, seconds = mod.run_task_on_threads(name, sp, p=4)
assert close(got, expected), f"checksum {got} != {expected}"
assert seconds >= 0
def test_run_task_on_threads_uses_pool():
src = (Path(__file__).resolve().parents[1] / "lab03_gil" / "solution.py"
).read_text(encoding="utf-8")
assert "ThreadPoolExecutor" in src or "threading.Thread" in src, (
"нужны именно потоки (ThreadPoolExecutor или Thread)")
def test_measure_cpu_threads_keys():
mod = load_solution("lab03_gil")
from common.tasks import get_variant
name = get_variant(mod.VARIANT_NUMBER)["task_name"]
sp = smoke_params(name)
res = mod.measure_cpu_threads(name, sp, [1, 2, 4])
assert sorted(res.keys()) == [1, 2, 4], f"ключи {sorted(res.keys())}"
assert all(t > 0 for t in res.values())
def test_measure_gil_release_parallel():
"""hashlib на блоках 1 МБ обязан показать S(4) > 1.4."""
mod = load_solution("lab03_gil")
res = mod.measure_gil_release([1, 4], block_size=1 << 20, blocks_per_part=8)
s4 = res[1] / res[4]
assert s4 > 1.4, (
f"S(4)={s4:.2f} — hashlib не ускорился потоками. Проверьте размер "
"блоков (нужны ≥ 256 КБ) и что общий объём делится между потоками")
def test_gil_status():
mod = load_solution("lab03_gil")
ver, gil = mod.gil_status()
assert isinstance(ver, str) and ver
assert isinstance(gil, bool)
+61
View File
@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
"""Автотесты лабораторной 4 (multiprocessing).
Важная особенность: тесты запускают solution.py как ПОДПРОЦЕСС с переменной
окружения LAB_SMOKE=1 — это одновременно проверяет и корректность, и
Windows-совместимость (наличие if __name__ == "__main__").
"""
import json
import os
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
LAB_DIR = "lab04_multiprocessing"
def run_smoke():
"""Запускает solution.py в подпроцессе в smoke-режиме, возвращает JSON."""
env = dict(os.environ)
env["LAB_SMOKE"] = "1"
env["PYTHONIOENCODING"] = "utf-8"
proc = subprocess.run(
[sys.executable, str(ROOT / LAB_DIR / "solution.py")],
capture_output=True, text=True, env=env, timeout=120,
cwd=str(ROOT))
if proc.returncode != 0:
raise AssertionError(
f"solution.py упал:\n--- stdout ---\n{proc.stdout}\n--- stderr ---\n{proc.stderr}")
# smoke-режим печатает одну строку JSON с префиксом LAB_SMOKE_JSON:
for line in proc.stdout.splitlines():
if line.startswith("LAB_SMOKE_JSON:"):
return json.loads(line[len("LAB_SMOKE_JSON:"):])
raise AssertionError(
f"в smoke-режиме не найден LAB_SMOKE_JSON. stdout:\n{proc.stdout}")
def test_smoke_processes_work():
"""Запуск на процессах из подпроцесса: checksum корректен, p>1 отработал."""
out = run_smoke()
assert out.get("ok") is True, f"smoke-отчёт: {out}"
assert out.get("result_ok") is True, (
"checksum параллельного запуска не совпал с последовательным")
assert out.get("p") and out["p"] >= 2, "smoke должен запускаться при p>=2"
def test_smoke_overhead_measured():
out = run_smoke()
ov = out.get("overhead", {})
assert len(ov) >= 2, "measure_overhead должна вернуть замеры минимум для 2 размеров"
assert all(t > 0 for t in ov.values())
def test_uses_dispatch_kernel():
src = (ROOT / LAB_DIR / "solution.py").read_text(encoding="utf-8")
assert "dispatch_kernel" in src, (
"для Pool нужен пиккелируемый dispatch_kernel из common.tasks")
assert 'if __name__ == "__main__"' in src, (
"обязательна защита if __name__ == '__main__' (Windows/spawn)")
+65
View File
@@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
"""Автотесты лабораторной 5 (синхронизация)."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from conftest import load_solution # noqa: E402
def test_variant_number_set():
mod = load_solution("lab05_sync")
assert 1 <= getattr(mod, "VARIANT_NUMBER", 0) <= 20, (
"Укажите свой VARIANT_NUMBER (1..20) в solution.py")
def test_race_fixed_exact():
"""С Lock результат точен при любых запусках."""
mod = load_solution("lab05_sync")
for _ in range(3):
got, expected = mod.race_fixed(6, 10_000)
assert got == expected, (
f"race_fixed потерял обновления: {got} из {expected} — "
"Lock не защищает счётчик")
def test_race_demo_type():
mod = load_solution("lab05_sync")
got, expected = mod.race_demo(4, 5_000)
assert isinstance(got, int) and isinstance(expected, int)
assert expected == 20_000
assert 0 < got <= expected
def test_semaphore_limits():
"""max_concurrent=2 на 10 запросах по 0.1 c должно занять ~0.5 c (не ~0.1)."""
mod = load_solution("lab05_sync")
res, t = mod.semaphore_limiter(10, 0.1, 2)
assert len(res) == 10
assert t >= 0.45, (
f"выполнено за {t:.2f} c — лимит {2} одновременных не соблюдается")
# и параллельность работает: на limiter=10 было бы ~0.1 c
res2, t2 = mod.semaphore_limiter(10, 0.1, 10)
assert t2 < t * 0.6, "при большем лимите время должно заметно упасть"
def test_producer_consumer_no_deadlock():
mod = load_solution("lab05_sync")
import signal
def handler(signum, frame):
raise TimeoutError("producer_consumer не завершился — дедлок!")
try:
signal.signal(signal.SIGALRM, handler)
signal.alarm(30)
processed, total_sum = mod.producer_consumer(2, 3, 30)
signal.alarm(0)
except (AttributeError, ValueError):
# Windows: SIGALRM нет — просто вызываем
processed, total_sum = mod.producer_consumer(2, 3, 30)
assert processed == 30, f"обработано {processed} из 30 — задачи потерялись"
assert total_sum == sum(range(30)), (
f"сумма {total_sum} неверна — данные искажены")
+54
View File
@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
"""Автотесты лабораторной 6 (asyncio)."""
import asyncio
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from conftest import load_solution, source_of # noqa: E402
LAB = "lab06_asyncio"
def test_variant_number_set():
mod = load_solution(LAB)
assert 1 <= getattr(mod, "VARIANT_NUMBER", 0) <= 20, (
"Укажите свой VARIANT_NUMBER (1..20) в solution.py")
def test_fetch_all_async_correct_and_concurrent():
mod = load_solution(LAB)
results, seconds = asyncio.run(mod.fetch_all_async(8, 0.1))
assert results == [(i, i * i) for i in range(8)], (
"результаты должны быть (item, item*item) и упорядочены по item")
assert seconds < 0.5, (
f"8 запросов по 0.1 c заняли {seconds:.2f} c — конкурентности нет; "
"проверьте, что используется asyncio.sleep и await")
def test_fetch_all_limited_respects_limit():
mod = load_solution(LAB)
results, seconds = asyncio.run(mod.fetch_all_limited(10, 0.1, 2))
assert results == [(i, i * i) for i in range(10)]
assert seconds >= 0.45, (
f"10 запросов по 0.1 c при лимите 2 заняли {seconds:.2f} c "
"— лимит не соблюдается (должно быть ~0.5 c)")
# и без лимита было бы ~0.1 c
_, t_all = asyncio.run(mod.fetch_all_limited(10, 0.1, 10))
assert t_all < seconds * 0.5, "при большем лимите время должно заметно упасть"
def test_blocking_mistake_is_sequential():
"""time.sleep в корутине должен дать время ≈ items*delay."""
mod = load_solution(LAB)
seconds = mod.measure_blocking_mistake(5, 0.1)
assert seconds >= 0.45, (
f"заняло {seconds:.2f} c вместо ~0.5 c — time.sleep внезапно "
"конкурентен? Проверьте, что в _fetch_blocking именно time.sleep")
def test_uses_async_syntax():
src = source_of(LAB)
assert "async def" in src, "нужны корутины (async def)"
assert "asyncio.gather" in src, "нужен asyncio.gather"
+56
View File
@@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
"""Автотесты лабораторной 7 (сводная).
solution.py запускается подпроцессом с LAB_SMOKE=1 (как в лабе 4),
т.к. в решении используется multiprocessing.
"""
import json
import os
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
LAB_DIR = "lab07_comparison"
def run_smoke():
env = dict(os.environ)
env["LAB_SMOKE"] = "1"
env["PYTHONIOENCODING"] = "utf-8"
proc = subprocess.run(
[sys.executable, str(ROOT / LAB_DIR / "solution.py")],
capture_output=True, text=True, env=env, timeout=120,
cwd=str(ROOT))
if proc.returncode != 0:
raise AssertionError(
f"solution.py упал:\n--- stdout ---\n{proc.stdout}\n--- stderr ---\n{proc.stderr}")
for line in proc.stdout.splitlines():
if line.startswith("LAB_SMOKE_JSON:"):
return json.loads(line[len("LAB_SMOKE_JSON:"):])
raise AssertionError(f"нет LAB_SMOKE_JSON. stdout:\n{proc.stdout}")
def test_smoke_all_approaches_work():
out = run_smoke()
assert out.get("ok") is True
assert out.get("cpu_ok") is True, (
"CPU-подходы дают неверный checksum (потоки и/или процессы)")
assert out.get("io_ok") is True, "I/O-подходы вернули неверный объём результатов"
def test_smoke_io_times_present():
out = run_smoke()
times = out.get("io_times", {})
assert all(k in times for k in ("seq", "threads", "async")), times
assert all(t >= 0 for t in times.values())
def test_uses_all_four_models():
src = (ROOT / LAB_DIR / "solution.py").read_text(encoding="utf-8")
assert "ThreadPoolExecutor" in src, "нужны потоки"
assert "Pool" in src or "ProcessPoolExecutor" in src, "нужны процессы"
assert "asyncio" in src and "gather" in src, "нужен asyncio"
assert 'if __name__ == "__main__"' in src, "нужен __main__-guard"
+51
View File
@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
"""Автотесты лабораторной 8 (MPI).
Если mpi4py не установлен — тесты пропускаются (SKIPPED), но сама работа
сдаётся с запуском через mpiexec -n 2/4 (проверяет преподаватель).
Тесты выполняются в режиме ОДНОГО ранга (без mpiexec): в этом режиме
scatter/gather работают тривиально, но корректность схемы и checksum
проверяются. Многоранговую корректность гарантирует эталон и проверка
преподавателем через mpiexec.
"""
import os
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
mpi4py = pytest.importorskip("mpi4py", reason="mpi4py не установлен")
from conftest import close, load_solution # noqa: E402
from common.tasks import run_sequential, smoke_params # noqa: E402
LAB = "lab08_mpi"
def test_variant_number_set():
mod = load_solution(LAB)
assert 1 <= getattr(mod, "VARIANT_NUMBER", 0) <= 20, (
"Укажите свой VARIANT_NUMBER (1..20) в solution.py")
def test_mpi_run_single_rank():
"""Один ранг: checksum должен совпадать с последовательным запуском."""
mod = load_solution(LAB)
from common.tasks import get_variant
name = get_variant(mod.VARIANT_NUMBER)["task_name"]
sp = smoke_params(name)
expected, _ = run_sequential(name, sp, parts=1)
got = mod.mpi_run(name, sp)
assert got is not None, "mpi_run должна вернуть checksum (на любом ранге)"
assert close(got, expected), f"checksum {got} != {expected}"
def test_mpi_pi():
mod = load_solution(LAB)
pi = mod.mpi_pi(100_000)
assert 3.0 < pi < 3.3, f"pi = {pi} — вне допустимого диапазона для 100k бросков"