Compare commits

..

4 commits

Author SHA1 Message Date
a182c703f4
Finishing testing promises.
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-17 03:38:36 +02:00
ef465b34e7
Finishing natural language plugin. 2024-10-17 02:08:24 +02:00
8fab9d8e52
Starting natural language plugin. 2024-10-16 22:18:53 +02:00
34caf20593
Fixing LaTeX tests, adding new sexy natural language method spy, started testing Promises. 2024-10-16 05:38:49 +02:00
27 changed files with 1546 additions and 49 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:
@ -75,11 +73,11 @@ class PyPromiseRunner(QRunnable):
class PyPromise(QObject): 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. 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(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):
@ -117,31 +115,60 @@ class PyPromise(QObject):
""" """
on_fulfill = check_callable(on_fulfill) on_fulfill = check_callable(on_fulfill)
on_reject = check_callable(on_reject) on_reject = check_callable(on_reject)
if on_fulfill is not None: self._fulfills.append(on_fulfill)
self._fulfills.append(on_fulfill) self._rejects.append(on_reject)
if on_reject is not None:
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)
def _fulfill(self, data): def _fulfill(self, data):
self._state = "fulfilled" self._state = "fulfilled"
no_return = [None, QJSValue.SpecialValue.UndefinedValue] no_return = [None, QJSValue.SpecialValue.UndefinedValue]
for on_fulfill in self._fulfills: print("Fulfill")
for i in range(len(self._fulfills)):
try: try:
result = on_fulfill(data) result = self._fulfills[i](data)
result = result.qjs_value if isinstance(result, PyJSValue) else result result = result.qjs_value if isinstance(result, PyJSValue) else result
data = result if result not in no_return else data # Forward data. data = result if result not in no_return else data # Forward data.
except Exception as e: except Exception as e:
self._reject(repr(e)) self._reject(repr(e), start_at=i)
break break
@Slot(QJSValue) @Slot(QJSValue)
@Slot(str) @Slot(str)
def _reject(self, error): def _reject(self, error, start_at=0):
self._state = "rejected" self._state = "rejected"
no_return = [None, QJSValue.SpecialValue.UndefinedValue] no_return = [None, QJSValue.SpecialValue.UndefinedValue]
for on_reject in self._rejects: for i in range(start_at, len(self._rejects)):
result = on_reject(error) 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. error = result if result not in no_return else error # Forward data.

View file

View file

@ -0,0 +1,22 @@
"""
* 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 .spy import Spy
from .that import that
from .interfaces.base import Assertion

View file

@ -0,0 +1,39 @@
"""
* 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/>.
"""
class Assertion(Exception):
def __init__(self, assertion: bool, message: str, invert: bool):
self.assertion = assertion
self.message = message
self.invert = invert
def _invert_message(self):
for verb in ('is', 'was', 'has', 'have'):
for negative in ("n't", ' not', ' never', ' no'):
self.message = self.message.replace(f"{verb}{negative}", verb.upper())
def __str__(self):
return self.message
def __bool__(self):
if not self.invert and not self.assertion:
raise self
if self.invert and self.assertion:
self._invert_message()
raise self
return True # Raises otherwise.

View file

@ -0,0 +1,171 @@
"""
* 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 Self, Callable, Any
from .assertion import Assertion
from .utils import repr_
class AssertionInterface:
"""
Most basic assertion interface.
You probably want to use BaseAssertionInterface
"""
def __init__(self, value, parent: Self = None):
self._value = value
self._parent = parent
if parent is None:
self.__not = False
@property
def _not(self) -> bool:
"""
Internal state of whether the expression was negated.
Use "not_" to set it.
:return:
"""
return self.__not if self._parent is None else self._parent._not
@_not.setter
def _not(self, value: bool):
if self._not is True:
raise RuntimeError("Cannot call is_not or was_not twice in the same statement.")
if self._parent is None:
self.__not = True
else:
self._parent._not = True
def instance_of(self, type_: type) -> Assertion:
"""
Checks if the current value is equal to the provided value
"""
value_type_name = type(self._value).__name__
if not isinstance(type_, type):
raise RuntimeError("Provided 'type' provided is not a class.")
return Assertion(
isinstance(self._value, type_),
f"The value ({value_type_name} {repr_(self._value)}) is not a {type_.__name__}.",
self._not
)
def __call__(self, condition: Callable[[Any], bool]) -> Assertion:
"""
Apply condition to value that returns whether or not the value is valid.
"""
return Assertion(
condition(self._value),
f"The value ({repr_(self._value)}) did not match given conditions.",
self._not
)
"""
NOT Properties.
"""
@property
def NOT(self) -> Self:
self._not = True
return self
@property
def not_(self) -> Self:
self._not = True
return self
@property
def never(self) -> Self:
self._not = True
return self
"""
Chain self properties to sound natural
"""
@property
def that(self) -> Self:
return self
@property
def is_(self) -> Self:
return self
@property
def does(self) -> Self:
return self
@property
def was(self) -> Self:
return self
@property
def been(self) -> Self:
return self
@property
def have(self) -> Self:
return self
@property
def has(self) -> Self:
return self
@property
def a(self) -> Self:
return self
@property
def an(self) -> Self:
return self
class EqualAssertionInterface(AssertionInterface):
"""
Interface created for when its value should be checked for equality
"""
def __init__(self, value, parent: AssertionInterface = None):
super().__init__(value, parent)
def __call__(self, value) -> Assertion:
return Assertion(
value == self._value,
f"The value {repr_(self._value)} is different from {repr(value)}.",
self._not
)
@property
def to(self) -> Self:
return self
class BaseAssertionInterface(AssertionInterface):
@property
def equals(self) -> EqualAssertionInterface:
"""
Checks if the current value is equal to the provided value
"""
return EqualAssertionInterface(self._value, self)
@property
def equal(self) -> EqualAssertionInterface:
"""
Checks if the current value is equal to the provided value
"""
return self.equals

View file

@ -0,0 +1,83 @@
"""
* 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 .assertion import Assertion
from .base import BaseAssertionInterface
from .int import NumberInterface
from .utils import repr_
class FixedIteratorInterface(BaseAssertionInterface):
@property
def length(self) -> NumberInterface:
return NumberInterface(len(self._value), self)
def elements(self, *elements) -> Assertion:
tests = [repr_(elem) for elem in elements if elem not in self._value]
return Assertion(
len(tests) == 0,
f"This value ({repr_(self._value)}) does not have elements {', '.join(tests)}.",
self._not
)
def element(self, element) -> Assertion:
return Assertion(
element in self._value,
f"This value ({repr_(self._value)}) does not have element {repr_(element)}.",
self._not
)
def contains(self, *elements) -> Assertion:
"""
Check if the element(s) are contained in the iterator.
"""
if len(elements) == 1:
return self.element(elements[0])
else:
return self.elements(*elements)
def contain(self, *elements):
"""
Check if the element(s) are contained in the iterator.
"""
return self.contains(*elements)
class BoolInterface(BaseAssertionInterface):
@property
def true(self):
return Assertion(
self._value == True,
f"The value ({repr_(self._value)}) is not True.",
self._not
)
@property
def false(self):
return Assertion(
self._value == False,
f"The value ({repr_(self._value)}) is not False.",
self._not
)
class StringInterface(FixedIteratorInterface):
pass
class ListInterface(FixedIteratorInterface):
pass

View file

@ -0,0 +1,321 @@
"""
* 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 idlelib.configdialog import is_int
from math import log10, floor, ceil
from typing import Self
from .assertion import Assertion
from .base import AssertionInterface
from .utils import repr_
class NumberComparisonAssertionInterface(AssertionInterface):
def __init__(self, value, parent: AssertionInterface = None):
super().__init__(value, parent)
self._compare_stack = []
def _generate_compare_to(self) -> int:
"""
The number generated by the comparison stack.
E.g. can parse one.hundred.million.and.thirty.three.thousand.and.twelve.hundred.and.seven
as ['one', 'hundred', 'million', 'thirty', 'three', 'thousand', 'twelve', 'hundred', 'seven']
which results 100,034,207
"""
minus = len(self._compare_stack) > 0 and self._compare_stack[0] == -1
if len(self._compare_stack) < (2 if minus else 1):
raise RuntimeError("No number to compare the value to provided.")
if minus:
self._compare_stack.pop(0)
# Compute the number
add_stack = [self._compare_stack.pop(0)]
for element in self._compare_stack:
last_power = floor(log10(abs(add_stack[-1])))
current_power = floor(log10(abs(element)))
if last_power < current_power: # E.g. one hundred
add_stack[-1] *= element
elif last_power == 1 and current_power == 0: # E.g thirty four
add_stack[-1] += element
elif last_power > current_power: # E.g a hundred and five
add_stack.append(element)
else:
raise RuntimeError(f"Cannot chain two numbers with the same power ({add_stack[-1]} => {element}.")
total = sum(add_stack)
return -total if minus else total
def _compare(self) -> Assertion:
raise RuntimeError(f"No comparison method defined in {type(self).__name__}.")
def __bool__(self) -> bool:
return bool(self._compare())
def __call__(self, compare_to: int) -> Self:
if type(compare_to) not in (float, int):
raise RuntimeError(f"Cannot compare number ({self._value}) to non number ({repr_(compare_to)}).")
self._compare_stack.append(compare_to)
return self
"""
Chain self properties
"""
@property
def and_(self) -> Self:
return self
@property
def AND(self) -> Self:
return self
"""
Number shorthands
"""
@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 minus(self) -> Self:
return self(-1)
@property
def zero(self) -> Self:
return self(0)
@property
def one(self) -> Self:
return self(1)
@property
def two(self) -> Self:
return self(2)
@property
def three(self) -> Self:
return self(3)
@property
def four(self) -> Self:
return self(4)
@property
def five(self) -> Self:
return self(5)
@property
def six(self) -> Self:
return self(6)
@property
def seven(self) -> Self:
return self(7)
@property
def eight(self) -> Self:
return self(8)
@property
def nine(self) -> Self:
return self(9)
@property
def ten(self) -> Self:
return self(10)
@property
def eleven(self) -> Self:
return self(11)
@property
def twelve(self) -> Self:
return self(12)
@property
def thirteen(self) -> Self:
return self(13)
@property
def fourteen(self) -> Self:
return self(14)
@property
def fifteen(self) -> Self:
return self(15)
@property
def sixteen(self) -> Self:
return self(16)
@property
def seventeen(self) -> Self:
return self(17)
@property
def eighteen(self) -> Self:
return self(18)
@property
def nineteen(self) -> Self:
return self(19)
@property
def twenty(self) -> Self:
return self(20)
@property
def thirty(self) -> Self:
return self(30)
@property
def forty(self) -> Self:
return self(40)
@property
def fifty(self) -> Self:
return self(50)
@property
def sixty(self) -> Self:
return self(60)
@property
def seventy(self) -> Self:
return self(70)
@property
def eighty(self) -> Self:
return self(80)
@property
def ninety(self) -> Self:
return self(90)
@property
def hundred(self) -> Self:
return self(100)
@property
def thousand(self) -> Self:
return self(1_000)
@property
def million(self) -> Self:
return self(1_000_000)
@property
def billion(self) -> Self:
return self(1_000_000_000)
class LessThanComparisonInterface(NumberComparisonAssertionInterface):
def _compare(self) -> Assertion:
compare = self._generate_compare_to()
return Assertion(
self._value < compare,
f"The value ({repr_(self._value)}) is not less than to {repr_(compare)}.",
self._not
)
class MoreThanComparisonInterface(NumberComparisonAssertionInterface):
def _compare(self) -> Assertion:
compare = self._generate_compare_to()
return Assertion(
self._value > compare,
f"The value ({repr_(self._value)}) is not more than to {repr_(compare)}.",
self._not
)
class AtLeastComparisonInterface(NumberComparisonAssertionInterface):
def _compare(self) -> Assertion:
compare = self._generate_compare_to()
return Assertion(
self._value >= compare,
f"The value ({repr_(self._value)}) is not at least to {repr_(compare)}.",
self._not
)
class AtMostComparisonInterface(NumberComparisonAssertionInterface):
def _compare(self) -> Assertion:
compare = self._generate_compare_to()
return Assertion(
self._value <= compare,
f"The value ({repr_(self._value)}) is not at least to {repr_(compare)}.",
self._not
)
class EqualComparisonInterface(NumberComparisonAssertionInterface):
def _compare(self) -> Assertion:
compare = self._generate_compare_to()
return Assertion(
self._value == compare,
f"The value ({repr_(self._value)}) is not equal to {repr_(compare)}.",
self._not
)
@property
def to(self) -> Self:
return self
class NumberInterface(AssertionInterface):
def __call__(self, value):
return EqualComparisonInterface(self._value, self)(value)
@property
def equals(self) -> EqualComparisonInterface:
return EqualComparisonInterface(self._value, self)
@property
def equal(self) -> EqualComparisonInterface:
return EqualComparisonInterface(self._value, self)
@property
def exactly(self) -> EqualComparisonInterface:
return EqualComparisonInterface(self._value, self)
@property
def of(self) -> EqualComparisonInterface:
return EqualComparisonInterface(self._value, self)
@property
def less_than(self) -> LessThanComparisonInterface:
return LessThanComparisonInterface(self._value, self)
@property
def more_than(self) -> MoreThanComparisonInterface:
return MoreThanComparisonInterface(self._value, self)
@property
def at_least(self) -> AtLeastComparisonInterface:
return AtLeastComparisonInterface(self._value, self)
@property
def at_most(self) -> AtMostComparisonInterface:
return AtMostComparisonInterface(self._value, self)

