basic functionallity
This commit is contained in:
14
pyproject.toml
Normal file
14
pyproject.toml
Normal 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
0
src/__init__.py
Normal file
119
src/testing.py
Normal file
119
src/testing.py
Normal 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
29
src/tests/primes.py
Normal 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
103
src/tests/test_testing.py
Normal 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
14
src/utils.py
Normal 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
|
Reference in New Issue
Block a user