basic functionallity

This commit is contained in:
2024-02-04 09:28:07 +01:00
parent 0bdd094e17
commit e677575530
6 changed files with 279 additions and 0 deletions

14
pyproject.toml Normal file
View File

@ -0,0 +1,14 @@
[build-system]
requires = ["flit_core >=3.2,<4"]
build-backend = "flit_core.buildapi"
[project]
name = "eip_testing"
authors = [{name = "Nils Pukropp", email = "nils@narl.io"}]
readme = "README.md"
license = {file = "LICENSE"}
classifiers = ["License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)"]
dynamic = ["version", "description"]
[project.urls]
Home = "https://github.com/narrrl/eip_testing"

0
src/__init__.py Normal file
View File

119
src/testing.py Normal file
View File

@ -0,0 +1,119 @@
import functools
import multiprocessing
from typing import Callable, Iterator
class TimeoutException(Exception):
pass
def get_subtasks_for(cls: type) -> Iterator[type]:
"""get all tasks in a tasks aka subtasks"""
return [e for e in cls.__dict__.values() if hasattr(e, 'is_task')]
def get_tests_for(cls: type) -> Iterator[Callable[[], None]]:
"""get all functions that are declared as tests"""
return [e for e in cls.__dict__.values() if hasattr(e, 'is_test')]
def test_wrapper(test: Callable[[], None]):
try:
test()
except Exception as e:
test.has_failed = True
test.cause = e
def run_test(test: Callable[[], None]):
"""run a test and catch any unexpected behavior and mark it as failed with the exception as cause"""
if not hasattr(test, 'is_test'):
return
timeout = test.timeout
p = multiprocessing.Process(target=test_wrapper, args=[test])
p.start()
p.join(timeout)
if p.is_alive():
test.has_failed = True
test.cause = TimeoutException(f"test failed after {timeout} seconds")
def run_tests_for_task(task: type) -> None:
for test in task.tests:
run_test(test)
for task in task.tasks:
run_tests_for_task(task)
def points_to_detuct(e: type | Callable) -> int:
match e:
case type() if hasattr(e, 'is_task'):
to_detuct = 0
for test in e.tests:
if hasattr(test, 'is_test'):
to_detuct += test.to_deduct()
for task in e.tasks:
to_detuct += points_to_detuct(task)
return e.max_points if to_detuct > e.max_points else to_detuct
case Callable(test):
return test.to_detuct()
case _:
return 0
class Exercise(object):
def __init__(self, id: str) -> None:
self.__tasks: list[type] = []
self.__id = id
self.__max_points = 0
self.__points = 0
def register(self, cls: type) -> None:
if hasattr(cls, 'is_task'):
self.__tasks.append(cls)
self.__max_points += cls.max_points
return cls
def run(self) -> str:
self.run_tests()
self.deduct_points()
def run_tests(self):
for task in self.__tasks:
run_tests_for_task(task)
def deduct_points(self):
for task in self.__tasks:
to_detuct = points_to_detuct(task)
task.points = 0 if to_detuct > task.max_points else task.max_points - to_detuct
self.__points = functools.reduce(
lambda a, b: a + b, map(lambda t: t.points, self.__tasks), 0.0)
def get_points(self) -> float:
return self.__points
def get_max_points(self) -> float:
return self.__max_points
def eip_task(header: str, max_points: float, ex: Exercise) -> Callable[[type], type]:
def wrapper(cls: type) -> type:
cls.is_task = True
cls.header = header
cls.max_points = max_points
cls.tasks = get_subtasks_for(cls)
cls.tests = get_tests_for(cls)
ex.register(cls)
return cls
return wrapper
def eip_test(msg: str, to_deduct: float, timeout = 10) -> Callable[[], None]:
def wrapper(test: Callable[[], None]) -> Callable[[], None]:
test.is_test = True
test.msg = msg
test.has_failed = False
test.to_deduct = lambda: to_deduct if test.has_failed else 0
test.timeout = timeout
test.cause = None
return test
return wrapper

29
src/tests/primes.py Normal file
View File