View file

@ -0,0 +1,218 @@
"""
* 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 .base import Assertion, repr_, AssertionInterface
from .int import NumberComparisonAssertionInterface
PRINT_PREFIX = (" " * 3)
class SpyAssertion(Assertion):
def __init__(self, assertion: bool, message: str, calls: list, invert: bool):
super().__init__(assertion, message + "\n", invert)
if len(calls) > 0:
self.message += self.render_calls(calls)
else:
self.message += f"{PRINT_PREFIX}0 registered calls."
def render_calls(self, calls):
lines = [f"{PRINT_PREFIX}{len(calls)} registered call(s):"]
for call in calls:
repr_args = [repr_(arg) for arg in call[0]]
repr_kwargs = [f"{key}={repr_(arg)}" for key, arg in call[1].items()]
lines.append(f" - {', '.join([*repr_args, *repr_kwargs])}")
return ("\n" + PRINT_PREFIX).join(lines)
class Methods:
AT_LEAST_ONCE = "AT_LEAST_ONCE"
EXACTLY = "EXACTLY"
AT_LEAST = "AT_LEAST"
AT_MOST = "AT_MOST"
MORE_THAN = "MORE_THAN"
LESS_THAN = "LESS_THAN"
class CalledInterface(NumberComparisonAssertionInterface):
"""
Internal class generated by Spy.called.
"""
def __init__(self, calls: list[tuple[list, dict]], parent: AssertionInterface):
super().__init__(len(calls), parent)
self.__calls = calls
self.__method = Methods.AT_LEAST_ONCE
def __apply_method(self, calls):
required = None if self._compare_stack == [] else self._generate_compare_to()
calls_count = len(calls)
match self.__method:
case Methods.AT_LEAST_ONCE:
compare = len(calls) >= 1
error = f"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) -> bool:
"""
Converts to boolean on assertion.
"""
compare, error = self.__apply_method(self.__calls)
return bool(SpyAssertion(compare, error + ".", self.__calls, self._not))
"""
Chaining methods
"""
def __call__(self, compare_to: int) -> Self:
super().__call__(compare_to)
if self.__method == Methods.AT_LEAST_ONCE:
self.__method = Methods.EXACTLY
return self
@property
def at_least(self) -> Self:
if self.__method == Methods.AT_LEAST_ONCE:
self.__method = Methods.AT_LEAST
else:
raise RuntimeError(f"Cannot redefine method from {self.__method} to {Methods.AT_MOST}")
return self
@property
def at_most(self) -> Self:
if self.__method == Methods.AT_LEAST_ONCE:
self.__method = Methods.AT_MOST
else:
raise RuntimeError(f"Cannot redefine method from {self.__method} to {Methods.AT_MOST}")
return self
@property
def more_than(self) -> Self:
if self.__method == Methods.AT_LEAST_ONCE:
self.__method = Methods.MORE_THAN
else:
raise RuntimeError(f"Cannot redefine method from {self.__method} to {Methods.MORE_THAN}")
return self
@property
def less_than(self) -> Self:
if self.__method == Methods.AT_LEAST_ONCE:
self.__method = Methods.LESS_THAN
else:
raise RuntimeError(f"Cannot redefine method from {self.__method} to {Methods.LESS_THAN}")
return self
@property
def time(self) -> Self:
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) -> SpyAssertion:
"""
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)
repr_args = ', '.join([repr(arg) for arg in args])
repr_kwargs = ', '.join([f"{key}={repr(arg)}" for key, arg in kwargs.items()])
msg = f"{error} with arguments ({repr_args}) and keyword arguments ({repr_kwargs})."
return SpyAssertion(compare, msg, self.__calls, self._not)
def with_arguments_matching(self, test_condition: Callable[[list, dict], bool]) -> SpyAssertion:
"""
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)
msg = f"{error} with arguments matching given conditions."
return SpyAssertion(compare, msg, self.__calls, self._not)
def with_exact_arguments(self, *args, **kwargs) -> SpyAssertion:
"""
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)
repr_args = ', '.join([repr(arg) for arg in args])
repr_kwargs = ', '.join([f"{key}={repr(arg)}" for key, arg in kwargs.items()])
msg = f"{error} with exact arguments ({repr_args}) and keyword arguments ({repr_kwargs})."
return SpyAssertion(compare, msg, self.__calls, self._not)
def with_no_argument(self) -> SpyAssertion:
"""
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)
return SpyAssertion(compare, f"{error} with no arguments.", self.__calls, self._not)
class SpyAssertionInterface(AssertionInterface):
@property
def called(self) -> CalledInterface:
"""
Returns a boolean-able interface to check conditions for a given number of
time the spy was called.
"""
return CalledInterface(self._value.calls, self)

View file

@ -0,0 +1,27 @@
"""
* 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 PySide6.QtQml import QJSValue
def repr_(data):
if isinstance(data, QJSValue):
variant = data.toVariant()
return f"QJSValue<{type(variant).__name__}>({repr(variant)})"
else:
return repr(data)

