Last active
December 3, 2022 23:24
-
-
Save TheRamsay/b67c0d33ae4f4519d96cebbc9075229b to your computer and use it in GitHub Desktop.
IZP second project tester
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
# | |
# Testy pro 2. IZP projekt [2022] | |
# Autor: - Ramsay#2303 | |
# Zdroj testu: https://github.com/harmim/vut-izp-proj3/tree/master/tests | |
# Priklady pouziti: | |
# python3 ./test.py cluster --valgrind | |
import argparse | |
import json | |
import os | |
from signal import SIGSEGV | |
from subprocess import CompletedProcess, run, PIPE | |
from typing import Dict, List, Tuple, Optional | |
TEST_LOG_FILENAME = "log.json" | |
INPUT_FILENAME = "test.in" | |
VALGRIND_LOG_FILENAME = "valgrind_log.txt" | |
PASS = "\033[38;5;154m[OK]\033[0m" | |
FAIL = "\033[38;5;196m[FAIL]\033[0m" | |
WARNING = "\033[1;33m[WARNING]\033[0m" | |
BLUE = "\033[38;5;12m" | |
BOLD = "\033[1m" | |
END = "\033[0m" | |
LOREM_IPSUM = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." | |
BASE_INPUT = [ | |
("40", "86", "663"), | |
("43", "747", "938"), | |
("47", "285", "973"), | |
("49", "548", "422"), | |
("52", "741", "541"), | |
("56", "44", "854"), | |
("57", "795", "59"), | |
("61", "267", "375"), | |
("62", "85", "874"), | |
("66", "125", "211"), | |
("68", "80", "770"), | |
("72", "277", "272"), | |
("74", "222", "444"), | |
("75", "28", "603"), | |
("79", "926", "463"), | |
("83", "603", "68"), | |
("86", "238", "650"), | |
("87", "149", "304"), | |
("89", "749", "190"), | |
("93", "944", "835"), | |
] | |
RANDOM_TEXT_INPUT = [(LOREM_IPSUM,)] | |
WRONG_COUNT_INPUT = [("1", "1", "")] | |
WRONG_ID_INPUT_1 = [("xx", "1", "2")] | |
WRONG_ID_INPUT_2 = [("3.15", "1", "2")] | |
WRONG_COORDINATE_INPUT_1 = [("1", "1", "1"), ("2", "xx", "2")] | |
WRONG_COORDINATE_INPUT_2 = [("1", "1", "1"), ("2", "3.15", "2")] | |
COORDINATE_OUT_OUF_RANGE_INPUT_1 = [("1", "-1000", "2")] | |
COORDINATE_OUT_OUF_RANGE_INPUT_2 = [("1", "2000", "2")] | |
NON_UNIQUE_ID_INPUT = [("1", "1", "2"), ("1", "4", "8")] | |
MULITPLE_OBJECTS_INPUT = [("1", "1", "2", "3", "4", "5")] | |
OUTPUT_1 = [(i + 1,) for i in range(20)] | |
OUTPUT_2 = [ | |
(1, 6, 9, 11, 14, 17), | |
(2,), | |
(3,), | |
(4,), | |
(5, 15), | |
(7, 16, 19), | |
(8, 10, 12, 13, 18), | |
(20,), | |
] | |
OUTPUT_3 = [tuple(i + 1 for i in range(20))] | |
OUTPUT_4 = [(1,)] | |
class Tester: | |
def __init__( | |
self, | |
program_name: str, | |
save_logs_file: bool, | |
valgrind_enabled: bool, | |
stop_on_error: bool, | |
) -> None: | |
self.program_name = "./" + program_name | |
self.test_count = 0 | |
self.pass_count = 0 | |
self.logs: List[Dict] = [] | |
self.valgrind_enabled = valgrind_enabled | |
self.stop_on_error = stop_on_error | |
self.save_logs_file = save_logs_file | |
def test( | |
self, | |
test_name: str, | |
args: List[str], | |
filename: Optional[str], | |
input_: List[Tuple[str, str]], | |
expected_contacts: Optional[List[int]] = None, | |
should_fail: bool = False, | |
check_crash: bool = False, | |
create_file: bool = True, | |
count: Optional[int] = None, | |
): | |
self.test_count += 1 | |
failed = False | |
error_msg: str = "" | |
if filename is not None and create_file: | |
self.create_input_file(input_, filename, count) | |
str_output = ( | |
self.create_output(input_, expected_contacts) | |
if expected_contacts is not None | |
else "" | |
) | |
p: CompletedProcess[str] | |
try: | |
all_args = [] | |
if filename is not None: | |
all_args += [filename] + args | |
else: | |
all_args += args | |
p = run( | |
[self.program_name] + all_args, | |
stdout=PIPE, | |
stderr=PIPE, | |
encoding="ascii", | |
) | |
except UnicodeEncodeError as e: | |
self.print_fail(test_name) | |
print("Vystup obsahuje znaky ktere nepatri do ASCII (napr. diakritika)") | |
print(e) | |
except Exception as e: | |
self.print_fail(test_name) | |
print("Chyba pri volani programu") | |
print(e) | |
exit(1) | |
if p.returncode != 0: | |
if p.returncode == -SIGSEGV and check_crash: | |
failed = True | |
error_msg += f"Program neocekavane spadl s navratovym kodem {p.returncode}. Pravdepodobne sahas do pameti ktera neni tvoje\n" | |
elif not should_fail and not check_crash: | |
failed = True | |
error_msg += f"Program vratil chybovy navratovy kod {p.returncode} prestoze nemel\n" | |
else: | |
if should_fail: | |
failed = True | |
error_msg += "Program byl uspesne ukoncen, i presto ze nemel byt\n" | |
if ( | |
not self.assert_equal(str_output, p.stdout) | |
and not should_fail | |
and not check_crash | |
): | |
failed = True | |
error_msg += "Vystup programu se neshoduje s ocekavanym vystupem" | |
if should_fail and len(p.stderr) == 0: | |
failed = True | |
error_msg += "Program nevratil chybovou hlasku na STDERR\n" | |
valgrind_out = "" | |
if self.valgrind_enabled and (valgrind_out := self.check_memory(all_args)): | |
failed = True | |
if failed: | |
self.print_fail(test_name) | |
print(error_msg) | |
print(f"{self.bold('Argumenty')}: {' '.join(all_args)}") | |
print(f"{self.bold('Predpokladany vystup')}:") | |
print(self.debug(str_output)) | |
print(f"{self.bold('STDOUT')}:") | |
print(self.debug(p.stdout)) | |
print(f"{self.bold('STDERR')}:") | |
print(self.debug(p.stderr)) | |
if valgrind_out: | |
print(f"{self.bold('Valgrind')}:") | |
print(self.debug(valgrind_out)) | |
else: | |
self.pass_count += 1 | |
self.print_pass(test_name) | |
input_file_content = "" | |
try: | |
input_file_content = open(f"./{INPUT_FILENAME}").read() | |
except Exception as e: | |
pass | |
data = { | |
"test_name": test_name, | |
"status": "failed" if failed else "ok", | |
"error_message": error_msg, | |
"args": " ".join(all_args), | |
"file_content": input_file_content, | |
"exptected_output": str_output, | |
"stdout": p.stdout, | |
"stderr": p.stderr, | |
"return_code": p.returncode, | |
"valgrind": valgrind_out, | |
} | |
self.test_cleanup() | |
self.logs.append(data) | |
if failed and self.stop_on_error: | |
self.valgrind_cleanup() | |
if self.save_logs_file: | |
t.save_logs() | |
t.print_stats() | |
exit(1) | |
def check_memory(self, args: List[str]) -> str: | |
try: | |
run( | |
[ | |
"valgrind", | |
"--leak-check=full", | |
"--track-origins=yes", | |
"--quiet", | |
f"--log-file={VALGRIND_LOG_FILENAME}", | |
self.program_name, | |
] | |
+ args, | |
stdout=PIPE, | |
stderr=PIPE, | |
) | |
except Exception as e: | |
self.print_fail("Neporadilo se spustit valgrind") | |
print(self.debug(e)) | |
self.valgrind_cleanup() | |
return "" | |
try: | |
with open(VALGRIND_LOG_FILENAME, encoding="utf8", mode="r") as f: | |
valgrind_out = f.read() | |
return valgrind_out | |
except Exception as e: | |
self.print_fail("Neporadilo se precist valgrind log") | |
print(self.debug(e)) | |
self.valgrind_cleanup() | |
return "" | |
def print_stats(self) -> None: | |
success_rate = self.pass_count / self.test_count * 100 | |
print( | |
self.bold( | |
f"Uspesnost: {success_rate:.2f} % [{self.pass_count} / {self.test_count}]" | |
) | |
) | |
def create_input_file( | |
self, | |
input_: List[Tuple[str, str, str]], | |
filename: str, | |
count: Optional[str] = None, | |
) -> None: | |
if count is None: | |
count = str(len(input_)) | |
out = f"count={count}\n" | |
for line in input_: | |
for item in line: | |
out += f"{item} " | |
out = out[:-1] | |
out += "\n" | |
with open(f"./{filename}", encoding="utf8", mode="w") as f: | |
f.write(out) | |
def assert_equal(self, output: str, expected_output: str) -> bool: | |
lines = {line.lower() for line in expected_output.rstrip().split("\n")} | |
for line in output.rstrip().split("\n"): | |
line = line.lower() | |
if line not in lines: | |
return False | |
return True | |
def create_output( | |
self, | |
input_: List[Tuple[str, str, str]], | |
exptected_contacts: List[Tuple[int, ...]], | |
) -> str: | |
out = "Clusters:\n" | |
for idx, cluster in enumerate(exptected_contacts): | |
out += f"cluster {idx}: " | |
for obj_idx in cluster: | |
obj_id, x, y = input_[obj_idx - 1] | |
out += f"{obj_id}[{x},{y}] " | |
out = out[:-1] | |
out += "\n" | |
out = out[:-1] | |
return out | |
def save_logs(self) -> None: | |
with open(TEST_LOG_FILENAME, "w", encoding="utf8") as f: | |
json.dump(self.logs, f, indent=4) | |
@staticmethod | |
def test_cleanup() -> None: | |
try: | |
os.remove(f"./{INPUT_FILENAME}") | |
except Exception: | |
pass | |
@staticmethod | |
def valgrind_cleanup() -> None: | |
try: | |
os.remove(f"./{VALGRIND_LOG_FILENAME}") | |
except Exception: | |
pass | |
@staticmethod | |
def debug(text: str) -> str: | |
return f"{BLUE}{text}{END}" | |
@staticmethod | |
def warning(text: str) -> str: | |
return f"{WARNING}{text}{END}" | |
@staticmethod | |
def bold(text: str) -> str: | |
return f"{BOLD}{text}{END}" | |
@staticmethod | |
def print_fail(msg: str) -> None: | |
print(FAIL, msg) | |
@staticmethod | |
def print_pass(msg: str) -> None: | |
print(PASS, msg) | |
@staticmethod | |
def detect_valgrind(): | |
try: | |
run(["valgrind"], stdout=PIPE, stderr=PIPE) | |
return True | |
except: | |
print( | |
Tester.warning( | |
"Valgrind neni nainstalovan, pro kontrolu memory leaku nainstalujte valgrind" | |
) | |
) | |
return False | |
if __name__ == "__main__": | |
parser = argparse.ArgumentParser(description="Tester 2. IZP projektu [2022]") | |
parser.add_argument( | |
"program_name", | |
metavar="PROGRAM_NAME", | |
type=str, | |
help="Cesta k programu (napriklad: ./cluster)", | |
) | |
parser.add_argument( | |
"--save-logs", | |
dest="save_logs", | |
action="store_true", | |
help="Zapne ukladani logu do souboru", | |
) | |
parser.add_argument( | |
"--valgrind", | |
dest="valgrind_enabled", | |
action="store_true", | |
help="Zapne kontrolu prace s pameti pomoci valgrindu", | |
) | |
parser.add_argument( | |
"--stop-on-error", | |
dest="stop_on_error", | |
action="store_true", | |
help="Testovani programu se prerusi pri vyskytu chyby", | |
) | |
args = parser.parse_args() | |
t = Tester( | |
args.program_name, args.save_logs, args.valgrind_enabled, args.stop_on_error | |
) | |
t.test_cleanup() | |
t.test( | |
"Test ze zadani #1", | |
["20"], | |
INPUT_FILENAME, | |
BASE_INPUT, | |
OUTPUT_1, | |
create_file=True, | |
) | |
t.test( | |
"Test ze zadani #2", | |
["8"], | |
INPUT_FILENAME, | |
BASE_INPUT, | |
OUTPUT_2, | |
create_file=True, | |
) | |
t.test( | |
"Test ze zadani #3", | |
[], | |
INPUT_FILENAME, | |
BASE_INPUT, | |
OUTPUT_3, | |
create_file=True, | |
) | |
t.test( | |
"Test parametru N #1", | |
["xx"], | |
INPUT_FILENAME, | |
BASE_INPUT, | |
None, | |
should_fail=True, | |
create_file=True, | |
) | |
t.test( | |
"Test parametru N #2", | |
["2.10"], | |
INPUT_FILENAME, | |
BASE_INPUT, | |
None, | |
should_fail=True, | |
create_file=True, | |
) | |
t.test( | |
"Test neznamych parametru #1", | |
["2", "xx", "yy"], | |
INPUT_FILENAME, | |
BASE_INPUT, | |
None, | |
should_fail=True, | |
create_file=True, | |
) | |
t.test( | |
"Test chybejiciho nazvu souboru #1", | |
[], | |
None, | |
BASE_INPUT, | |
None, | |
should_fail=True, | |
) | |
t.test( | |
"Test neexistujiciho souboru #1", | |
["1"], | |
INPUT_FILENAME, | |
BASE_INPUT, | |
None, | |
should_fail=True, | |
create_file=False, | |
) | |
t.test( | |
"Test nahodneho textu #1", | |
[], | |
INPUT_FILENAME, | |
RANDOM_TEXT_INPUT, | |
[], | |
create_file=True, | |
should_fail=True, | |
count=LOREM_IPSUM, | |
) | |
t.test( | |
"Test parametru count #1", | |
["1"], | |
INPUT_FILENAME, | |
BASE_INPUT, | |
OUTPUT_3, | |
create_file=True, | |
should_fail=True, | |
count="30", | |
) | |
t.test( | |
"Test parametru count #2", | |
["1"], | |
INPUT_FILENAME, | |
BASE_INPUT, | |
OUTPUT_4, | |
create_file=True, | |
count="1", | |
) | |
t.test( | |
"Test parametru count #3", | |
["1"], | |
INPUT_FILENAME, | |
WRONG_COUNT_INPUT, | |
[], | |
create_file=True, | |
should_fail=True, | |
count="-100", | |
) | |
t.test( | |
"Test vice objektu na radku #1", | |
["1"], | |
INPUT_FILENAME, | |
MULITPLE_OBJECTS_INPUT, | |
None, | |
create_file=True, | |
should_fail=True, | |
) | |
t.test( | |
"Test ID neni cislo #1", | |
["1"], | |
INPUT_FILENAME, | |
WRONG_ID_INPUT_1, | |
None, | |
create_file=True, | |
should_fail=True, | |
) | |
t.test( | |
"Test ID neni cele cislo #1", | |
["1"], | |
INPUT_FILENAME, | |
WRONG_ID_INPUT_2, | |
None, | |
create_file=True, | |
should_fail=True, | |
) | |
t.test( | |
"Test souradnice neni cislo #1", | |
["1"], | |
INPUT_FILENAME, | |
WRONG_COORDINATE_INPUT_1, | |
None, | |
create_file=True, | |
should_fail=True, | |
) | |
t.test( | |
"Test souradnice neni cele cislo #2", | |
["1"], | |
INPUT_FILENAME, | |
WRONG_COORDINATE_INPUT_2, | |
None, | |
create_file=True, | |
should_fail=True, | |
) | |
t.test( | |
"Test souradnice je mimo rozsah #1", | |
["1"], | |
INPUT_FILENAME, | |
COORDINATE_OUT_OUF_RANGE_INPUT_1, | |
None, | |
create_file=True, | |
should_fail=True, | |
) | |
t.test( | |
"Test souradnice je mimo rozsah #2", | |
["1"], | |
INPUT_FILENAME, | |
COORDINATE_OUT_OUF_RANGE_INPUT_2, | |
None, | |
create_file=True, | |
should_fail=True, | |
) | |
t.test( | |
"Test ID neni unikatni #1", | |
["1"], | |
INPUT_FILENAME, | |
NON_UNIQUE_ID_INPUT, | |
None, | |
create_file=True, | |
should_fail=True, | |
) | |
if args.save_logs: | |
t.save_logs() | |
t.print_stats() | |
t.valgrind_cleanup() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment