2nd_Sem_Bogachev/2025.03.07/10Ex/test_runner.py
2025-03-03 22:19:13 +03:00

442 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import json
import subprocess
import os
import time
import platform
import re
import signal
from colorama import Fore, Style, init
import math
init(autoreset=True) # Enable color support in Windows
def color_text(text, color):
"""Returns colored text."""
return color + text + Style.RESET_ALL
def cleanup_and_exit():
"""Handles cleanup on Ctrl+C or forced exit."""
print(color_text("\n[ABORT] Operation interrupted. Cleaning up...", Fore.RED))
run_command("make clean")
exit(1)
# Register Ctrl+C handler
signal.signal(signal.SIGINT, lambda sig, frame: cleanup_and_exit())
class TestCase:
"""
Represents a single test case for matrix multiplication:
n, m, k: dimensions
p: number of displayed rows/columns
kA: formula for matrix A (0 => read from file)
kB: formula for matrix B (0 => read from file)
matrix_a, matrix_b: text for input files if kA=0 or kB=0
name: optional test name
"""
def __init__(self,
n=None,
m=None,
k=None,
p=None,
kA=None,
kB=None,
matrix_a=None,
matrix_b=None,
name=None):
# Required parameters
self.n = n
self.m = m
self.k = k
self.kA = kA
self.kB = kB
# Possibly missing p => p = max(n, m, k)
self.p = p
# If kA=0 => matrix_a must be provided
self.matrix_a = matrix_a
# If kB=0 => matrix_b must be provided
self.matrix_b = matrix_b
# If no name => auto
self.name = name if name else f"Test (n={n}, m={m}, k={k}, kA={kA}, kB={kB})"
# Fix up the dimension logic:
# n not required if kA=0 => read from matrix_a lines
# m not required if kA=0 => read from matrix_a columns or lines from matrix_b
# k not required if kB=0 => read from matrix_b columns
# p => default = max(n,m,k)
self._fix_dimensions()
def _fix_dimensions(self):
"""Resolve missing n, m, k, p from matrix files if needed."""
# If kA=0 => we must figure out n,m from matrix_a
if self.kA == 0 and self.matrix_a:
lines_a = self.matrix_a.strip().split("\n")
_n = len(lines_a)
_m = len(lines_a[0].split()) if _n > 0 else 0
if not self.n:
self.n = _n
if not self.m:
self.m = _m
# If kB=0 => figure out dimensions from matrix_b
if self.kB == 0 and self.matrix_b:
lines_b = self.matrix_b.strip().split("\n")
_m2 = len(lines_b)
_k = len(lines_b[0].split()) if _m2 > 0 else 0
if not self.m:
self.m = _m2
if not self.k:
self.k = _k
# If p not specified => p = max(n,m,k)
if not self.p:
self.p = max(self.n, self.m, self.k)
def validate_inputs(self):
"""Check minimal validity for the test."""
if self.kA is None or self.kB is None:
print(color_text(f"[ERROR] Missing kA/kB in test '{self.name}'", Fore.RED))
return False
if self.kA == 0 and not self.matrix_a:
print(color_text(f"[ERROR] kA=0 but no matrix_a provided: {self.name}", Fore.RED))
return False
if self.kB == 0 and not self.matrix_b:
print(color_text(f"[ERROR] kB=0 but no matrix_b provided: {self.name}", Fore.RED))
return False
if any(x is None for x in [self.n, self.m, self.k]):
print(color_text(f"[ERROR] Dimensions not resolved in test '{self.name}'", Fore.RED))
return False
return True
class TestSuite:
"""Loads the config, builds TestCase objects, tracks exe / files."""
def __init__(self, config_file):
self.config = self.load_config(config_file)
self.exe = self.config["exe"]
# fA / fB are file names for matrix A and B input
self.fA = self.config["fA"]
self.fB = self.config["fB"]
self.tests = [TestCase(**test) for test in self.config["tests"]]
@staticmethod
def load_config(filename):
with open(filename, "r", encoding="utf-8") as f:
return json.load(f)
def run_command(cmd, exit_on_error=False):
"""Runs shell command with errors captured."""
try:
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
return result
except subprocess.CalledProcessError as e:
print(color_text(f"[ERROR] Command failed: {cmd}", Fore.RED))
print(e.stderr)
if exit_on_error:
exit(1)
return None
def wait_for_executable(exe):
"""Wait for the .exe to appear after compilation."""
print(color_text(f"[WAIT] Waiting for {exe} to be compiled...", Fore.YELLOW))
while not os.path.exists(exe):
time.sleep(0.1)
print(color_text(f"[READY] {exe} compiled successfully.", Fore.GREEN))
def format_matrix(text):
"""
Convert a raw matrix text (e.g. '1 2 3\n4 5 6') into a string with each
value formatted as '%10.3e' to match the program's output.
"""
lines = text.strip().split("\n")
out_lines = []
for line in lines:
nums = line.split()
fmt_nums = [f"{float(n):10.3e}" for n in nums]
out_lines.append(" ".join(fmt_nums))
return "\n".join(out_lines)
def parse_matrix_output(output, label):
"""
Extracts a matrix from the test program's output, e.g.:
"Initial matrix A:\n ... \n... \nInitial vector b:"
If not found, returns ''.
"""
pattern = rf"{label}:\s*\n(.*?)\n[a-zA-Z]" # match until next label or next letter
match = re.search(pattern, output, flags=re.DOTALL)
if match:
raw = match.group(1).strip()
return raw
return ""
def matrix_multiply(A, B, n, m, k):
"""
Multiply A(n×m) by B(m×k) => C(n×k).
A, B are lists of lists (floats).
Returns list of lists for C.
"""
# Initialize C
C = [[0.0]*k for _ in range(n)]
for i in range(n):
for j in range(k):
val = 0.0
for z in range(m):
val += A[i][z]*B[z][j]
C[i][j] = val
return C
def parse_matrix_lines(matrix_str):
"""
Convert matrix string (with ' %10.3e') lines => list of lists (floats).
"""
lines = matrix_str.strip().split("\n")
mat = []
for ln in lines:
row = ln.strip().split()
# row = ['1.000e+000','2.000e+000',...]
mat.append([float(x) for x in row])
return mat
def mat_to_str(mat):
"""
Convert a list-of-lists (floats) to the string with '%10.3e' format.
"""
lines = []
for row in mat:
lines.append(" ".join(f"{val:10.3e}" for val in row))
return "\n".join(lines)
def generate_matrix(n, m, k_formula):
"""
If k_formula !=0 => generate an n×m matrix using formula f(k_formula, n, m, i, j).
"""
def f(kf, n, m, i, j):
if kf == 1:
return max(n, m) - max(i, j) + 1
elif kf == 2:
return max(i, j)
elif kf == 3:
return abs(i-j)
elif kf == 4:
return 1.0/(i+j-1) if (i+j-1)!=0 else 0
return 0
mat = []
for i in range(1, n+1):
row = []
for j in range(1, m+1):
row.append(f(k_formula, n, m, i, j))
mat.append(row)
return mat
import math
def print_matrix_mismatch(expected, actual, matrix_name, test_name):
"""
Печатает подробную информацию о несовпадении двух матриц.
expected, actual: list-of-lists (float values)
matrix_name: как назвать матрицу (например, "Matrix A" или "Matrix B")
test_name: имя теста или любая другая справочная информация
"""
print(f"[FAIL] {test_name} - mismatch in {matrix_name}")
# Полный вывод матриц
print(f"\n{matrix_name} (EXPECTED):")
print(_matrix_to_str(expected))
print(f"\n{matrix_name} (ACTUAL):")
print(_matrix_to_str(actual))
# Если есть разница в размере — выводим
if len(expected) != len(actual) or any(len(e) != len(a) for e,a in zip(expected, actual)):
print("\n[INFO] Shape mismatch.")
return
# Вывод различий поэлементно
print("\n[INFO] Differences (index, expected, got):")
found_diff = False
for i, (rowE, rowA) in enumerate(zip(expected, actual)):
for j, (valE, valA) in enumerate(zip(rowE, rowA)):
if not math.isclose(valE, valA, rel_tol=1e-3, abs_tol=1e-4):
found_diff = True
print(f" [{i},{j}] {valE:10.3e} != {valA:10.3e}")
if not found_diff:
print(" No elementwise differences found (shape was likely mismatched or zero-size).")
def _matrix_to_str(matrix):
"""
Вспомогательная функция для форматирования матрицы:
печатает элементы в стиле '%10.3e' (как в тестируемой программе).
"""
lines = []
for row in matrix:
line = " ".join(f"{val:10.3e}" for val in row)
lines.append(line)
return "\n".join(lines)
def print_matrix_shape_mismatch(expected, actual, matrix_name, test_name):
"""
Выводит информацию при несовпадении размеров (shape) двух матриц.
expected, actual: list-of-lists (float values)
matrix_name: как назвать матрицу (например, "Matrix A" или "Matrix B")
test_name: имя теста или любая другая справочная информация
"""
print(f"[FAIL] {test_name} - mismatch shape in {matrix_name}")
len_expected = len(expected)
len_actual = len(actual)
print(f" {matrix_name} (EXPECTED) shape: {len_expected} x {len(expected[0]) if len_expected>0 else 0}")
print(f" {matrix_name} (ACTUAL) shape: {len_actual} x {len(actual[0]) if len_actual>0 else 0}")
# Если ещё неясно, где именно несоответствие,
# можно вывести детальнее по каждой строке:
if len_expected == len_actual:
for i, (rowE, rowA) in enumerate(zip(expected, actual)):
if len(rowE) != len(rowA):
print(f" Row {i} length mismatch: {len(rowE)} != {len(rowA)}")
def run_test(test_suite, test):
"""Main test logic."""
if not test.validate_inputs():
return
# Prepare matrix A, B => write to fA, fB if kA=0 or kB=0
# Or generate them if kA!=0 / kB!=0
# 1) build/format matrix A => string => write to fA if kA=0
# 2) build/format matrix B => string => write to fB if kB=0
fA = test_suite.fA
fB = test_suite.fB
# Build A
if test.kA == 0 and test.matrix_a:
# Write matrix_a (formatted) to fA
with open(fA, "w", encoding="utf-8") as fa:
fa.write(format_matrix(test.matrix_a) + "\n")
# Also parse it as 2D float array for local multiplication
A_list = parse_matrix_lines(format_matrix(test.matrix_a))
else:
# generate
A_list = generate_matrix(test.n, test.m, test.kA)
# Build B
if test.kB == 0 and test.matrix_b:
with open(fB, "w", encoding="utf-8") as fb:
fb.write(format_matrix(test.matrix_b) + "\n")
B_list = parse_matrix_lines(format_matrix(test.matrix_b))
else:
B_list = generate_matrix(test.m, test.k, test.kB)
# -- Build command line --
# n m k p kA [fA_if kA=0] kB [fB_if kB=0]
cmd_list = [test_suite.exe,
str(test.n), str(test.m), str(test.k),
str(test.p),
str(test.kA)]
if test.kA==0:
cmd_list.append(fA)
cmd_list.append(str(test.kB))
if test.kB==0:
cmd_list.append(fB)
cmd = " ".join(cmd_list)
# Run the program
result = run_command(cmd)
# 2) Extract matrix A => "Initial matrix A:"
# matrix B => "Initial vector b:"
# matrix C => "Result vector c:"
output = result.stdout
initA_str = parse_matrix_output(output, "Initial matrix A")
initB_str = parse_matrix_output(output, "Initial vector b")
resC_str = parse_matrix_output(output, "Result vector c")
# Convert them to list-of-lists
initA_list = parse_matrix_lines(format_matrix(initA_str))
initB_list = parse_matrix_lines(format_matrix(initB_str))
resC_list = parse_matrix_lines(format_matrix(resC_str))
# 3) Compare initA_list with A_list (both 2D arrays)
# Compare initB_list with B_list
# Then multiply A_list * B_list => localC_list
# Compare localC_list with resC_list
# Make sure shapes match.
# Compare shapes for A
if len(initA_list) != len(A_list) or any(len(rowA) != len(rowB) for rowA, rowB in zip(initA_list, A_list)):
print(color_text(f"[FAIL] {test.name} - mismatch shape in A", Fore.RED))
print_matrix_shape_mismatch(A_list, initA_list, "Matrix A", test.name)
return
# Compare each element
for rowA, rowB in zip(initA_list, A_list):
for valA, valB in zip(rowA, rowB):
if not math.isclose(valA, valB, rel_tol=1e-3, abs_tol=1e-4):
print(color_text(f"[FAIL] {test.name} - mismatch in matrix A", Fore.RED))
print_matrix_mismatch(A_list, initA_list, "Matrix A", test.name)
return
# Compare shapes for B
if len(initB_list) != len(B_list) or any(len(rA) != len(rB) for rA, rB in zip(initB_list, B_list)):
print(color_text(f"[FAIL] {test.name} - mismatch shape in B", Fore.RED))
print_matrix_shape_mismatch(B_list, initB_list, "Matrix B", test.name)
return
for rowA, rowB in zip(initB_list, B_list):
for valA, valB in zip(rowA, rowB):
if not math.isclose(valA, valB, rel_tol=1e-3, abs_tol=1e-4):
print(color_text(f"[FAIL] {test.name} - mismatch in matrix B", Fore.RED))
print_matrix_mismatch(B_list, initB_list, "Matrix B", test.name)
return
# Multiply local
localC_list = matrix_multiply(A_list, B_list, test.n, test.m, test.k)
# Compare shape with resC_list
if len(resC_list) != len(localC_list) or any(len(aRow) != len(bRow) for aRow,bRow in zip(resC_list, localC_list)):
print(color_text(f"[FAIL] {test.name} - mismatch shape in C", Fore.RED))
print_matrix_shape_mismatch(localC_list, resC_list, "Matrix A", test.name)
return
# Compare each element
for rC, lC in zip(resC_list, localC_list):
for vC, vLoc in zip(rC, lC):
if not math.isclose(vC, vLoc, rel_tol=1e-3, abs_tol=1e-4):
print(color_text(f"[FAIL] {test.name} - mismatch in matrix C", Fore.RED))
print_matrix_mismatch(resC_list, localC_list, "Matrix C", test.name)
return
print(color_text(f"[PASS] {test.name}", Fore.GREEN))
# Cleanup
if test.kA==0:
try:
os.remove(test_suite.fA)
except:
pass
if test.kB==0:
try:
os.remove(test_suite.fB)
except:
pass
def main():
print(color_text("[CLEAN] Cleaning project...", Fore.BLUE))
run_command("make clean", exit_on_error=True)
print(color_text("[BUILD] Compiling project...", Fore.BLUE))
run_command("make", exit_on_error=True)
test_suite = TestSuite("test_cases.json")
wait_for_executable(test_suite.exe)
for test in test_suite.tests:
run_test(test_suite, test)
print(color_text("[CLEAN] Final cleanup...", Fore.BLUE))
run_command("make clean")
if __name__ == "__main__":
main()