View file

@ -0,0 +1,33 @@
"""
* 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
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:
return self.function(*args, **kwargs)

View file

@ -0,0 +1,68 @@
"""
* 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 overload, Generic, TypeVar
from . import Spy
from .interfaces.base import AssertionInterface, BaseAssertionInterface
from .interfaces.basic import StringInterface, BoolInterface, ListInterface
from .interfaces.int import NumberInterface
from .interfaces.spy import SpyAssertionInterface
Interface = TypeVar("Interface", bound=AssertionInterface)
MATCHES = [
(str, StringInterface),
(bool, BoolInterface),
(int, NumberInterface),
(float, NumberInterface),
(list, ListInterface),
(Spy, SpyAssertionInterface)
]
@overload
def that(value: str) -> StringInterface: ...
@overload
def that(value: bool) -> BoolInterface: ...
@overload
def that(value: int) -> NumberInterface: ...
@overload
def that(value: float) -> NumberInterface: ...
@overload
def that(value: Spy) -> SpyAssertionInterface: ...
@overload
def that[Interface](value: Interface) -> Interface: ...
def that(value: any) -> AssertionInterface:
if not isinstance(value, AssertionInterface):
interface = next((i for t, i in MATCHES if isinstance(value, t)), None)
if interface is not None:
value = interface(value)
else:
value = BaseAssertionInterface(value)
return value

