Fixing LaTeX tests, adding new sexy natural language method spy, started testing Promises.

This commit is contained in:
Adsooi 2024-10-16 05:38:49 +02:00
parent a85a4721e3
commit 34caf20593
Signed by: Ad5001
GPG key ID: EF45F9C6AFE20160
12 changed files with 511 additions and 35 deletions

View file

@ -530,14 +530,14 @@ class CanvasAPI extends Module {
if(this.#redrawCount === currentRedrawCount) if(this.#redrawCount === currentRedrawCount)
callback(imgData) callback(imgData)
else else
console.log("1Discard render of", imgData.source, this.#redrawCount, currentRedrawCount) console.log("1. Discard render of", imgData.source, this.#redrawCount, currentRedrawCount)
}) })
} else { } else {
// Callback directly // Callback directly
if(this.#redrawCount === currentRedrawCount) if(this.#redrawCount === currentRedrawCount)
callback(imgData) callback(imgData)
else else
console.log("2Discard render of", imgData.source, this.#redrawCount, currentRedrawCount) console.log("2. Discard render of", imgData.source, this.#redrawCount, currentRedrawCount)
} }
} }
const prerendered = Latex.findPrerendered(ltxText, this.textsize, color) const prerendered = Latex.findPrerendered(ltxText, this.textsize, color)

View file

