From a182c703f43a9542bb55662b0dd68bc12872b48a Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Thu, 17 Oct 2024 03:38:36 +0200 Subject: [PATCH] Finishing testing promises. --- .../LogarithmPlotter/util/promise.py | 24 ++--- .../tests/plugins/natural/interfaces/spy.py | 2 +- runtime-pyside6/tests/plugins/natural/spy.py | 2 +- .../tests/plugins/tests/test_natural.py | 6 +- runtime-pyside6/tests/test_main.py | 3 +- runtime-pyside6/tests/test_promise.py | 98 +++++++++++++++---- runtime-pyside6/tests/test_pyjs.py | 1 - scripts/run-tests.sh | 14 +++ 8 files changed, 114 insertions(+), 36 deletions(-) diff --git a/runtime-pyside6/LogarithmPlotter/util/promise.py b/runtime-pyside6/LogarithmPlotter/util/promise.py index 3c2d223..f43e85b 100644 --- a/runtime-pyside6/LogarithmPlotter/util/promise.py +++ b/runtime-pyside6/LogarithmPlotter/util/promise.py @@ -73,10 +73,10 @@ class PyPromiseRunner(QRunnable): class PyPromise(QObject): """ - Asynchronous Promise-like object meant to interface between Python and Javascript easily. + Threaded A+/Promise implementation meant to interface between Python and Javascript easily. Runs to_run in another thread, and calls fulfilled (populated by then) with its return value. """ - fulfilled = Signal((QJSValue,), (QObject,)) + fulfilled = Signal(QJSValue) rejected = Signal(str) def __init__(self, to_run: Callable|QJSValue, args=[], start_automatically=True): @@ -115,10 +115,8 @@ class PyPromise(QObject): """ on_fulfill = check_callable(on_fulfill) on_reject = check_callable(on_reject) - if on_fulfill is not None: - self._fulfills.append(on_fulfill) - if on_reject is not None: - self._rejects.append(on_reject) + self._fulfills.append(on_fulfill) + self._rejects.append(on_reject) return self def calls_upon_fulfillment(self, function: Callable | QJSValue) -> bool: @@ -155,20 +153,22 @@ class PyPromise(QObject): def _fulfill(self, data): self._state = "fulfilled" no_return = [None, QJSValue.SpecialValue.UndefinedValue] - for on_fulfill in self._fulfills: + print("Fulfill") + for i in range(len(self._fulfills)): try: - result = on_fulfill(data) + result = self._fulfills[i](data) result = result.qjs_value if isinstance(result, PyJSValue) else result data = result if result not in no_return else data # Forward data. except Exception as e: - self._reject(repr(e)) + self._reject(repr(e), start_at=i) break @Slot(QJSValue) @Slot(str) - def _reject(self, error): + def _reject(self, error, start_at=0): self._state = "rejected" no_return = [None, QJSValue.SpecialValue.UndefinedValue] - for on_reject in self._rejects: - result = on_reject(error) + for i in range(start_at, len(self._rejects)): + result = self._rejects[i](error) + result = result.qjs_value if isinstance(result, PyJSValue) else result error = result if result not in no_return else error # Forward data. diff --git a/runtime-pyside6/tests/plugins/natural/interfaces/spy.py b/runtime-pyside6/tests/plugins/natural/interfaces/spy.py index 59b09d3..524fd71 100644 --- a/runtime-pyside6/tests/plugins/natural/interfaces/spy.py +++ b/runtime-pyside6/tests/plugins/natural/interfaces/spy.py @@ -21,7 +21,7 @@ from typing import Callable, Self from .base import Assertion, repr_, AssertionInterface from .int import NumberComparisonAssertionInterface -PRINT_PREFIX = (" " * 24) +PRINT_PREFIX = (" " * 3) class SpyAssertion(Assertion): diff --git a/runtime-pyside6/tests/plugins/natural/spy.py b/runtime-pyside6/tests/plugins/natural/spy.py index fbd8e17..2f9211a 100644 --- a/runtime-pyside6/tests/plugins/natural/spy.py +++ b/runtime-pyside6/tests/plugins/natural/spy.py @@ -30,4 +30,4 @@ class Spy: def __call__(self, *args, **kwargs): self.calls.append((args, kwargs)) if self.function is not None: - self.function(*args, **kwargs) + return self.function(*args, **kwargs) diff --git a/runtime-pyside6/tests/plugins/tests/test_natural.py b/runtime-pyside6/tests/plugins/tests/test_natural.py index d7fe26c..f381639 100644 --- a/runtime-pyside6/tests/plugins/tests/test_natural.py +++ b/runtime-pyside6/tests/plugins/tests/test_natural.py @@ -134,13 +134,13 @@ def test_add_natural_complex(): assert that(2).equals.one.minus.two def test_spy(): - spy = Spy() + spy = Spy(lambda *args, **kw: 10) assert that(spy).is_.an.instance_of(Spy) assert that(spy).is_(callable) # Check calls assert that(spy).was.never.called assert that(spy).was.called.zero.times - spy(30, arg="string") + assert spy(30, arg="string") == 10 assert that(spy).was.called assert that(spy).was.called.once assert that(spy).was.called.one.time @@ -159,7 +159,7 @@ def test_spy(): assert that(spy).was.called.with_no_argument() def test_spy_seral_calls(): - spy = Spy(lambda *args, **kw: None) + spy = Spy() obj = object() spy() spy(30, arg="string") diff --git a/runtime-pyside6/tests/test_main.py b/runtime-pyside6/tests/test_main.py index 047e21f..6b161ad 100644 --- a/runtime-pyside6/tests/test_main.py +++ b/runtime-pyside6/tests/test_main.py @@ -22,12 +22,13 @@ from os.path import exists, join from PySide6.QtGui import QIcon from tempfile import TemporaryDirectory +from .globals import app + from LogarithmPlotter.logarithmplotter import get_linux_theme, LINUX_THEMES, get_platform_qt_style, \ register_icon_directories, install_translation, create_engine from LogarithmPlotter.util import config from LogarithmPlotter.util.helper import Helper from LogarithmPlotter.util.latex import Latex -from globals import app THEMES = [ "Basic", diff --git a/runtime-pyside6/tests/test_promise.py b/runtime-pyside6/tests/test_promise.py index a97f864..30f0a1e 100644 --- a/runtime-pyside6/tests/test_promise.py +++ b/runtime-pyside6/tests/test_promise.py @@ -17,9 +17,10 @@ """ from time import sleep +import pytest from PySide6.QtQml import QJSValue -from tests.plugins.natural import that, Spy +from .plugins.natural import that, Spy from LogarithmPlotter.util.js import PyJSValue from LogarithmPlotter.util.promise import PyPromise @@ -56,6 +57,10 @@ def async_throw(): class TestPyPromise: + def test_invalid_function(self): + with pytest.raises(ValueError): + promise = PyPromise("not a function") + def test_fulfill_values(self, qtbot): qjsv = QJSValue(3) values = [ @@ -70,7 +75,6 @@ class TestPyPromise: for [value, test] in values: promise = PyPromise(create_async_func(value)) with qtbot.assertNotEmitted(promise.rejected, wait=1000): - print("Testing", value) with qtbot.waitSignal(promise.fulfilled, check_params_cb=test, timeout=2000): assert promise.state == "pending" assert promise.state == "fulfilled" @@ -84,30 +88,90 @@ class TestPyPromise: assert promise.state == "rejected" def test_fulfill(self, qtbot): - spy_fulfilled = Spy() - spy_rejected = Spy() + fulfilled = Spy() + rejected = Spy() promise = PyPromise(create_async_func(3)) - then_res = promise.then(spy_fulfilled, spy_rejected) + then_res = promise.then(fulfilled, rejected) # Check if the return value is the same promise (so we can chain then) - assert then_res == promise + assert that(then_res).does.equal(promise) # Check on our spy. with qtbot.waitSignal(promise.fulfilled, timeout=10000): pass - assert that(spy_fulfilled).was.called.once - assert that(spy_fulfilled).was.not_called.with_arguments(3) - assert that(spy_fulfilled).was.called.with_arguments_matching(check_promise_result(3)) - assert spy_rejected.was.not_called + assert that(fulfilled).was.called.once + assert that(fulfilled).was.NOT.called.with_arguments(3) + assert that(fulfilled).was.called.with_arguments_matching(check_promise_result(3)) + assert that(rejected).was.never.called def test_rejected(self, qtbot): - spy_fulfilled = Spy() - spy_rejected = Spy() + fulfilled = Spy() + rejected = Spy() promise = PyPromise(async_throw) - then_res = promise.then(spy_fulfilled, spy_rejected) + then_res = promise.then(fulfilled, rejected) # Check if the return value is the same promise (so we can chain then) - assert that(then_res).is_equal.to(promise) + assert that(then_res).does.equal(promise) # Check on our spies. with qtbot.waitSignal(promise.rejected, timeout=10000): pass - assert that(spy_rejected).was.called.once - assert that(spy_rejected).was.called.with_arguments("Exception('aaaa')") - assert that(spy_fulfilled).was.not_called + assert that(rejected).was.called.once + assert that(rejected).was.called.with_arguments("Exception('aaaa')") + assert that(fulfilled).has.never.been.called + + def test_chain_fulfill(self, qtbot): + convert = Spy(lambda v: v.toVariant()) + plus = Spy(lambda v: v + 1) + rejected = Spy() + promise = PyPromise(create_async_func(5)) + then_res = promise.then(convert, rejected).then(plus, rejected).then(plus, rejected).then(plus, rejected) + # Check if the return value is the same promise (so we can chain then) + assert that(then_res).does.equal(promise) + with qtbot.waitSignal(promise.fulfilled, timeout=10000): + pass + assert that(convert).was.called.once.with_arguments_matching(check_promise_result(5)) + assert that(rejected).was.never.called + assert that(plus).was.called.three.times + assert that(plus).was.called.once.with_exact_arguments(5) + assert that(plus).was.called.once.with_exact_arguments(6) + assert that(plus).was.called.once.with_exact_arguments(7) + + def test_chain_reject(self, qtbot): + fulfilled = Spy() + convert = Spy(lambda v: len(v)) + minus = Spy(lambda v: v - 1) + promise = PyPromise(async_throw) + then_res = promise.then(fulfilled, convert).then(fulfilled, minus).then(fulfilled, minus).then(fulfilled, minus) + # Check if the return value is the same promise (so we can chain then) + assert that(then_res).does.equal(promise) + with qtbot.waitSignal(promise.rejected, timeout=10000): + pass + assert that(fulfilled).was.never.called + assert that(convert).was.called.once.with_arguments_matching(check_promise_result("Exception('aaaa')")) + assert that(minus).was.called.three.times + assert that(minus).was.called.once.with_exact_arguments(17) + assert that(minus).was.called.once.with_exact_arguments(16) + assert that(minus).was.called.once.with_exact_arguments(15) + + def test_check_calls_upon(self): + promise = PyPromise(async_throw) + fulfilled = Spy() + rejected = Spy() + promise.then(fulfilled, rejected) + assert promise.calls_upon_fulfillment(fulfilled) + assert promise.calls_upon_rejection(rejected) + assert not promise.calls_upon_fulfillment(rejected) + assert not promise.calls_upon_rejection(fulfilled) + + def test_reject_in_fulfill(self, qtbot): + def fulfilled_throw(x): + raise Exception('noooo') + promise = PyPromise(create_async_func("3")) + fulfilled_throw = Spy(fulfilled_throw) + fulfilled = Spy() + rejected = Spy() + then_res = promise.then(fulfilled, rejected).then(fulfilled_throw, rejected).then(fulfilled, rejected).then(fulfilled, rejected) + # Check if the return value is the same promise (so we can chain then) + assert that(then_res).does.equal(promise) + with qtbot.waitSignal(promise.fulfilled, timeout=10000): + pass + assert that(fulfilled_throw).has.been.called.once + assert that(rejected).has.been.called.three.times + assert that(rejected).has.been.called.three.times.with_arguments("Exception('noooo')") \ No newline at end of file diff --git a/runtime-pyside6/tests/test_pyjs.py b/runtime-pyside6/tests/test_pyjs.py index 5742cfc..95c3cf6 100644 --- a/runtime-pyside6/tests/test_pyjs.py +++ b/runtime-pyside6/tests/test_pyjs.py @@ -21,7 +21,6 @@ from re import Pattern from PySide6.QtQml import QJSEngine, QJSValue from LogarithmPlotter.util.js import PyJSValue, InvalidAttributeValueException, NotAPrimitiveException -from globals import app @pytest.fixture() def data(): diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index ac1fdc5..8c09c54 100644 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -1,8 +1,20 @@ #!/bin/bash cd "$(dirname "$(readlink -f "$0" || realpath "$0")")/.." || exit 1 +box() { + len=${#1} + echo "┌─$(printf '─%.0s' $(seq 1 "$len"))─┐" + echo "│ $1 │" + echo "└─$(printf '─%.0s' $(seq 1 "$len"))─┘" +} + rebuild=true +cd runtime-pyside6/tests/plugins || exit 1 +box "Testing pytest natural plugins..." +PYTHONPATH="$PYTHONPATH:." pytest --cov=natural --cov-report term-missing . +cd ../../../ + while [ $# -gt 0 ]; do case "$1" in --no-rebuild) @@ -27,10 +39,12 @@ rm -rf build/runtime-pyside6/tests cp -r runtime-pyside6/tests build/runtime-pyside6 cp -r ci CHANGELOG.md build/runtime-pyside6 cd build/runtime-pyside6 || exit 1 +box "Testing runtime-pyside6..." PYTHONPATH="$PYTHONPATH:." pytest --cov=LogarithmPlotter --cov-report term-missing . cd ../../ # Run js tests cd common || exit 1 +box "Testing common..." npm test