442 lines
16 KiB
Python
442 lines
16 KiB
Python
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()
|