@ -0,0 +1,29 @@
def is_prime(n: int) -> bool:
if n < 2:
return False
for i in range(2, n // 2 + 1):
if n % i == 0:
return False
return True
def next_prime(n: int) -> int:
num = n + 1
while not is_prime(num):
num += 1
return num
def prime_factorize(n: int) -> list[int]:
prime_factors: list[int] = []
num = n
prime = 2
while num > 1:
if num % prime == 0:
prime_factors.append(prime)
num //= prime
prime = 2
else:
prime = next_prime(prime)
return prime_factors

103
src/tests/test_testing.py Normal file
View File

@ -0,0 +1,103 @@
from utils import has_annotation_callable
from testing import eip_task, eip_test, Exercise
PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37,
41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]
PRIME_FACTORS = {2: [2], 3: [3], 4: [2, 2], 5: [5], 6: [2, 3], 7: [7], 8: [2, 2, 2], 9: [3, 3], 10: [2, 5], 11: [11], 12: [2, 2, 3], 13: [13], 14: [2, 7], 15: [3, 5], 16: [2, 2, 2, 2], 17: [17], 18: [2, 3, 3], 19: [19], 20: [2, 2, 5], 21: [3, 7], 22: [2, 11], 23: [23], 24: [2, 2, 2, 3], 25: [5, 5], 26: [2, 13], 27: [3, 3, 3], 28: [2, 2, 7], 29: [29], 30: [2, 3, 5], 31: [31], 32: [2, 2, 2, 2, 2], 33: [3, 11], 34: [2, 17], 35: [5, 7], 36: [2, 2, 3, 3], 37: [37], 38: [2, 19], 39: [3, 13], 40: [2, 2, 2, 5], 41: [41], 42: [2, 3, 7], 43: [43], 44: [2, 2, 11], 45: [3, 3, 5], 46: [2, 23], 47: [47], 48: [2, 2, 2, 2, 3], 49: [7, 7], 50: [2, 5, 5], 51: [3, 17], 52: [2, 2, 13], 53: [53], 54: [
2, 3, 3, 3], 55: [5, 11], 56: [2, 2, 2, 7], 57: [3, 19], 58: [2, 29], 59: [59], 60: [2, 2, 3, 5], 61: [61], 62: [2, 31], 63: [3, 3, 7], 64: [2, 2, 2, 2, 2, 2], 65: [5, 13], 66: [2, 3, 11], 67: [67], 68: [2, 2, 17], 69: [3, 23], 70: [2, 5, 7], 71: [71], 72: [2, 2, 2, 3, 3], 73: [73], 74: [2, 37], 75: [3, 5, 5], 76: [2, 2, 19], 77: [7, 11], 78: [2, 3, 13], 79: [79], 80: [2, 2, 2, 2, 5], 81: [3, 3, 3, 3], 82: [2, 41], 83: [83], 84: [2, 2, 3, 7], 85: [5, 17], 86: [2, 43], 87: [3, 29], 88: [2, 2, 2, 11], 89: [89], 90: [2, 3, 3, 5], 91: [7, 13], 92: [2, 2, 23], 93: [3, 31], 94: [2, 47], 95: [5, 19], 96: [2, 2, 2, 2, 2, 3], 97: [97], 98: [2, 7, 7], 99: [3, 3, 11], 100: [2, 2, 5, 5]}
MAX = 100
primes_exercise = Exercise("primes")
@eip_task("`is_prime`", 4, primes_exercise)
class IsPrime:
@eip_test("`is_prime` nicht implementiert", 4)
def is_implemented():
from primes import is_prime as _
@eip_test("Manche Primzahlen evaluieren zu `False`", 2)
def test_primes():
from primes import is_prime
for prime in PRIMES:
assert is_prime(prime), f"`is_prime({prime}) = False`"
@eip_test("Manche Zahlen evaluieren zu `True` obwohl keine Primzahl", 2)
def test_non_primes():
from primes import is_prime
for not_prime in [num for num in range(4, MAX + 1) if num not in PRIMES]:
assert not is_prime(not_prime), f"`is_prime({not_prime}) = True`"
@eip_test("Typeannotation ist nicht korrekt/unvollständig", 0.5)
def test_annotation():
from primes import is_prime
has_annotation_callable(is_prime, [int], bool)
@eip_task("`next_prime`", 6, primes_exercise)
class NextPrime:
@eip_test("`next_prime` nicht implementiert", 6)
def is_implemented():
from primes import next_prime as _
@eip_test("Stimmt nicht für alle überprüften Werte", 4)
def test_next_prime():
from primes import next_prime
for num in range(2, PRIMES[len(PRIMES) - 1]):
prime = next_prime(num)
i = 0
while i < len(PRIMES) - 1 and num >= PRIMES[i + 1]:
i += 1
p1 = PRIMES[i]
p2 = PRIMES[i + 1]
assert p1 <= num < p2 and p2 == prime, f"`next_prime({num}) = {prime} != {p2}`"
@eip_test("`next_prime` evaluiert nicht `2` für Werte kleiner als `2`", 1)
def test_lower_values_next_prime():
import primes
for num in range(-2, 2):
assert primes.next_prime(num) == 2, f"`next_prime({num}) != 2`"
@eip_test("`Typannotation inkorrekt/unvollständig", 0.5)
def test_next_prime_annotation():
import primes
has_annotation_callable(primes.next_prime, [int], int)
@eip_task("c) `prime_factorize`", 10, primes_exercise)
class PrimeFactorize:
@eip_test("`prime_factorize` nicht implementiert", 10)
def prime_factorize_implemented():
from primes import prime_factorize as _
@eip_test("`prime_factorize` gibt nicht immer die korrekten Faktoren", 8)
def test_prime_factorize():
import primes
for prime in PRIMES:
assert primes.prime_factorize(prime) == [prime], f"prime_factorize({prime}) = {primes.prime_factorize(prime)} != [{prime}]"
prime_factors = [(n, primes.prime_factorize(n))
for n in range(2, MAX + 1)]
for n, factors in prime_factors:
for factor in factors:
assert factor in PRIMES, f"{factor} in {factors} is not prime"
assert PRIME_FACTORS[n] == factors, f"prime_factorize({n}) = {factors} != {PRIME_FACTORS[n]}"
@eip_test("`prime_factorize` gibt keine leere Liste für Werte kleiner 2", 2)
def test_lower_values_next_prime():
import primes
for num in range(-2, 2):
assert primes.next_prime(num) == 2
@eip_test("`Typannotation inkorrekt/unvollständig", 0.5)
def test_type_annotation():
import primes
has_annotation_callable(primes.prime_factorize, [int], list[int])
if __name__ == "__main__":
primes_exercise.run()
print(f"# exercise ({primes_exercise.get_points():.2g} / {primes_exercise.get_max_points()})")

14
src/utils.py Normal file
View File

@ -0,0 +1,14 @@
from typing import Callable, TypeAliasType
def has_annotation_callable(f: Callable, param_types: list[type], return_type: object = None):
a = f.__annotations__
ret = a['return'] if 'return' in a else None
assert ret == return_type
params = list(a.values())[:-1] if 'return' in a else list(a.values())
for param, param_type in zip(params, param_types):
while isinstance(param, TypeAliasType):
param = param.__value__
while isinstance(param_type, TypeAliasType):
param_type = param_type.__value__
assert param == param_type