@ -23,6 +23,10 @@ const PIXEL = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVR4AWNgAAAAA
export class MockLatex { export class MockLatex {
constructor() { constructor() {
} }
get supportsAsyncRender() {
return true
}
/** /**
* Creates a simple string hash. * Creates a simple string hash.
@ -55,13 +59,23 @@ export class MockLatex {
return `${TMP}/${name}.png` return `${TMP}/${name}.png`
} }
/**
* @param {string} markup - LaTeX markup to render
* @param {number} fontSize - Font size (in pt) to render
* @param {string} color - Color of the text to render
* @returns {Promise<string>} - Comma separated data of the image (source, width, height)
*/
async renderAsync(markup, fontSize, color) {
return this.renderSync(markup, fontSize, color)
}
/** /**
* @param {string} markup - LaTeX markup to render * @param {string} markup - LaTeX markup to render
* @param {number} fontSize - Font size (in pt) to render * @param {number} fontSize - Font size (in pt) to render
* @param {string} color - Color of the text to render * @param {string} color - Color of the text to render
* @returns {string} - Comma separated data of the image (source, width, height) * @returns {string} - Comma separated data of the image (source, width, height)
*/ */
render(markup, fontSize, color) { renderSync(markup, fontSize, color) {
const file = this.__getFileName(markup, fontSize, color) const file = this.__getFileName(markup, fontSize, color)
writeFileSync(file, PIXEL, "base64") writeFileSync(file, PIXEL, "base64")
return `${file},1,1` return `${file},1,1`
@ -87,4 +101,4 @@ export class MockLatex {
checkLatexInstallation() { checkLatexInstallation() {
return true // We're not *actually* doing any latex. return true // We're not *actually* doing any latex.
} }
} }

View file

@ -75,6 +75,7 @@ class PyJSValue:
return value return value
def type(self) -> any: def type(self) -> any:
ret = None
matcher = [ matcher = [
(lambda: self.qjs_value.isArray(), list), (lambda: self.qjs_value.isArray(), list),
(lambda: self.qjs_value.isBool(), bool), (lambda: self.qjs_value.isBool(), bool),
@ -93,8 +94,9 @@ class PyJSValue:
] ]
for (test, value) in matcher: for (test, value) in matcher:
if test(): if test():
return value ret = value
return None break
return ret
def primitive(self): def primitive(self):
""" """

View file

@ -31,7 +31,7 @@ def check_callable(function: Callable|QJSValue) -> Callable|None:
""" """
if isinstance(function, QJSValue) and function.isCallable(): if isinstance(function, QJSValue) and function.isCallable():
return PyJSValue(function) return PyJSValue(function)
elif isinstance(function, Callable): elif callable(function):
return function return function
return None return None
@ -51,9 +51,7 @@ class PyPromiseRunner(QRunnable):
def run(self): def run(self):
try: try:
data = self.runner(*self.args) data = self.runner(*self.args)
if isinstance(data, QObject): if type(data) in [int, str, float, bool]:
data = data
elif type(data) in [int, str, float, bool, bytes]:
data = QJSValue(data) data = QJSValue(data)
elif data is None: elif data is None:
data = QJSValue.SpecialValue.UndefinedValue data = QJSValue.SpecialValue.UndefinedValue
@ -62,7 +60,7 @@ class PyPromiseRunner(QRunnable):
elif isinstance(data, PyJSValue): elif isinstance(data, PyJSValue):
data = data.qjs_value data = data.qjs_value
else: else:
raise InvalidReturnValue("Must return either a primitive, a valid QObject, JS Value, or None.") raise InvalidReturnValue("Must return either a primitive, a JS Value, or None.")
self.promise.fulfilled.emit(data) self.promise.fulfilled.emit(data)
except Exception as e: except Exception as e:
try: try:
@ -79,7 +77,7 @@ class PyPromise(QObject):
Runs to_run in another thread, and calls fulfilled (populated by then) with its return value. Runs to_run in another thread, and calls fulfilled (populated by then) with its return value.
""" """
fulfilled = Signal((QJSValue,), (QObject,)) fulfilled = Signal((QJSValue,), (QObject,))
rejected = Signal(Exception) rejected = Signal(str)
def __init__(self, to_run: Callable|QJSValue, args=[], start_automatically=True): def __init__(self, to_run: Callable|QJSValue, args=[], start_automatically=True):
QObject.__init__(self) QObject.__init__(self)
@ -94,7 +92,7 @@ class PyPromise(QObject):
raise ValueError("New PyPromise created with invalid function") raise ValueError("New PyPromise created with invalid function")
self._runner = PyPromiseRunner(to_run, self, args) self._runner = PyPromiseRunner(to_run, self, args)
if start_automatically: if start_automatically:
self._start() self.start()
@Slot() @Slot()
def start(self, *args, **kwargs): def start(self, *args, **kwargs):
@ -122,6 +120,35 @@ class PyPromise(QObject):
if on_reject is not None: if on_reject is not None:
self._rejects.append(on_reject) self._rejects.append(on_reject)
return self return self
def calls_upon_fulfillment(self, function: Callable | QJSValue) -> bool:
"""
Returns True if the given function will be callback upon the promise fulfillment.
False otherwise.
"""
return self._calls_in(function, self._fulfills)
def calls_upon_rejection(self, function: Callable | QJSValue) -> bool:
"""
Returns True if the given function will be callback upon the promise rejection.
False otherwise.
"""
return self._calls_in(function, self._rejects)
def _calls_in(self, function: Callable | QJSValue, within: list) -> bool:
"""
Returns True if the given function resides in the given within list, False otherwise.
Internal method of calls_upon_fulfill
"""
function = check_callable(function)
ret = False
if isinstance(function, PyJSValue):
found = next((f for f in within if f.qjs_value == function.qjs_value), None)
ret = found is not None
elif callable(function):
found = next((f for f in within if f == function), None)
ret = found is not None
return ret
@Slot(QJSValue) @Slot(QJSValue)
@Slot(QObject) @Slot(QObject)

View file

@ -0,0 +1,254 @@
"""
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 Ad5001
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from typing import Callable, Self
from PySide6.QtQml import QJSValue
PRINT_PREFIX = (" " * 24)
class SpyAssertionFailed(Exception):
def __init__(self, message, calls):
self.message = message + "\n"
if len(calls) > 0:
self.message += self.render_calls(calls)
else:
self.message += f"{PRINT_PREFIX}0 registered calls."
def repr(self, data):
if isinstance(data, QJSValue):
variant = data.toVariant()
return f"QJSValue<{type(variant).__name__}>({repr(variant)})"
else:
return repr(data)
def render_calls(self, calls):
lines = [f"{PRINT_PREFIX}{len(calls)} registered call(s):"]
for call in calls:
repr_args = [self.repr(arg) for arg in call[0]]
repr_kwargs =[f"{key}={self.repr(arg)}" for key, arg in call[1].items()]
lines.append(f" - {', '.join([*repr_args, *repr_kwargs])}")
return ("\n" + PRINT_PREFIX).join(lines)
def __str__(self):
return self.message
class Methods:
AT_LEAST_ONCE = "AT_LEAST_ONCE"
EXACTLY = "EXACTLY"
AT_LEAST = "AT_LEAST"
AT_MOST = "AT_MOST"
MORE_THAN = "AT_LEAST"
LESS_THAN = "AT_MOST"
class CalledInterface:
"""
Internal class generated by Spy.was_called.
"""
def __init__(self, calls: list[tuple[list, dict]]):
self.__calls = calls
self.__method = Methods.AT_LEAST_ONCE
self.__times = None
def __apply_method(self, calls):
required = self.__times
calls_count = len(calls)
match self.__method:
case Methods.AT_LEAST_ONCE:
compare = len(calls) >= 1
error = "Method was not called"
case Methods.EXACTLY:
compare = len(calls) == required
error = f"Method was not called {required} times ({required} != {calls_count})"
case Methods.AT_LEAST:
compare = len(calls) >= required
error = f"Method was not called at least {required} times ({required} > {calls_count})"
case Methods.AT_MOST:
compare = len(calls) <= required
error = f"Method was not called at most {required} times ({required} < {calls_count})"
case Methods.MORE_THAN:
compare = len(calls) > required
error = f"Method was not called more than {required} times ({required} >= {calls_count})"
case Methods.LESS_THAN:
compare = len(calls) < required
error = f"Method was not called less than {required} times ({required} <= {calls_count})"
case _:
raise RuntimeError(f"Unknown method {self.__method}.")
return compare, error
def __bool__(self):
"""
Converts to boolean on assertion.
"""
compare, error = self.__apply_method(self.__calls)
if not compare:
raise SpyAssertionFailed(error+".")
return compare
"""
Chaining methods
"""
def __call__(self, *args, **kwargs) -> Self:
if len(args) != 1:
raise RuntimeError("Cannot call called interface with more than one argument.")
self.__times = int(args[0])
if self.__method == Methods.AT_LEAST_ONCE:
self.__method = Methods.EXACTLY
return self
@property
def never(self) -> Self:
return self(0)
@property
def once(self) -> Self:
return self(1)
@property
def twice(self) -> Self:
return self(2)
@property
def thrice(self) -> Self:
return self(3)
@property
def at_least(self) -> Self:
self.__method = Methods.AT_LEAST
return self
@property
def at_most(self) -> Self:
self.__method = Methods.AT_MOST
return self
@property
def more_than(self) -> Self:
self.__method = Methods.MORE_THAN
return self
@property
def less_than(self) -> Self:
self.__method = Methods.LESS_THAN
return self
@property
def times(self) -> Self:
return self
"""
Class properties.
"""
def __match_calls_for_condition(self, condition: Callable[[list, dict], bool]) -> tuple[bool, str]:
calls = []
for call in self.__calls:
if condition(call[0], call[1]):
calls.append(call)
compare, error = self.__apply_method(calls)
return compare, error
def with_arguments(self, *args, **kwargs):
"""
Checks if the Spy has been called the given number of times
with at least the given arguments.
"""
def some_args_matched(a, kw):
args_matched = all((
arg in a
for arg in args
))
kwargs_matched = all((
key in kw and kw[key] == arg
for key, arg in kwargs.items()
))
return args_matched and kwargs_matched
compare, error = self.__match_calls_for_condition(some_args_matched)
if not compare:
repr_args = ', '.join([repr(arg) for arg in args])
repr_kwargs = ', '.join([f"{key}={repr(arg)}" for key, arg in kwargs.items()])
raise SpyAssertionFailed(f"{error} with arguments ({repr_args}) and keyword arguments ({repr_kwargs}).", self.__calls)
return compare
def with_arguments_matching(self, test_condition: Callable[[list, dict], bool]):
"""
Checks if the Spy has been called the given number of times
with arguments matching the given conditions.
"""
compare, error = self.__match_calls_for_condition(test_condition)
if not compare:
raise SpyAssertionFailed(f"{error} with arguments matching given conditions.", self.__calls)
return compare
def with_exact_arguments(self, *args, **kwargs):
"""
Checks if the Spy has been called the given number of times
with all the given arguments.
"""
compare, error = self.__match_calls_for_condition(lambda a, kw: a == args and kw == kwargs)
if not compare:
repr_args = ', '.join([repr(arg) for arg in args])
repr_kwargs = ', '.join([f"{key}={repr(arg)}" for key, arg in kwargs.items()])
raise SpyAssertionFailed(f"{error} with exact arguments ({repr_args}) and keyword arguments ({repr_kwargs}).", self.__calls)
return compare
def with_no_argument(self):
"""
Checks if the Spy has been called the given number of times
with all the given arguments.
"""
compare, error = self.__match_calls_for_condition(lambda a, kw: len(a) == 0 and len(kw) == 0)
if not compare:
raise SpyAssertionFailed(f"{error} with no arguments.", self.__calls)
return compare
class Spy:
"""
Class to spy into method calls with natural language expressions.
"""
def __init__(self, function: Callable = None):
self.function = function
self.calls = []
def __call__(self, *args, **kwargs):
self.calls.append((args, kwargs))
if self.function is not None:
self.function(*args, **kwargs)
@property
def was_called(self) -> CalledInterface:
"""
Returns a boolean-able interface to check conditions for a given number of
time the spy was called.
"""
return CalledInterface(self.calls)
@property
def was_not_called(self) -> CalledInterface:
"""
Returns a boolean-able interface to check that conditions were never fulfilled
in the times the spy was called.
"""
ret = CalledInterface(self.calls)
return ret(0)

View file

@ -1,3 +1,21 @@
"""
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 Ad5001
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest import pytest
from LogarithmPlotter.util import config from LogarithmPlotter.util import config
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory

View file

@ -30,7 +30,7 @@ from PySide6.QtWidgets import QApplication
from LogarithmPlotter import __VERSION__ as version from LogarithmPlotter import __VERSION__ as version
from LogarithmPlotter.util import config, helper from LogarithmPlotter.util import config, helper
from LogarithmPlotter.util.helper import Helper, InvalidFileException from LogarithmPlotter.util.helper import Helper, fetch_changelog, read_changelog, InvalidFileException
pwd = getcwd() pwd = getcwd()
helper.SHOW_GUI_MESSAGES = False helper.SHOW_GUI_MESSAGES = False
@ -60,19 +60,23 @@ CHANGELOG_BASE_PATH = path.realpath(path.join(path.dirname(path.realpath(__file_
class TestHelper: class TestHelper:
def test_changelog(self, temporary, qtbot): def test_changelog(self, temporary, qtbot):
# Exists
helper.CHANGELOG_VERSION = '0.5.0' helper.CHANGELOG_VERSION = '0.5.0'
tmpfile, directory = temporary tmpfile, directory = temporary
obj = Helper(pwd, tmpfile) obj = Helper(pwd, tmpfile)
promise = obj.fetchChangelog() promise = obj.fetchChangelog()
create_changelog_callback_asserter(promise, expect_404=False) create_changelog_callback_asserter(promise, expect_404=False)
qtbot.waitSignal(promise.fulfilled, timeout=10000) with qtbot.waitSignal(promise.fulfilled, timeout=10000):
# No exist pass
assert type(fetch_changelog()) == str
# Does not exist
helper.CHANGELOG_VERSION = '2.0.0' helper.CHANGELOG_VERSION = '2.0.0'
tmpfile, directory = temporary tmpfile, directory = temporary
obj = Helper(pwd, tmpfile) obj = Helper(pwd, tmpfile)
promise = obj.fetchChangelog() promise = obj.fetchChangelog()
create_changelog_callback_asserter(promise, expect_404=True) create_changelog_callback_asserter(promise, expect_404=True)
qtbot.waitSignal(promise.fulfilled, timeout=10000) with qtbot.waitSignal(promise.fulfilled, timeout=10000):
pass
# Local # Local
tmpfile, directory = temporary tmpfile, directory = temporary
obj = Helper(pwd, tmpfile) obj = Helper(pwd, tmpfile)
@ -81,7 +85,9 @@ class TestHelper:
assert path.exists(helper.CHANGELOG_CACHE_PATH) assert path.exists(helper.CHANGELOG_CACHE_PATH)
promise = obj.fetchChangelog() promise = obj.fetchChangelog()
create_changelog_callback_asserter(promise, expect_404=False) create_changelog_callback_asserter(promise, expect_404=False)
qtbot.waitSignal(promise.fulfilled, timeout=10000) # Local with qtbot.waitSignal(promise.fulfilled, timeout=100): # Local
pass
assert type(read_changelog()) == str
def test_read(self, temporary): def test_read(self, temporary):
# Test file reading and information loading. # Test file reading and information loading.

View file

@ -22,6 +22,7 @@ from shutil import which
from os.path import exists from os.path import exists
from re import match from re import match
from PySide6.QtGui import QColor from PySide6.QtGui import QColor
from PySide6.QtQml import QJSValue
from LogarithmPlotter.util import latex from LogarithmPlotter.util import latex
@ -38,10 +39,24 @@ def latex_obj():
directory.cleanup() directory.cleanup()
BLACK = QColor(0, 0, 0, 255)
BLUE = QColor(128, 128, 255, 255)
def check_render_results(result):
if isinstance(result, QJSValue):
result = result.toVariant()
assert type(result) == str
[path, width, height] = result.split(",")
assert exists(path)
assert match(r"\d+", width)
assert match(r"\d+", height)
return True
class TestLatex: class TestLatex:
def test_check_install(self, latex_obj: latex.Latex) -> None: def test_check_install(self, latex_obj: latex.Latex) -> None:
assert latex_obj.latexSupported == True assert latex_obj.latexSupported == True
assert latex_obj.checkLatexInstallation() == True assert latex_obj.checkLatexInstallation() == True
assert type(latex_obj.supportsAsyncRender) is bool
bkp = [latex.DVIPNG_PATH, latex.LATEX_PATH] bkp = [latex.DVIPNG_PATH, latex.LATEX_PATH]
# Check what happens when one is missing. # Check what happens when one is missing.
latex.DVIPNG_PATH = None latex.DVIPNG_PATH = None
@ -55,25 +70,25 @@ class TestLatex:
[latex.DVIPNG_PATH, latex.LATEX_PATH] = bkp [latex.DVIPNG_PATH, latex.LATEX_PATH] = bkp
def test_render_sync(self, latex_obj: latex.Latex) -> None: def test_render_sync(self, latex_obj: latex.Latex) -> None:
result = latex_obj.renderSync(r"\frac{d\sqrt{\mathrm{f}(x \times 2.3)}}{dx}", 14, QColor(0, 0, 0, 255)) result = latex_obj.renderSync("\\frac{d \\sqrt{\\mathrm{f}(x \\times 2.3)}}{dx}", 14, BLACK)
# Ensure result format # Ensure result format
assert type(result) == str check_render_results(result)
[path, width, height] = result.split(",")
assert exists(path)
assert match(r"\d+", width)
assert match(r"\d+", height)
# Ensure it returns errors on invalid latex. # Ensure it returns errors on invalid latex.
with pytest.raises(latex.RenderError): with pytest.raises(latex.RenderError):
latex_obj.renderSync(r"\nonexistant", 14, QColor(0, 0, 0, 255)) latex_obj.renderSync("\\nonexistant", 14, BLACK)
# Replace latex bin with one that returns errors # Replace latex bin with one that returns errors
bkp = latex.LATEX_PATH bkp = latex.LATEX_PATH
latex.LATEX_PATH = which("false") latex.LATEX_PATH = which("false")
with pytest.raises(latex.RenderError): with pytest.raises(latex.RenderError):
latex_obj.renderSync(r"\mathrm{f}(x)", 14, QColor(0, 0, 0, 255)) latex_obj.renderSync("\\mathrm{f}(x)", 14, BLACK)
# Replace latex bin with one goes indefinitely
latex.LATEX_PATH = which("import")
with pytest.raises(latex.RenderError):
latex_obj.renderSync("\\mathrm{f}(x)", 14, BLACK)
latex.LATEX_PATH = bkp latex.LATEX_PATH = bkp
def test_prerendered(self, latex_obj: latex.Latex) -> None: def test_prerendered(self, latex_obj: latex.Latex) -> None:
args = [r"\frac{d\sqrt{\mathrm{f}(x \times 2.3)}}{dx}", 14, QColor(0, 0, 0, 255)] args = ["\\frac{d \\sqrt{\\mathrm{f}(x \\times 2.3)}}{dx}", 14, BLACK]
latex_obj.renderSync(*args) latex_obj.renderSync(*args)
prerendered = latex_obj.findPrerendered(*args) prerendered = latex_obj.findPrerendered(*args)
assert type(prerendered) == str assert type(prerendered) == str
@ -84,3 +99,27 @@ class TestLatex:
prerendered2 = latex_obj.findPrerendered(args[0], args[1]+2, args[2]) prerendered2 = latex_obj.findPrerendered(args[0], args[1]+2, args[2])
assert prerendered2 == "" assert prerendered2 == ""
def test_render_async(self, latex_obj: latex.Latex, qtbot) -> None:
formula = "\\int\\limits^{3x}_{-\\infty}9\\mathrm{f}(x)^3+t dx"
og_promise = latex_obj.renderAsync(formula, 14, BLACK)
# Ensure we get the same locked one if we try to render it again.
assert og_promise == latex_obj.renderAsync(formula, 14, BLACK)
# Ensure queued renders.
promises = [
latex_obj.renderAsync(formula, 14, BLUE),
latex_obj.renderAsync(formula, 10, BLACK),
latex_obj.renderAsync(formula, 10, BLUE),
]
for prom in promises:
assert og_promise.calls_upon_fulfillment(prom.start)
# Ensure other renders get done in parallel.
other_promise = latex_obj.renderAsync(formula+" dt", 10, BLACK)
assert not og_promise.calls_upon_fulfillment(other_promise.start)
# Ensure all of them render.
proms = [og_promise, *promises, other_promise]
with qtbot.waitSignals(
[p.fulfilled for p in proms],
raising=True, timeout=10000,
check_params_cbs=[check_render_results]*len(proms)
):
pass

View file

@ -17,7 +17,6 @@
""" """
import pytest import pytest
from os.path import exists
from PySide6.QtCore import QEvent, QObject, QUrl from PySide6.QtCore import QEvent, QObject, QUrl
from PySide6.QtGui import QActionEvent, QFileOpenEvent from PySide6.QtGui import QActionEvent, QFileOpenEvent

View file

@ -0,0 +1,115 @@
"""
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 Ad5001
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from time import sleep
import pytest
from PySide6.QtCore import QObject
from PySide6.QtQml import QJSValue
from spy import Spy
from LogarithmPlotter.util.js import PyJSValue
from LogarithmPlotter.util.promise import PyPromise
def check_promise_result(value):
def got_result(args, kwargs, val=value):
valid = len(args) == 1 and len(kwargs) == 0
if valid:
got_value = args[0].toVariant() if isinstance(args[0], QJSValue) else args[0]
valid = got_value == val
return valid
return got_result
def create_async_func(value):
def async_function(val=value):
sleep(1)
return val
return async_function
def qjs_eq(origin):
def compare(result):
res = result.toVariant() == origin
print("Unknown res!", res, repr(result.toVariant()), repr(origin))
return res
return compare
def async_throw():
sleep(1)
raise Exception("aaaa")
class TestPyPromise:
def test_fulfill_values(self, qtbot):
qjsv = QJSValue(3)
values = [
[True, qjs_eq(True)],
[3, qjs_eq(3)],
[2.2, qjs_eq(2.2)],
["String", qjs_eq("String")],
[qjsv, qjs_eq(3)],
[None, qjs_eq(None)],
[PyJSValue(QJSValue("aaa")), qjs_eq("aaa")]
]
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"
def test_reject(self, qtbot):
promise = PyPromise(async_throw)
with qtbot.assertNotEmitted(promise.fulfilled, wait=1000):
with qtbot.waitSignal(promise.rejected, timeout=10000,
check_params_cb=lambda t: t == "Exception('aaaa')"):
assert promise.state == "pending"
assert promise.state == "rejected"
def test_fulfill(self, qtbot):
spy_fulfilled = Spy()
spy_rejected = Spy()
promise = PyPromise(create_async_func(3))
then_res = promise.then(spy_fulfilled, spy_rejected)
# Check if the return value is the same promise (so we can chain then)
assert then_res == promise
# Check on our spy.
with qtbot.waitSignal(promise.fulfilled, timeout=10000):
pass
assert spy_fulfilled.was_called.once
assert spy_fulfilled.was_not_called.with_arguments(3)
assert spy_fulfilled.was_called.with_arguments_matching(check_promise_result(3))
assert spy_rejected.was_not_called
def test_rejected(self, qtbot):
spy_fulfilled = Spy()
spy_rejected = Spy()
promise = PyPromise(async_throw)
then_res = promise.then(spy_fulfilled, spy_rejected)
# Check if the return value is the same promise (so we can chain then)
assert then_res == promise
# Check on our spies.
with qtbot.waitSignal(promise.rejected, timeout=10000):
pass
assert spy_rejected.was_called.once
assert spy_rejected.was_called.with_arguments("Exception('aaaa')")
assert spy_fulfilled.was_not_called

View file

@ -49,19 +49,20 @@ def test_update(qtbot):
update_info_newer = UpdateInformation() update_info_newer = UpdateInformation()
update_info_newer.got_update_info.connect(check_newer) update_info_newer.got_update_info.connect(check_newer)
runnable = UpdateCheckerRunnable('1.0.0', update_info_newer) runnable = UpdateCheckerRunnable('1.0.0', update_info_newer)
runnable.run() with qtbot.waitSignal(update_info_newer.got_update_info, timeout=10000):
qtbot.waitSignal(update_info_newer.got_update_info, timeout=10000) runnable.run()
runnable = UpdateCheckerRunnable('0.1.0', update_info_older) runnable = UpdateCheckerRunnable('0.1.0', update_info_older)
runnable.run() with qtbot.waitSignal(update_info_older.got_update_info, timeout=10000):
qtbot.waitSignal(update_info_older.got_update_info, timeout=10000) runnable.run()
runnable = UpdateCheckerRunnable('0.5.0+dev0+git20240101', update_info_older) runnable = UpdateCheckerRunnable('0.5.0+dev0+git20240101', update_info_older)
runnable.run() with qtbot.waitSignal(update_info_older.got_update_info, timeout=10000):
qtbot.waitSignal(update_info_older.got_update_info, timeout=10000) runnable.run()
def test_update_checker(qtbot): def test_update_checker(qtbot):
update_info = check_for_updates('0.6.0', MockWindow()) update_info = check_for_updates('0.6.0', MockWindow())
assert QThreadPool.globalInstance().activeThreadCount() >= 1 assert QThreadPool.globalInstance().activeThreadCount() >= 1
qtbot.waitSignal(update_info.got_update_info, timeout=10000) with qtbot.waitSignal(update_info.got_update_info, timeout=10000):
pass
argv.append("--no-check-for-updates") argv.append("--no-check-for-updates")
update_info = check_for_updates('0.6.0', MockWindow()) update_info = check_for_updates('0.6.0', MockWindow())
assert QThreadPool.globalInstance().activeThreadCount() < 2 # No new update checks where added assert QThreadPool.globalInstance().activeThreadCount() < 2 # No new update checks where added

View file

@ -23,6 +23,7 @@ fi
# Run python tests # Run python tests
rm -rf build/runtime-pyside6/tests
cp -r runtime-pyside6/tests build/runtime-pyside6 cp -r runtime-pyside6/tests build/runtime-pyside6
cp -r ci CHANGELOG.md build/runtime-pyside6 cp -r ci CHANGELOG.md build/runtime-pyside6
cd build/runtime-pyside6 || exit 1 cd build/runtime-pyside6 || exit 1