View file

@ -0,0 +1,217 @@
"""
* 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
from ..natural import that, Assertion, Spy
def test_string():
assert that("QWERTY").is_.an.instance_of(str)
assert that("QWERTY").is_.not_.an.instance_of(int)
assert that("QWERTY").is_.equal.to("QWERTY")
assert that("QWERTY").is_.NOT.equal.to("QWERTYUIOP")
assert that("QWERTY").is_.NOT.equal.to(3)
assert that("QWERTY").has.a.length.of(6)
assert that("QWERTY").does.NOT.have.a.length.of(7)
assert that("QWERTY").has.a.length.that.is_.NOT(5)
assert that("QWERTY").contains("WER")
assert that("QWERTY").contains("WER", "TY")
assert that("QWERTY").does.not_.contain("AZERTY")
with pytest.raises(Assertion):
assert that("QWERTY").is_.an.instance_of(int)
with pytest.raises(Assertion):
assert that("QWERTY").is_.equal.to(False)
with pytest.raises(Assertion):
assert that("QWERTY").has.a.length.of(1)
with pytest.raises(Assertion):
assert that("QWERTY").contains("AZERTY")
with pytest.raises(Assertion):
assert that("QWERTY").does.NOT.contain("QWE")
def test_bool():
assert that(True).is_.an.instance_of(bool)
assert that(True).is_.an.instance_of(int)
assert that(True).is_.NOT.an.instance_of(str)
assert that(True).equals(True)
assert that(True).is_.true
assert that(True).is_.NOT.false
assert that(False).is_.equal.to(False)
assert that(False).is_.false
assert that(False).is_.NOT.true
with pytest.raises(Assertion):
assert that(True).is_.false
with pytest.raises(Assertion):
assert that(True).is_.NOT.true
def test_int():
assert that(2).is_.an.instance_of(int)
assert that(2).is_.NOT.an.instance_of(bool)
assert that(2).is_.NOT.an.instance_of(str)
assert that(2).is_.more_than(1)
assert that(2).is_.NOT.less_than(1)
assert that(2).is_.less_than(3)
assert that(2).is_.NOT.more_than(3)
assert that(2).is_.at_least(1)
assert that(2).is_.NOT.at_most(1)
assert that(2).is_.at_most(3)
assert that(2).is_.NOT.at_least(3)
assert that(2).is_.at_most(2)
assert that(2).is_.at_least(2)
# Equality
assert that(2).is_(2)
assert that(2).was(2)
assert that(2).is_.exactly(2)
assert that(2).is_.equal.to(2)
assert that(2).equals(2)
assert that(2).is_.NOT(3)
assert that(2).does.NOT.equal(3)
def test_int_shorthands():
# Direct numbers
assert that(0).equals.zero
assert that(1).equals.one
assert that(2).equals.two
assert that(3).equals.three
assert that(4).equals.four
assert that(5).equals.five
assert that(6).equals.six
assert that(7).equals.seven
assert that(8).equals.eight
assert that(9).equals.nine
assert that(10).equals.ten
assert that(11).equals.eleven
assert that(12).equals.twelve
assert that(13).equals.thirteen
assert that(14).equals.fourteen
assert that(15).equals.fifteen
assert that(16).equals.sixteen
assert that(17).equals.seventeen
assert that(18).equals.eighteen
assert that(19).equals.nineteen
assert that(20).equals.twenty
assert that(30).equals.thirty
assert that(40).equals.forty
assert that(50).equals.fifty
assert that(60).equals.sixty
assert that(70).equals.seventy
assert that(80).equals.eighty
assert that(90).equals.ninety
assert that(100).equals.a.hundred
assert that(1000).equals.a.thousand
assert that(1_000_000).equals.a.million
def test_add_natural_complex():
# Test composed
assert that(34).equals.thirty.four
assert that(-34).equals.minus.thirty.four
assert that(100_033_207).equals.one.hundred.million.AND.thirty.three.thousand.AND.two.hundred.AND.seven
assert that(-1_200_033_207).equals.minus.one.billion.AND.two.hundred.million.AND.thirty.three.thousand.AND.two.hundred.AND.seven
assert that(7890).equals.seven.thousand.eight.hundred.and_.ninety
assert that(7890).equals.seventy.eight.hundred.and_.ninety
assert that(7890).equals(78)(100)(90)
with pytest.raises(RuntimeError):
assert that(1_000_000).equals.a.thousand.thousand
with pytest.raises(RuntimeError):
assert that(600).equals.one.twenty.thirty
with pytest.raises(RuntimeError):
assert that(2).equals
with pytest.raises(RuntimeError):
assert that(2).equals.one.minus.two
def test_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
assert spy(30, arg="string") == 10
assert that(spy).was.called
assert that(spy).was.called.once
assert that(spy).was.called.one.time
assert that(spy).was.NOT.called.more_than.once
assert that(spy).was.called.with_arguments(30)
assert that(spy).was.called.with_arguments_matching(lambda args, kwargs: len(args) == 1 and len(kwargs) == 1)
assert that(spy).was.NOT.called.with_arguments(50)
assert that(spy).was.NOT.called.with_exact_arguments(30)
assert that(spy).was.NOT.called.with_no_argument()
assert that(spy).was.called.with_exact_arguments(30, arg="string")
with pytest.raises(Assertion):
assert that(spy).was.called.with_arguments(50)
with pytest.raises(Assertion):
assert that(spy).was.called.with_exact_arguments(30)
with pytest.raises(Assertion):
assert that(spy).was.called.with_no_argument()
def test_spy_seral_calls():
spy = Spy()
obj = object()
spy()
spy(30, arg="string")
spy(obj, 30, example=obj, none=None)
assert that(spy).was.called
assert that(spy).was.called.more_than.once
assert that(spy).was.called.more_than.twice
assert that(spy).was.NOT.called.more_than.thrice
assert that(spy).was.called.at_most.thrice
assert that(spy).was.called.at_least.thrice
assert that(spy).was.called.three.times
assert that(spy).was.called.less_than(4).times
# Check arguments
assert that(spy).was.called.once.with_no_argument()
assert that(spy).was.called.at_most.once.with_no_argument()
assert that(spy).was.called.twice.with_arguments(30)
assert that(spy).was.NOT.called.less_than.twice.with_arguments(30)
assert that(spy).was.called.once.with_arguments(obj)
assert that(spy).was.called.once.with_arguments(arg="string")
assert that(spy).was.called.once.with_arguments(30, obj)
assert that(spy).was.called.once.with_arguments(none=None)
assert that(spy).was.NOT.called.with_arguments(None)
assert that(spy).was.NOT.called.with_arguments(obj, 30, arg="string")
with pytest.raises(Assertion):
assert that(spy).was.called.with_arguments(obj, 30, arg="string")
# Checking with exact arguments
assert that(spy).was.called.once.with_exact_arguments(30, arg="string")
assert that(spy).was.called.once.with_exact_arguments(obj, 30, example=obj, none=None)
assert that(spy).was.NOT.called.with_exact_arguments(obj, 30, arg="string")
with pytest.raises(Assertion):
assert that(spy).was.called.with_exact_arguments(obj, 30, arg="string")
# Check arguments matching
assert that(spy).has.NOT.been.called.with_arguments_matching(lambda a, kw: len(a) + len(kw) == 3)
assert that(spy).was.called.once.with_arguments_matching(lambda a, kw: len(a) + len(kw) == 2)
assert that(spy).was.called.once.with_arguments_matching(lambda a, kw: len(a) + len(kw) == 4)
with pytest.raises(Assertion):
assert that(spy).was.called.with_arguments_matching(lambda a, kw: len(a) + len(kw) == 3)
def test_wrongful_expressions():
spy = Spy()
with pytest.raises(RuntimeError):
assert that(3).is_.less_than("str")
with pytest.raises(RuntimeError):
assert that(3).does.NOT.NOT.equal(3)
with pytest.raises(RuntimeError):
assert that(3).is_.an.instance_of("non type")
with pytest.raises(RuntimeError):
assert that(spy).was.called.more_than.at_least.once
with pytest.raises(RuntimeError):
assert that(spy).was.called.more_than.at_most.once
with pytest.raises(RuntimeError):
assert that(spy).was.called.more_than.less_than.once
with pytest.raises(RuntimeError):
assert that(spy).was.called.more_than.more_than.once

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

@ -22,12 +22,13 @@ from os.path import exists, join
from PySide6.QtGui import QIcon from PySide6.QtGui import QIcon
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from .globals import app
from LogarithmPlotter.logarithmplotter import get_linux_theme, LINUX_THEMES, get_platform_qt_style, \ from LogarithmPlotter.logarithmplotter import get_linux_theme, LINUX_THEMES, get_platform_qt_style, \
register_icon_directories, install_translation, create_engine register_icon_directories, install_translation, create_engine
from LogarithmPlotter.util import config from LogarithmPlotter.util import config
from LogarithmPlotter.util.helper import Helper from LogarithmPlotter.util.helper import Helper
from LogarithmPlotter.util.latex import Latex from LogarithmPlotter.util.latex import Latex
from globals import app
THEMES = [ THEMES = [
"Basic", "Basic",

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,177 @@
"""
* 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.QtQml import QJSValue
from .plugins.natural import that, 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_invalid_function(self):
with pytest.raises(ValueError):
promise = PyPromise("not a function")
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):
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):
fulfilled = Spy()
rejected = Spy()
promise = PyPromise(create_async_func(3))
then_res = promise.then(fulfilled, rejected)
# Check if the return value is the same promise (so we can chain then)
assert that(then_res).does.equal(promise)
# Check on our spy.
with qtbot.waitSignal(promise.fulfilled, timeout=10000):
pass
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):
fulfilled = Spy()
rejected = Spy()
promise = PyPromise(async_throw)
then_res = promise.then(fulfilled, rejected)
# Check if the return value is the same promise (so we can chain then)
assert that(then_res).does.equal(promise)
# Check on our spies.
with qtbot.waitSignal(promise.rejected, timeout=10000):
pass
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')")

View file

@ -21,7 +21,6 @@ from re import Pattern
from PySide6.QtQml import QJSEngine, QJSValue from PySide6.QtQml import QJSEngine, QJSValue
from LogarithmPlotter.util.js import PyJSValue, InvalidAttributeValueException, NotAPrimitiveException from LogarithmPlotter.util.js import PyJSValue, InvalidAttributeValueException, NotAPrimitiveException
from globals import app
@pytest.fixture() @pytest.fixture()
def data(): def data():

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

@ -1,8 +1,20 @@
#!/bin/bash #!/bin/bash
cd "$(dirname "$(readlink -f "$0" || realpath "$0")")/.." || exit 1 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 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 while [ $# -gt 0 ]; do
case "$1" in case "$1" in
--no-rebuild) --no-rebuild)
@ -23,13 +35,16 @@ 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
box "Testing runtime-pyside6..."
PYTHONPATH="$PYTHONPATH:." pytest --cov=LogarithmPlotter --cov-report term-missing . PYTHONPATH="$PYTHONPATH:." pytest --cov=LogarithmPlotter --cov-report term-missing .
cd ../../ cd ../../
# Run js tests # Run js tests
cd common || exit 1 cd common || exit 1
box "Testing common..."
npm test npm test