From 02b6c442fa6d617a59a1106db1c003cfe8af30b1 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Mon, 30 Mar 2026 15:06:39 -0600 Subject: [PATCH 01/41] initital setup (Copied over files from FD) --- TestBridge.ts | 153 +++++++++++++++++++++ package.json | 20 +++ utils.py | 371 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 544 insertions(+) create mode 100644 TestBridge.ts create mode 100644 package.json create mode 100644 utils.py diff --git a/TestBridge.ts b/TestBridge.ts new file mode 100644 index 0000000..e30d0c8 --- /dev/null +++ b/TestBridge.ts @@ -0,0 +1,153 @@ +import {gson} from "gson"; + +interface TestRequest { + method: string; + args: any[]; +} + +interface TestResponse { + success: boolean; + result?: any; + error?: string; +} + +function processRequest(request: TestRequest): TestResponse { + try { + switch (request.method) { + case 'createFlexibleDate': + const [dateString] = request.args; + const fd = new FlexibleDate(dateString); + return { + success: true, + result: serializeFlexibleDate(fd) + }; + + case 'createFlexibleDateFromFormalDate': + const [formalDateString] = request.args; + const fdFromFormal = new FlexibleDate(null, null, null); + const result = fdFromFormal.createFlexibleDateFromFormalDate(formalDateString); + return { + success: true, + result: serializeFlexibleDate(result) + }; + + case 'compareDates': + const [date1Data, date2Data] = request.args; + const fd1 = deserializeFlexibleDate(date1Data); + const fd2 = deserializeFlexibleDate(date2Data); + const score = fd1.compareDates(fd2); + return { + success: true, + result: score + }; + + case 'combineFlexibleDates': + const [datesData] = request.args; + const dates = datesData.map((d: any) => deserializeFlexibleDate(d)); + const fd_temp = new FlexibleDate(null, null, null); + const combined = fd_temp.combineFlexibleDates(dates); + return { + success: true, + result: serializeFlexibleDate(combined) + }; + + case 'toString': + const [fdData] = request.args; + const fdForString = deserializeFlexibleDate(fdData); + return { + success: true, + result: fdForString.toString() + }; + + case 'valueOf': + const [fdDataValue] = request.args; + const fdForValue = deserializeFlexibleDate(fdDataValue); + return { + success: true, + result: fdForValue.valueOf() + }; + + case 'testBool': + const [fdDataBool] = request.args; + const fdForBool = deserializeFlexibleDate(fdDataBool); + return { + success: true, + result: fdForBool.valueOf() + }; + + case 'testStr': + const [fdDataStr] = request.args; + const fdForStr = deserializeFlexibleDate(fdDataStr); + return { + success: true, + result: fdForStr.toString() + }; + + case 'testRepr': + const [fdDataRepr] = request.args; + const fdForRepr = deserializeFlexibleDate(fdDataRepr); + return { + success: true, + result: fdForRepr.inspect() + }; + + case 'test_equals': + const [fdDataEquals1, fdDataEquals2] = request.args; + const fdForEquals1 = deserializeFlexibleDate(fdDataEquals1); + const fdForEquals2 = deserializeFlexibleDate(fdDataEquals2); + return { + success: true, + result: fdForEquals1.equals(fdForEquals2) + }; + + case 'testValidator': + try { + const [fdDataValidator] = request.args; + const fdForValidator = deserializeFlexibleDate(fdDataValidator); + return { + success: true, + result: serializeFlexibleDate(fdForValidator) + }; + } catch (error) { + return { + success: true, + result: "ValueError" + }; + } + + default: + return { + success: false, + error: `Unknown method: ${request.method}` + }; + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error) + }; + } +} + +// Main execution +if (require.main === module) { + const args = process.argv.slice(2); + if (args.length === 0) { + console.error('Usage: node test_bridge.js '); + process.exit(1); + } + + try { + const request: TestRequest = JSON.parse(args[0]); + const response = processRequest(request); + console.log(JSON.stringify(response)); + } catch (error) { + const errorResponse: TestResponse = { + success: false, + error: error instanceof Error ? error.message : String(error) + }; + console.log(JSON.stringify(errorResponse)); + } +} + +export { processRequest, TestRequest, TestResponse }; diff --git a/package.json b/package.json new file mode 100644 index 0000000..aae0a78 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "pyscripttestutils", + "version": "1.0.0", + "description": "A package that runs pytests concurrently in both python and TypeScript to enforce equal behavior between packages that support both languages", + "homepage": "https://github.com/byuawsfhtl/PyScriptTestUtils#readme", + "bugs": { + "url": "https://github.com/byuawsfhtl/PyScriptTestUtils/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/byuawsfhtl/PyScriptTestUtils.git" + }, + "license": "ISC", + "author": "", + "type": "commonjs", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..79a3960 --- /dev/null +++ b/utils.py @@ -0,0 +1,371 @@ +import json +import subprocess +import threading +from pathlib import Path +from typing import Any, Dict +from unittest.mock import patch +import threading + + +class PyScriptTestRunner: + """Self-contained test runner for dual-language FlexibleDate testing.""" + + _environment_initialized = False + _setup_lock = threading.Lock() + + def __init__(self): + """Initialize the test runner with automatic environment setup.""" + self.root_dir = Path(__file__).parent.parent + self.ts_bridge_path = self.root_dir / "FlexibleDateTS" / "dist" / "test_bridge.js" + + with PyScriptTestRunner._setup_lock: + if not PyScriptTestRunner._environment_initialized: + self._setup_environment() + PyScriptTestRunner._environment_initialized = True + + def _setup_environment(self): + """One-time setup of Node.js environment and TypeScript compilation.""" + print("Setting up dual-language testing environment...") + + # Check if TypeScript bridge exists + if not self.ts_bridge_path.exists(): + # Only check Node.js if we need to compile + if not self._check_nodejs(): + raise EnvironmentError( + "Node.js not found and TypeScript bridge not compiled. Install Node.js to run dual-language tests.\n" + "Download from: https://nodejs.org/" + ) + self._compile_typescript() + else: + print("TypeScript bridge found, skipping compilation.") + + print("Environment setup complete.") + + def _check_nodejs(self) -> bool: + """Check if Node.js is available.""" + try: + result = subprocess.run(['node', '--version'], + capture_output=True, text=True) + return result.returncode == 0 + except FileNotFoundError: + return False + + def _check_typescript_compiled(self) -> bool: + """Check if TypeScript bridge is compiled and up-to-date.""" + ts_source = self.root_dir / "FlexibleDateTS" / "test_bridge.ts" + js_output = self.ts_bridge_path + + if not js_output.exists(): + return False + + # Check if source is newer than compiled output + if ts_source.exists() and ts_source.stat().st_mtime > js_output.stat().st_mtime: + return False + + return True + + def _compile_typescript(self): + """Compile TypeScript code.""" + ts_dir = self.root_dir / "FlexibleDateTS" + + try: + print("Installing TypeScript dependencies...") + result = subprocess.run(['npm', 'ci'], + cwd=str(ts_dir), + capture_output=True, text=True) + if result.returncode != 0: + raise RuntimeError(f"Failed to install npm dependencies: {result.stderr}") + + print("Compiling TypeScript...") + result = subprocess.run(['npm', 'run', 'build'], + cwd=str(ts_dir), + capture_output=True, text=True) + if result.returncode != 0: + raise RuntimeError(f"Failed to compile TypeScript: {result.stderr}") + + except FileNotFoundError: + raise EnvironmentError( + "npm command not found. Please ensure Node.js and npm are installed and in your PATH.\n" + "Download from: https://nodejs.org/\n" + "After installation, restart your terminal/IDE and try again." + ) + + def run_dual_test(self, python_function: str, ts_function: str, test_data: Dict[str, Any]) -> tuple[Any, Any]: + """ + Run a test against both Python and TypeScript implementations. + + Args: + python_function: Name of the Python function to test + ts_function: Name of the TypeScript function to test + test_data: Dictionary containing 'input', 'expected', and optional 'mocks' and 'expected_error' + + Returns: + Tuple of (python_result, typescript_result) + """ + input_data = test_data["input"] + mocks = test_data.get("mocks", {}) + expected_error = test_data.get("expected_error", False) + + py_result_holder = {} + ts_result_holder = {} + + def run_python(): + py_result_holder["result"] = self._call_python_function_with_mocks( + python_function, input_data, mocks.get("python", {}), expected_error + ) + + def run_typescript(): + ts_result_holder["result"] = self._call_typescript_function_with_mocks( + ts_function, input_data, mocks.get("typescript", {}), expected_error + ) + + t_py = threading.Thread(target=run_python) + t_ts = threading.Thread(target=run_typescript) + t_py.start() + t_ts.start() + t_py.join() + t_ts.join() + + py_result = py_result_holder["result"] if "result" in py_result_holder else None + ts_result = ts_result_holder["result"] if "result" in ts_result_holder else None + + return py_result, ts_result + + def _call_python_function_with_mocks(self, function_name: str, input_data: Any, mocks: Dict[str, Any], expected_error: bool = False) -> Any: + """Call a Python function with optional mocking.""" + try: + # Apply mocks if provided + mock_contexts = [] + for mock_target, mock_value in mocks.items(): + mock_contexts.append(patch(mock_target, return_value=mock_value)) + + # Enter all mock contexts + for mock_context in mock_contexts: + mock_context.__enter__() + + try: + # Call the appropriate function + if function_name == "create_flexible_date": + result = create_flexible_date(input_data) + elif function_name == "create_flexible_date_from_formal_date": + result = create_flexible_date_from_formal_date(input_data) + elif function_name == "compare_two_dates": + fd1 = self._deserialize_flexible_date(input_data[0]) + fd2 = self._deserialize_flexible_date(input_data[1]) + result = compare_two_dates(fd1, fd2) + elif function_name == "combine_flexible_dates": + dates = [self._deserialize_flexible_date(d) for d in input_data] + result = combine_flexible_dates(dates) + elif function_name == "test_bool": + fd = self._deserialize_flexible_date(input_data) + result = bool(fd) + elif function_name == "test_str": + fd = self._deserialize_flexible_date(input_data) + result = str(fd) + elif function_name == "test_repr": + fd = self._deserialize_flexible_date(input_data) + result = repr(fd) + elif function_name == "test_equals": + fd1 = self._deserialize_flexible_date(input_data[0]) + fd2 = self._deserialize_flexible_date(input_data[1]) + result = fd1 == fd2 + elif function_name == "test_validator": + try: + fd = self._deserialize_flexible_date(input_data) + result = self._serialize_flexible_date(fd) + except (ValueError, PydanticValidationError) as e: + result = "ValueError" + else: + raise ValueError(f"Unknown Python function: {function_name}") + + # Serialize the result for comparison + if isinstance(result, FlexibleDate): + return self._serialize_flexible_date(result) + else: + return result + + finally: + # Exit all mock contexts + for mock_context in reversed(mock_contexts): + mock_context.__exit__(None, None, None) + + except Exception as e: + if expected_error: + # Return a standardized error representation + return {"error": True, "error_type": type(e).__name__, "error_message": str(e)} + raise RuntimeError(f"Python function {function_name} failed: {str(e)}") + + def _call_typescript_function_with_mocks(self, function_name: str, input_data: Any, mocks: Dict[str, Any], expected_error: bool = False) -> Any: + """Call a TypeScript function via subprocess with optional mocking.""" + try: + # For combineFlexibleDates, input_data is a list of dates that should be passed as a single argument + if function_name == "combineFlexibleDates": + args = [input_data] + else: + args = [input_data] if not isinstance(input_data, list) else input_data + + request = { + "method": function_name, + "args": args, + "mocks": mocks + } + + # Call the TypeScript bridge + result = subprocess.run( + ["node", str(self.ts_bridge_path), json.dumps(request)], + capture_output=True, + text=True, + cwd=str(self.root_dir) + ) + + if result.stderr: + print("=== TypeScript Debug Output ===") + print(result.stderr) + print("================================") + + + if result.returncode != 0: + raise RuntimeError(f"TypeScript bridge failed: {result.stderr}") + + response = json.loads(result.stdout) + + if not response.get("success", False): + if expected_error: + # Return a standardized error representation + return {"error": True, "error_type": "Error", "error_message": response.get('error', 'Unknown error')} + raise RuntimeError(f"TypeScript function failed: {response.get('error', 'Unknown error')}") + + return response["result"] + + except json.JSONDecodeError as e: + if expected_error: + return {"error": True, "error_type": "JSONDecodeError", "error_message": str(e)} + raise RuntimeError(f"Failed to parse TypeScript response: {str(e)}") + except Exception as e: + if expected_error: + return {"error": True, "error_type": type(e).__name__, "error_message": str(e)} + raise RuntimeError(f"TypeScript function {function_name} failed: {str(e)}") + + def _serialize_flexible_date(self, fd: FlexibleDate) -> Dict[str, Any]: + """Convert a Python FlexibleDate to a serializable dictionary.""" + return { + "likelyYear": fd.likely_year, + "likelyMonth": fd.likely_month, + "likelyDay": fd.likely_day + } + + def _deserialize_flexible_date(self, data: Dict[str, Any]) -> FlexibleDate: + """Convert a dictionary back to a Python FlexibleDate.""" + return FlexibleDate( + likely_year=data.get("likelyYear"), + likely_month=data.get("likelyMonth"), + likely_day=data.get("likelyDay") + ) + + def compare_results(self, py_result: Any, ts_result: Any) -> bool: + """ + Perform strict comparison between Python and TypeScript results. + + This method checks not only value equality but also: + - Type consistency + - Field presence and ordering (for dictionaries) + - Null/None representation consistency + - No extra metadata fields + - Error state consistency (both errored or both succeeded) + + Args: + py_result: Result from Python implementation + ts_result: Result from TypeScript implementation + + Returns: + bool: True if results are strictly identical, False otherwise + """ + # Special handling for error results + if isinstance(py_result, dict) and isinstance(ts_result, dict): + # If both are error results, they match if both have error=True + if py_result.get("error") is True and ts_result.get("error") is True: + return True + + # Basic equality check + if py_result != ts_result: + return False + + # Type checking - must be exactly the same type + if type(py_result) != type(ts_result): + return False + + # For dictionaries, perform deep field-by-field comparison + if isinstance(py_result, dict) and isinstance(ts_result, dict): + # Check that both have exactly the same keys + if set(py_result.keys()) != set(ts_result.keys()): + return False + + # Check that each field has the same type + for key in py_result.keys(): + py_value = py_result[key] + ts_value = ts_result[key] + + # Recursive type checking for nested structures + if type(py_value) != type(ts_value): + return False + + # For nested dictionaries, recurse + if isinstance(py_value, dict) and isinstance(ts_value, dict): + if not self.compare_results(py_value, ts_value): + return False + + # For lists, check element types + elif isinstance(py_result, list) and isinstance(ts_result, list): + if len(py_result) != len(ts_result): + return False + + for py_item, ts_item in zip(py_result, ts_result): + if not self.compare_results(py_item, ts_item): + return False + + return True + + def assert_strict_parity(self, py_result: Any, ts_result: Any, context: str = ""): + """ + Assert strict parity between Python and TypeScript results with detailed error reporting. + + Args: + py_result: Result from Python implementation + ts_result: Result from TypeScript implementation + context: Additional context for error messages + + Raises: + AssertionError: If results are not strictly identical, with detailed explanation + """ + if not self.compare_results(py_result, ts_result): + error_details = [] + + # Basic equality + if py_result != ts_result: + error_details.append(f"Value mismatch: Python={py_result}, TypeScript={ts_result}") + + # Type checking + if type(py_result) != type(ts_result): + error_details.append(f"Type mismatch: Python={type(py_result).__name__}, TypeScript={type(ts_result).__name__}") + error_details.append(f"Python value: {py_result}, TypeScript value: {ts_result}") + + # Dictionary field analysis + if isinstance(py_result, dict) and isinstance(ts_result, dict): + py_keys = set(py_result.keys()) + ts_keys = set(ts_result.keys()) + + if py_keys != ts_keys: + missing_in_ts = py_keys - ts_keys + missing_in_py = ts_keys - py_keys + if missing_in_ts: + error_details.append(f"Fields missing in TypeScript: {missing_in_ts}") + if missing_in_py: + error_details.append(f"Fields missing in Python: {missing_in_py}") + + # Field type mismatches + for key in py_keys & ts_keys: + if type(py_result[key]) != type(ts_result[key]): + error_details.append(f"Field '{key}' type mismatch: Python={type(py_result[key]).__name__}, TypeScript={type(ts_result[key]).__name__}") + + context_str = f" ({context})" if context else "" + raise AssertionError(f"Implementation parity check failed{context_str}:\n" + "\n".join(f" - {detail}" for detail in error_details)) \ No newline at end of file From ae0e63ad2a53df17cf12f1512d821865edbc4bb9 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Mon, 30 Mar 2026 15:40:27 -0600 Subject: [PATCH 02/41] first draft --- .gitignore | 1 + PyScriptTestBridge.ts | 41 ++ PyScriptTestRunner.py | 332 ++++++++++++++++ README.md | 64 ++- TestBridge.ts | 153 -------- .../PyScriptTestRunner.cpython-312.pyc | Bin 0 -> 15902 bytes dist/PyScriptTestBridge.d.ts | 15 + dist/PyScriptTestBridge.js | 33 ++ dist/test_bridge_entry.d.ts | 5 + dist/test_bridge_entry.js | 115 ++++++ package-lock.json | 48 +++ package.json | 7 +- test_bridge_entry.ts | 126 ++++++ tsconfig.json | 16 + utils.py | 371 ------------------ 15 files changed, 801 insertions(+), 526 deletions(-) create mode 100644 .gitignore create mode 100644 PyScriptTestBridge.ts create mode 100644 PyScriptTestRunner.py delete mode 100644 TestBridge.ts create mode 100644 __pycache__/PyScriptTestRunner.cpython-312.pyc create mode 100644 dist/PyScriptTestBridge.d.ts create mode 100644 dist/PyScriptTestBridge.js create mode 100644 dist/test_bridge_entry.d.ts create mode 100644 dist/test_bridge_entry.js create mode 100644 package-lock.json create mode 100644 test_bridge_entry.ts create mode 100644 tsconfig.json delete mode 100644 utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40b878d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/PyScriptTestBridge.ts b/PyScriptTestBridge.ts new file mode 100644 index 0000000..60e49a5 --- /dev/null +++ b/PyScriptTestBridge.ts @@ -0,0 +1,41 @@ +export interface TestRequest { + method: string; + args: any[]; +} + +export interface TestResponse { + success: boolean; + result?: any; + error?: string; +} + +export type TestMethodHandler = (args: any[]) => TestResponse; + +export class PyScriptTestBridge { + private readonly handlers = new Map(); + + addMethod(tsMethodName: string, handler: TestMethodHandler): void { + if (!tsMethodName || !String(tsMethodName).trim()) { + throw new Error("tsMethodName must be non-empty"); + } + if (this.handlers.has(tsMethodName)) { + throw new Error(`Duplicate TypeScript method: ${tsMethodName}`); + } + this.handlers.set(tsMethodName, handler); + } + + processRequest(request: TestRequest): TestResponse { + const handler = this.handlers.get(request.method); + if (!handler) { + return { success: false, error: `Unknown method: ${request.method}` }; + } + try { + return handler(request.args); + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } +} diff --git a/PyScriptTestRunner.py b/PyScriptTestRunner.py new file mode 100644 index 0000000..8f90be3 --- /dev/null +++ b/PyScriptTestRunner.py @@ -0,0 +1,332 @@ +import json +import subprocess +import threading +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Callable, Dict, Optional +from unittest.mock import patch + + +@dataclass(frozen=True) +class RegisteredMethod: + py_fn: Callable[[Any], Any] + ts_name: str + ts_pack_input: bool = False + + +class PyScriptTestRunner: + """Dual-language test runner: register Python callables and matching TS RPC names.""" + + _environment_initialized = False + _setup_lock = threading.Lock() + + def __init__( + self, + ts_bridge_path: Optional[Path] = None, + package_root: Optional[Path] = None, + ) -> None: + assert ts_bridge_path is None or isinstance(ts_bridge_path, Path) + assert package_root is None or isinstance(package_root, Path) + + self.package_root = ( + package_root if package_root is not None else Path(__file__).resolve().parent + ) + self.ts_bridge_path = ( + ts_bridge_path + if ts_bridge_path is not None + else self.package_root / "dist" / "test_bridge_entry.js" + ) + self._by_py: Dict[str, RegisteredMethod] = {} + self._by_ts: Dict[str, RegisteredMethod] = {} + + with PyScriptTestRunner._setup_lock: + if not PyScriptTestRunner._environment_initialized: + self._setup_environment() + PyScriptTestRunner._environment_initialized = True + + def add_method( + self, + py_callable: Callable[[Any], Any], + ts_method_name: str, + *, + ts_pack_input: bool = False, + ) -> None: + assert callable(py_callable) + if not ts_method_name or not str(ts_method_name).strip(): + raise ValueError("ts_method_name must be non-empty") + py_key = getattr(py_callable, "__name__", None) + if not py_key or py_key == "": + raise ValueError("py_callable must be a named function (not lambda or )") + + if py_key in self._by_py: + raise ValueError(f"Duplicate Python registration: {py_key!r}") + if ts_method_name in self._by_ts: + raise ValueError(f"Duplicate TypeScript registration: {ts_method_name!r}") + + rec = RegisteredMethod( + py_fn=py_callable, ts_name=ts_method_name, ts_pack_input=ts_pack_input + ) + self._by_py[py_key] = rec + self._by_ts[ts_method_name] = rec + + def run( + self, + python_function_name: str, + ts_function_name: str, + test_data: Dict[str, Any], + ) -> tuple[Any, Any]: + return self.run_dual_test(python_function_name, ts_function_name, test_data) + + def run_dual_test( + self, python_function: str, ts_function: str, test_data: Dict[str, Any] + ) -> tuple[Any, Any]: + rec = self._by_py.get(python_function) + if rec is None: + raise ValueError(f"Unknown Python function: {python_function!r}") + if rec.ts_name != ts_function: + raise ValueError( + f"TypeScript name mismatch for {python_function!r}: " + f"expected {rec.ts_name!r}, got {ts_function!r}" + ) + + input_data = test_data["input"] + mocks = test_data.get("mocks", {}) + expected_error = test_data.get("expected_error", False) + + py_result_holder: Dict[str, Any] = {} + ts_result_holder: Dict[str, Any] = {} + + def run_python() -> None: + py_result_holder["result"] = self._call_python_function_with_mocks( + python_function, input_data, mocks.get("python", {}), expected_error + ) + + def run_typescript() -> None: + ts_result_holder["result"] = self._call_typescript_function_with_mocks( + ts_function, input_data, mocks.get("typescript", {}), expected_error + ) + + t_py = threading.Thread(target=run_python) + t_ts = threading.Thread(target=run_typescript) + t_py.start() + t_ts.start() + t_py.join() + t_ts.join() + + py_result = py_result_holder.get("result") + ts_result = ts_result_holder.get("result") + + return py_result, ts_result + + def _setup_environment(self) -> None: + print("Setting up dual-language testing environment...") + + if not self.ts_bridge_path.exists(): + if not self._check_nodejs(): + raise EnvironmentError( + "Node.js not found and TypeScript bridge not compiled. " + "Install Node.js to run dual-language tests.\n" + "Download from: https://nodejs.org/" + ) + self._compile_typescript() + else: + print("TypeScript bridge found, skipping compilation.") + + print("Environment setup complete.") + + def _check_nodejs(self) -> bool: + try: + result = subprocess.run( + ["node", "--version"], capture_output=True, text=True + ) + return result.returncode == 0 + except FileNotFoundError: + return False + + def _compile_typescript(self) -> None: + ts_dir = self.package_root + try: + print("Installing TypeScript dependencies...") + result = subprocess.run( + ["npm", "ci"], cwd=str(ts_dir), capture_output=True, text=True + ) + if result.returncode != 0: + raise RuntimeError(f"Failed to install npm dependencies: {result.stderr}") + + print("Compiling TypeScript...") + result = subprocess.run( + ["npm", "run", "build"], cwd=str(ts_dir), capture_output=True, text=True + ) + if result.returncode != 0: + raise RuntimeError(f"Failed to compile TypeScript: {result.stderr}") + + except FileNotFoundError as exc: + raise EnvironmentError( + "npm command not found. Ensure Node.js and npm are installed and on PATH.\n" + "Download from: https://nodejs.org/\n" + "After installation, restart your terminal/IDE and try again." + ) from exc + + def _call_python_function_with_mocks( + self, + function_name: str, + input_data: Any, + mocks: Dict[str, Any], + expected_error: bool = False, + ) -> Any: + rec = self._by_py.get(function_name) + if rec is None: + raise ValueError(f"Unknown Python function: {function_name!r}") + + try: + mock_contexts = [] + for mock_target, mock_value in mocks.items(): + mock_contexts.append(patch(mock_target, return_value=mock_value)) + + for mock_context in mock_contexts: + mock_context.__enter__() + + try: + return rec.py_fn(input_data) + finally: + for mock_context in reversed(mock_contexts): + mock_context.__exit__(None, None, None) + + except Exception as e: + if expected_error: + return {"error": True, "error_type": type(e).__name__, "error_message": str(e)} + raise RuntimeError(f"Python function {function_name} failed: {str(e)}") from e + + def _call_typescript_function_with_mocks( + self, + function_name: str, + input_data: Any, + mocks: Dict[str, Any], + expected_error: bool = False, + ) -> Any: + rec = self._by_ts.get(function_name) + if rec is None: + raise ValueError(f"Unknown TypeScript function: {function_name!r}") + + try: + if rec.ts_pack_input: + args = [input_data] + else: + args = [input_data] if not isinstance(input_data, list) else input_data + + request = {"method": function_name, "args": args, "mocks": mocks} + + result = subprocess.run( + ["node", str(self.ts_bridge_path), json.dumps(request)], + capture_output=True, + text=True, + cwd=str(self.package_root), + ) + + if result.stderr: + print("=== TypeScript Debug Output ===") + print(result.stderr) + print("================================") + + if result.returncode != 0: + raise RuntimeError(f"TypeScript bridge failed: {result.stderr}") + + response = json.loads(result.stdout) + + if not response.get("success", False): + if expected_error: + return { + "error": True, + "error_type": "Error", + "error_message": response.get("error", "Unknown error"), + } + raise RuntimeError( + f"TypeScript function failed: {response.get('error', 'Unknown error')}" + ) + + return response["result"] + + except json.JSONDecodeError as e: + if expected_error: + return {"error": True, "error_type": "JSONDecodeError", "error_message": str(e)} + raise RuntimeError(f"Failed to parse TypeScript response: {str(e)}") from e + except Exception as e: + if expected_error: + return {"error": True, "error_type": type(e).__name__, "error_message": str(e)} + raise RuntimeError(f"TypeScript function {function_name} failed: {str(e)}") from e + + def compare_results(self, py_result: Any, ts_result: Any) -> bool: + if isinstance(py_result, dict) and isinstance(ts_result, dict): + if py_result.get("error") is True and ts_result.get("error") is True: + return True + + if py_result != ts_result: + return False + + if type(py_result) != type(ts_result): + return False + + if isinstance(py_result, dict) and isinstance(ts_result, dict): + if set(py_result.keys()) != set(ts_result.keys()): + return False + + for key in py_result.keys(): + py_value = py_result[key] + ts_value = ts_result[key] + + if type(py_value) != type(ts_value): + return False + + if isinstance(py_value, dict) and isinstance(ts_value, dict): + if not self.compare_results(py_value, ts_value): + return False + + elif isinstance(py_result, list) and isinstance(ts_result, list): + if len(py_result) != len(ts_result): + return False + + for py_item, ts_item in zip(py_result, ts_result): + if not self.compare_results(py_item, ts_item): + return False + + return True + + def assert_strict_parity(self, py_result: Any, ts_result: Any, context: str = "") -> None: + if not self.compare_results(py_result, ts_result): + error_details = [] + + if py_result != ts_result: + error_details.append(f"Value mismatch: Python={py_result}, TypeScript={ts_result}") + + if type(py_result) != type(ts_result): + error_details.append( + f"Type mismatch: Python={type(py_result).__name__}, " + f"TypeScript={type(ts_result).__name__}" + ) + error_details.append(f"Python value: {py_result}, TypeScript value: {ts_result}") + + if isinstance(py_result, dict) and isinstance(ts_result, dict): + py_keys = set(py_result.keys()) + ts_keys = set(ts_result.keys()) + + if py_keys != ts_keys: + missing_in_ts = py_keys - ts_keys + missing_in_py = ts_keys - py_keys + if missing_in_ts: + error_details.append(f"Fields missing in TypeScript: {missing_in_ts}") + if missing_in_py: + error_details.append(f"Fields missing in Python: {missing_in_py}") + + for key in py_keys & ts_keys: + if type(py_result[key]) != type(ts_result[key]): + error_details.append( + f"Field '{key}' type mismatch: " + f"Python={type(py_result[key]).__name__}, " + f"TypeScript={type(ts_result[key]).__name__}" + ) + + context_str = f" ({context})" if context else "" + raise AssertionError( + f"Implementation parity check failed{context_str}:\n" + + "\n".join(f" - {detail}" for detail in error_details) + ) diff --git a/README.md b/README.md index 2dd5b8d..0039778 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,64 @@ # PyScriptTestUtils -A package that runs pytests concurrently in both python and TypeScript to enforce equal behavior between packages that support both languages + +A package that runs pytest-style checks concurrently in Python and TypeScript so dual-language libraries can enforce the same behavior in both implementations. + +## Architecture + +- **`PyScriptTestRunner`** (Python): register one **named** Python function per operation with the TypeScript RPC name your Node bridge expects, then call `run` / `run_dual_test` with the same `test_data` shape as before. +- **`PyScriptTestBridge`** (TypeScript): register handlers with `addMethod(tsName, (args) => response)`. The compiled **`test_bridge_entry.js`** is invoked by the runner via `node`; it parses one JSON request from argv and prints one JSON response. + +## Python: registration and `run` + +```python +runner = PyScriptTestRunner() + +def create_flexible_date(input_data): + result = my_create_flexible_date(input_data) + return serialize_if_needed(result) + +runner.add_method(create_flexible_date, "createFlexibleDate") +runner.add_method(combine_flexible_dates, "combineFlexibleDates", ts_pack_input=True) +# ... + +py_result, ts_result = runner.run( + "create_flexible_date", + "createFlexibleDate", + test_data, +) +``` + +Rules: + +- **`add_method(py_callable, ts_method_name, *, ts_pack_input=False)`** + - `py_callable` must be a **named** function (not a lambda). The registry key is `py_callable.__name__` (what you pass as the first argument to `run`). + - `ts_method_name` must match `addMethod` on the TS side and the JSON `method` field. + - **`ts_pack_input=True`**: for this operation the runner sends `args: [input_data]` to Node (single array argument). Use this for TS handlers that expect one aggregate argument (for example `combineFlexibleDates` with `const [datesData] = args`). +- **`run(python_name, ts_name, test_data)`** checks that `ts_name` matches the name registered for `python_name`. +- Registered Python callables receive a **single** argument: `test_data["input"]`. Shape the input in your tests so each callable can implement the operation (unwrap lists, deserialize dicts to domain objects, etc.). +- Return **JSON-serializable** values from Python (or the same shapes your TS bridge returns). The runner no longer auto-serializes domain types; keep that logic in your registered functions. + +## TypeScript: `test_bridge_entry.ts` + +This repo ships an example entry file that registers FlexibleDate operations and imports the sibling **`../FlexibleDate/FlexibleDateTS/dist/FlexibleDateTS`** build. For your own repo, point that import at your published or local TS package and keep the same `addMethod` names as on the Python side. + +Build output goes to **`dist/test_bridge_entry.js`**. The Python runner defaults to **`package_root / "dist" / "test_bridge_entry.js"`**. + +## RPC argument list (`args`) + +The subprocess request is `{ "method": string, "args": any[], "mocks": object }`. + +- For most operations the runner sets **`args`** from `test_data["input"]` as: + - `[input_data]` when `input_data` is not a `list`, + - or **`input_data` as-is** when it is already a `list` (so it becomes multiple `args` elements, matching the old behavior for `compareDates`, `test_equals`, etc.). +- When **`ts_pack_input=True`**, the runner always sends **`args: [input_data]`** (one element), so the TS handler uses `const [x] = args` (for example a list of serialized dates). + +Align Python `input_data` in tests with this contract so both sides see the same logical inputs. + +## Developing + +```bash +npm ci +npm run build +``` + +Requires Node.js and npm. The example bridge import expects the FlexibleDate TS package to be built (`npm run build` in that repo) when using the default relative path. diff --git a/TestBridge.ts b/TestBridge.ts deleted file mode 100644 index e30d0c8..0000000 --- a/TestBridge.ts +++ /dev/null @@ -1,153 +0,0 @@ -import {gson} from "gson"; - -interface TestRequest { - method: string; - args: any[]; -} - -interface TestResponse { - success: boolean; - result?: any; - error?: string; -} - -function processRequest(request: TestRequest): TestResponse { - try { - switch (request.method) { - case 'createFlexibleDate': - const [dateString] = request.args; - const fd = new FlexibleDate(dateString); - return { - success: true, - result: serializeFlexibleDate(fd) - }; - - case 'createFlexibleDateFromFormalDate': - const [formalDateString] = request.args; - const fdFromFormal = new FlexibleDate(null, null, null); - const result = fdFromFormal.createFlexibleDateFromFormalDate(formalDateString); - return { - success: true, - result: serializeFlexibleDate(result) - }; - - case 'compareDates': - const [date1Data, date2Data] = request.args; - const fd1 = deserializeFlexibleDate(date1Data); - const fd2 = deserializeFlexibleDate(date2Data); - const score = fd1.compareDates(fd2); - return { - success: true, - result: score - }; - - case 'combineFlexibleDates': - const [datesData] = request.args; - const dates = datesData.map((d: any) => deserializeFlexibleDate(d)); - const fd_temp = new FlexibleDate(null, null, null); - const combined = fd_temp.combineFlexibleDates(dates); - return { - success: true, - result: serializeFlexibleDate(combined) - }; - - case 'toString': - const [fdData] = request.args; - const fdForString = deserializeFlexibleDate(fdData); - return { - success: true, - result: fdForString.toString() - }; - - case 'valueOf': - const [fdDataValue] = request.args; - const fdForValue = deserializeFlexibleDate(fdDataValue); - return { - success: true, - result: fdForValue.valueOf() - }; - - case 'testBool': - const [fdDataBool] = request.args; - const fdForBool = deserializeFlexibleDate(fdDataBool); - return { - success: true, - result: fdForBool.valueOf() - }; - - case 'testStr': - const [fdDataStr] = request.args; - const fdForStr = deserializeFlexibleDate(fdDataStr); - return { - success: true, - result: fdForStr.toString() - }; - - case 'testRepr': - const [fdDataRepr] = request.args; - const fdForRepr = deserializeFlexibleDate(fdDataRepr); - return { - success: true, - result: fdForRepr.inspect() - }; - - case 'test_equals': - const [fdDataEquals1, fdDataEquals2] = request.args; - const fdForEquals1 = deserializeFlexibleDate(fdDataEquals1); - const fdForEquals2 = deserializeFlexibleDate(fdDataEquals2); - return { - success: true, - result: fdForEquals1.equals(fdForEquals2) - }; - - case 'testValidator': - try { - const [fdDataValidator] = request.args; - const fdForValidator = deserializeFlexibleDate(fdDataValidator); - return { - success: true, - result: serializeFlexibleDate(fdForValidator) - }; - } catch (error) { - return { - success: true, - result: "ValueError" - }; - } - - default: - return { - success: false, - error: `Unknown method: ${request.method}` - }; - } - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : String(error) - }; - } -} - -// Main execution -if (require.main === module) { - const args = process.argv.slice(2); - if (args.length === 0) { - console.error('Usage: node test_bridge.js '); - process.exit(1); - } - - try { - const request: TestRequest = JSON.parse(args[0]); - const response = processRequest(request); - console.log(JSON.stringify(response)); - } catch (error) { - const errorResponse: TestResponse = { - success: false, - error: error instanceof Error ? error.message : String(error) - }; - console.log(JSON.stringify(errorResponse)); - } -} - -export { processRequest, TestRequest, TestResponse }; diff --git a/__pycache__/PyScriptTestRunner.cpython-312.pyc b/__pycache__/PyScriptTestRunner.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..89dd073a646fedc6cd364a4137d0b77bd9a16a9b GIT binary patch literal 15902 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 TestResponse; +export declare class PyScriptTestBridge { + private readonly handlers; + addMethod(tsMethodName: string, handler: TestMethodHandler): void; + processRequest(request: TestRequest): TestResponse; +} diff --git a/dist/PyScriptTestBridge.js b/dist/PyScriptTestBridge.js new file mode 100644 index 0000000..d749f9b --- /dev/null +++ b/dist/PyScriptTestBridge.js @@ -0,0 +1,33 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.PyScriptTestBridge = void 0; +class PyScriptTestBridge { + constructor() { + this.handlers = new Map(); + } + addMethod(tsMethodName, handler) { + if (!tsMethodName || !String(tsMethodName).trim()) { + throw new Error("tsMethodName must be non-empty"); + } + if (this.handlers.has(tsMethodName)) { + throw new Error(`Duplicate TypeScript method: ${tsMethodName}`); + } + this.handlers.set(tsMethodName, handler); + } + processRequest(request) { + const handler = this.handlers.get(request.method); + if (!handler) { + return { success: false, error: `Unknown method: ${request.method}` }; + } + try { + return handler(request.args); + } + catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } +} +exports.PyScriptTestBridge = PyScriptTestBridge; diff --git a/dist/test_bridge_entry.d.ts b/dist/test_bridge_entry.d.ts new file mode 100644 index 0000000..2866f2c --- /dev/null +++ b/dist/test_bridge_entry.d.ts @@ -0,0 +1,5 @@ +#!/usr/bin/env node +import { PyScriptTestBridge } from "./PyScriptTestBridge"; +declare function buildBridge(): PyScriptTestBridge; +declare const bridge: PyScriptTestBridge; +export { buildBridge, bridge }; diff --git a/dist/test_bridge_entry.js b/dist/test_bridge_entry.js new file mode 100644 index 0000000..429289b --- /dev/null +++ b/dist/test_bridge_entry.js @@ -0,0 +1,115 @@ +#!/usr/bin/env node +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.bridge = void 0; +exports.buildBridge = buildBridge; +/** + * Consumer entry: register domain handlers, then serve JSON-RPC lines from argv. + * Adjust the FlexibleDate import to your package layout when publishing elsewhere. + */ +const FlexibleDateTS_1 = __importDefault(require("../FlexibleDate/FlexibleDateTS/dist/FlexibleDateTS")); +const PyScriptTestBridge_1 = require("./PyScriptTestBridge"); +function serializeFlexibleDate(fd) { + return { + likelyYear: fd.likelyYear, + likelyMonth: fd.likelyMonth, + likelyDay: fd.likelyDay, + }; +} +function deserializeFlexibleDate(data) { + return new FlexibleDateTS_1.default(data.likelyDay, data.likelyMonth, data.likelyYear); +} +function buildBridge() { + const bridge = new PyScriptTestBridge_1.PyScriptTestBridge(); + bridge.addMethod("createFlexibleDate", (args) => { + const [dateString] = args; + const fd = new FlexibleDateTS_1.default(dateString); + return { success: true, result: serializeFlexibleDate(fd) }; + }); + bridge.addMethod("createFlexibleDateFromFormalDate", (args) => { + const [formalDateString] = args; + const fdFromFormal = new FlexibleDateTS_1.default(null, null, null); + const result = fdFromFormal.createFlexibleDateFromFormalDate(formalDateString); + return { success: true, result: serializeFlexibleDate(result) }; + }); + bridge.addMethod("compareDates", (args) => { + const [date1Data, date2Data] = args; + const fd1 = deserializeFlexibleDate(date1Data); + const fd2 = deserializeFlexibleDate(date2Data); + const score = fd1.compareDates(fd2); + return { success: true, result: score }; + }); + bridge.addMethod("combineFlexibleDates", (args) => { + const [datesData] = args; + const dates = datesData.map((d) => deserializeFlexibleDate(d)); + const fdTemp = new FlexibleDateTS_1.default(null, null, null); + const combined = fdTemp.combineFlexibleDates(dates); + return { success: true, result: serializeFlexibleDate(combined) }; + }); + bridge.addMethod("toString", (args) => { + const [fdData] = args; + const fdForString = deserializeFlexibleDate(fdData); + return { success: true, result: fdForString.toString() }; + }); + bridge.addMethod("valueOf", (args) => { + const [fdDataValue] = args; + const fdForValue = deserializeFlexibleDate(fdDataValue); + return { success: true, result: fdForValue.valueOf() }; + }); + bridge.addMethod("testBool", (args) => { + const [fdDataBool] = args; + const fdForBool = deserializeFlexibleDate(fdDataBool); + return { success: true, result: fdForBool.valueOf() }; + }); + bridge.addMethod("testStr", (args) => { + const [fdDataStr] = args; + const fdForStr = deserializeFlexibleDate(fdDataStr); + return { success: true, result: fdForStr.toString() }; + }); + bridge.addMethod("testRepr", (args) => { + const [fdDataRepr] = args; + const fdForRepr = deserializeFlexibleDate(fdDataRepr); + return { success: true, result: fdForRepr.inspect() }; + }); + bridge.addMethod("test_equals", (args) => { + const [fdDataEquals1, fdDataEquals2] = args; + const fdForEquals1 = deserializeFlexibleDate(fdDataEquals1); + const fdForEquals2 = deserializeFlexibleDate(fdDataEquals2); + return { success: true, result: fdForEquals1.equals(fdForEquals2) }; + }); + bridge.addMethod("testValidator", (args) => { + try { + const [fdDataValidator] = args; + const fdForValidator = deserializeFlexibleDate(fdDataValidator); + return { success: true, result: serializeFlexibleDate(fdForValidator) }; + } + catch { + return { success: true, result: "ValueError" }; + } + }); + return bridge; +} +const bridge = buildBridge(); +exports.bridge = bridge; +if (require.main === module) { + const argv = process.argv.slice(2); + if (argv.length === 0) { + console.error("Usage: node test_bridge_entry.js "); + process.exit(1); + } + try { + const request = JSON.parse(argv[0]); + const response = bridge.processRequest(request); + console.log(JSON.stringify(response)); + } + catch (error) { + const errorResponse = { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + console.log(JSON.stringify(errorResponse)); + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6513fed --- /dev/null +++ b/package-lock.json @@ -0,0 +1,48 @@ +{ + "name": "pyscripttestutils", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pyscripttestutils", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@types/node": "^22.10.1", + "typescript": "^5.6.3" + } + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json index aae0a78..7090561 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,13 @@ "license": "ISC", "author": "", "type": "commonjs", - "main": "index.js", + "main": "dist/PyScriptTestBridge.js", "scripts": { + "build": "tsc", "test": "echo \"Error: no test specified\" && exit 1" + }, + "devDependencies": { + "@types/node": "^22.10.1", + "typescript": "^5.6.3" } } diff --git a/test_bridge_entry.ts b/test_bridge_entry.ts new file mode 100644 index 0000000..2490060 --- /dev/null +++ b/test_bridge_entry.ts @@ -0,0 +1,126 @@ +#!/usr/bin/env node + +/** + * Consumer entry: register domain handlers, then serve JSON-RPC lines from argv. + * Adjust the FlexibleDate import to your package layout when publishing elsewhere. + */ +import FlexibleDate from "../FlexibleDate/FlexibleDateTS/dist/FlexibleDateTS"; +import { PyScriptTestBridge, TestRequest, TestResponse } from "./PyScriptTestBridge"; + +function serializeFlexibleDate(fd: FlexibleDate): any { + return { + likelyYear: fd.likelyYear, + likelyMonth: fd.likelyMonth, + likelyDay: fd.likelyDay, + }; +} + +function deserializeFlexibleDate(data: any): FlexibleDate { + return new FlexibleDate(data.likelyDay, data.likelyMonth, data.likelyYear); +} + +function buildBridge(): PyScriptTestBridge { + const bridge = new PyScriptTestBridge(); + + bridge.addMethod("createFlexibleDate", (args) => { + const [dateString] = args; + const fd = new FlexibleDate(dateString); + return { success: true, result: serializeFlexibleDate(fd) }; + }); + + bridge.addMethod("createFlexibleDateFromFormalDate", (args) => { + const [formalDateString] = args; + const fdFromFormal = new FlexibleDate(null, null, null); + const result = fdFromFormal.createFlexibleDateFromFormalDate(formalDateString); + return { success: true, result: serializeFlexibleDate(result) }; + }); + + bridge.addMethod("compareDates", (args) => { + const [date1Data, date2Data] = args; + const fd1 = deserializeFlexibleDate(date1Data); + const fd2 = deserializeFlexibleDate(date2Data); + const score = fd1.compareDates(fd2); + return { success: true, result: score }; + }); + + bridge.addMethod("combineFlexibleDates", (args) => { + const [datesData] = args; + const dates = datesData.map((d: any) => deserializeFlexibleDate(d)); + const fdTemp = new FlexibleDate(null, null, null); + const combined = fdTemp.combineFlexibleDates(dates); + return { success: true, result: serializeFlexibleDate(combined) }; + }); + + bridge.addMethod("toString", (args) => { + const [fdData] = args; + const fdForString = deserializeFlexibleDate(fdData); + return { success: true, result: fdForString.toString() }; + }); + + bridge.addMethod("valueOf", (args) => { + const [fdDataValue] = args; + const fdForValue = deserializeFlexibleDate(fdDataValue); + return { success: true, result: fdForValue.valueOf() }; + }); + + bridge.addMethod("testBool", (args) => { + const [fdDataBool] = args; + const fdForBool = deserializeFlexibleDate(fdDataBool); + return { success: true, result: fdForBool.valueOf() }; + }); + + bridge.addMethod("testStr", (args) => { + const [fdDataStr] = args; + const fdForStr = deserializeFlexibleDate(fdDataStr); + return { success: true, result: fdForStr.toString() }; + }); + + bridge.addMethod("testRepr", (args) => { + const [fdDataRepr] = args; + const fdForRepr = deserializeFlexibleDate(fdDataRepr); + return { success: true, result: fdForRepr.inspect() }; + }); + + bridge.addMethod("test_equals", (args) => { + const [fdDataEquals1, fdDataEquals2] = args; + const fdForEquals1 = deserializeFlexibleDate(fdDataEquals1); + const fdForEquals2 = deserializeFlexibleDate(fdDataEquals2); + return { success: true, result: fdForEquals1.equals(fdForEquals2) }; + }); + + bridge.addMethod("testValidator", (args) => { + try { + const [fdDataValidator] = args; + const fdForValidator = deserializeFlexibleDate(fdDataValidator); + return { success: true, result: serializeFlexibleDate(fdForValidator) }; + } catch { + return { success: true, result: "ValueError" }; + } + }); + + return bridge; +} + +const bridge = buildBridge(); + +if (require.main === module) { + const argv = process.argv.slice(2); + if (argv.length === 0) { + console.error("Usage: node test_bridge_entry.js "); + process.exit(1); + } + + try { + const request = JSON.parse(argv[0]) as TestRequest; + const response: TestResponse = bridge.processRequest(request); + console.log(JSON.stringify(response)); + } catch (error) { + const errorResponse: TestResponse = { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + console.log(JSON.stringify(errorResponse)); + } +} + +export { buildBridge, bridge }; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0b8a2b4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2019", + "module": "commonjs", + "moduleResolution": "node", + "outDir": "dist", + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "declaration": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["PyScriptTestBridge.ts", "test_bridge_entry.ts"] +} diff --git a/utils.py b/utils.py deleted file mode 100644 index 79a3960..0000000 --- a/utils.py +++ /dev/null @@ -1,371 +0,0 @@ -import json -import subprocess -import threading -from pathlib import Path -from typing import Any, Dict -from unittest.mock import patch -import threading - - -class PyScriptTestRunner: - """Self-contained test runner for dual-language FlexibleDate testing.""" - - _environment_initialized = False - _setup_lock = threading.Lock() - - def __init__(self): - """Initialize the test runner with automatic environment setup.""" - self.root_dir = Path(__file__).parent.parent - self.ts_bridge_path = self.root_dir / "FlexibleDateTS" / "dist" / "test_bridge.js" - - with PyScriptTestRunner._setup_lock: - if not PyScriptTestRunner._environment_initialized: - self._setup_environment() - PyScriptTestRunner._environment_initialized = True - - def _setup_environment(self): - """One-time setup of Node.js environment and TypeScript compilation.""" - print("Setting up dual-language testing environment...") - - # Check if TypeScript bridge exists - if not self.ts_bridge_path.exists(): - # Only check Node.js if we need to compile - if not self._check_nodejs(): - raise EnvironmentError( - "Node.js not found and TypeScript bridge not compiled. Install Node.js to run dual-language tests.\n" - "Download from: https://nodejs.org/" - ) - self._compile_typescript() - else: - print("TypeScript bridge found, skipping compilation.") - - print("Environment setup complete.") - - def _check_nodejs(self) -> bool: - """Check if Node.js is available.""" - try: - result = subprocess.run(['node', '--version'], - capture_output=True, text=True) - return result.returncode == 0 - except FileNotFoundError: - return False - - def _check_typescript_compiled(self) -> bool: - """Check if TypeScript bridge is compiled and up-to-date.""" - ts_source = self.root_dir / "FlexibleDateTS" / "test_bridge.ts" - js_output = self.ts_bridge_path - - if not js_output.exists(): - return False - - # Check if source is newer than compiled output - if ts_source.exists() and ts_source.stat().st_mtime > js_output.stat().st_mtime: - return False - - return True - - def _compile_typescript(self): - """Compile TypeScript code.""" - ts_dir = self.root_dir / "FlexibleDateTS" - - try: - print("Installing TypeScript dependencies...") - result = subprocess.run(['npm', 'ci'], - cwd=str(ts_dir), - capture_output=True, text=True) - if result.returncode != 0: - raise RuntimeError(f"Failed to install npm dependencies: {result.stderr}") - - print("Compiling TypeScript...") - result = subprocess.run(['npm', 'run', 'build'], - cwd=str(ts_dir), - capture_output=True, text=True) - if result.returncode != 0: - raise RuntimeError(f"Failed to compile TypeScript: {result.stderr}") - - except FileNotFoundError: - raise EnvironmentError( - "npm command not found. Please ensure Node.js and npm are installed and in your PATH.\n" - "Download from: https://nodejs.org/\n" - "After installation, restart your terminal/IDE and try again." - ) - - def run_dual_test(self, python_function: str, ts_function: str, test_data: Dict[str, Any]) -> tuple[Any, Any]: - """ - Run a test against both Python and TypeScript implementations. - - Args: - python_function: Name of the Python function to test - ts_function: Name of the TypeScript function to test - test_data: Dictionary containing 'input', 'expected', and optional 'mocks' and 'expected_error' - - Returns: - Tuple of (python_result, typescript_result) - """ - input_data = test_data["input"] - mocks = test_data.get("mocks", {}) - expected_error = test_data.get("expected_error", False) - - py_result_holder = {} - ts_result_holder = {} - - def run_python(): - py_result_holder["result"] = self._call_python_function_with_mocks( - python_function, input_data, mocks.get("python", {}), expected_error - ) - - def run_typescript(): - ts_result_holder["result"] = self._call_typescript_function_with_mocks( - ts_function, input_data, mocks.get("typescript", {}), expected_error - ) - - t_py = threading.Thread(target=run_python) - t_ts = threading.Thread(target=run_typescript) - t_py.start() - t_ts.start() - t_py.join() - t_ts.join() - - py_result = py_result_holder["result"] if "result" in py_result_holder else None - ts_result = ts_result_holder["result"] if "result" in ts_result_holder else None - - return py_result, ts_result - - def _call_python_function_with_mocks(self, function_name: str, input_data: Any, mocks: Dict[str, Any], expected_error: bool = False) -> Any: - """Call a Python function with optional mocking.""" - try: - # Apply mocks if provided - mock_contexts = [] - for mock_target, mock_value in mocks.items(): - mock_contexts.append(patch(mock_target, return_value=mock_value)) - - # Enter all mock contexts - for mock_context in mock_contexts: - mock_context.__enter__() - - try: - # Call the appropriate function - if function_name == "create_flexible_date": - result = create_flexible_date(input_data) - elif function_name == "create_flexible_date_from_formal_date": - result = create_flexible_date_from_formal_date(input_data) - elif function_name == "compare_two_dates": - fd1 = self._deserialize_flexible_date(input_data[0]) - fd2 = self._deserialize_flexible_date(input_data[1]) - result = compare_two_dates(fd1, fd2) - elif function_name == "combine_flexible_dates": - dates = [self._deserialize_flexible_date(d) for d in input_data] - result = combine_flexible_dates(dates) - elif function_name == "test_bool": - fd = self._deserialize_flexible_date(input_data) - result = bool(fd) - elif function_name == "test_str": - fd = self._deserialize_flexible_date(input_data) - result = str(fd) - elif function_name == "test_repr": - fd = self._deserialize_flexible_date(input_data) - result = repr(fd) - elif function_name == "test_equals": - fd1 = self._deserialize_flexible_date(input_data[0]) - fd2 = self._deserialize_flexible_date(input_data[1]) - result = fd1 == fd2 - elif function_name == "test_validator": - try: - fd = self._deserialize_flexible_date(input_data) - result = self._serialize_flexible_date(fd) - except (ValueError, PydanticValidationError) as e: - result = "ValueError" - else: - raise ValueError(f"Unknown Python function: {function_name}") - - # Serialize the result for comparison - if isinstance(result, FlexibleDate): - return self._serialize_flexible_date(result) - else: - return result - - finally: - # Exit all mock contexts - for mock_context in reversed(mock_contexts): - mock_context.__exit__(None, None, None) - - except Exception as e: - if expected_error: - # Return a standardized error representation - return {"error": True, "error_type": type(e).__name__, "error_message": str(e)} - raise RuntimeError(f"Python function {function_name} failed: {str(e)}") - - def _call_typescript_function_with_mocks(self, function_name: str, input_data: Any, mocks: Dict[str, Any], expected_error: bool = False) -> Any: - """Call a TypeScript function via subprocess with optional mocking.""" - try: - # For combineFlexibleDates, input_data is a list of dates that should be passed as a single argument - if function_name == "combineFlexibleDates": - args = [input_data] - else: - args = [input_data] if not isinstance(input_data, list) else input_data - - request = { - "method": function_name, - "args": args, - "mocks": mocks - } - - # Call the TypeScript bridge - result = subprocess.run( - ["node", str(self.ts_bridge_path), json.dumps(request)], - capture_output=True, - text=True, - cwd=str(self.root_dir) - ) - - if result.stderr: - print("=== TypeScript Debug Output ===") - print(result.stderr) - print("================================") - - - if result.returncode != 0: - raise RuntimeError(f"TypeScript bridge failed: {result.stderr}") - - response = json.loads(result.stdout) - - if not response.get("success", False): - if expected_error: - # Return a standardized error representation - return {"error": True, "error_type": "Error", "error_message": response.get('error', 'Unknown error')} - raise RuntimeError(f"TypeScript function failed: {response.get('error', 'Unknown error')}") - - return response["result"] - - except json.JSONDecodeError as e: - if expected_error: - return {"error": True, "error_type": "JSONDecodeError", "error_message": str(e)} - raise RuntimeError(f"Failed to parse TypeScript response: {str(e)}") - except Exception as e: - if expected_error: - return {"error": True, "error_type": type(e).__name__, "error_message": str(e)} - raise RuntimeError(f"TypeScript function {function_name} failed: {str(e)}") - - def _serialize_flexible_date(self, fd: FlexibleDate) -> Dict[str, Any]: - """Convert a Python FlexibleDate to a serializable dictionary.""" - return { - "likelyYear": fd.likely_year, - "likelyMonth": fd.likely_month, - "likelyDay": fd.likely_day - } - - def _deserialize_flexible_date(self, data: Dict[str, Any]) -> FlexibleDate: - """Convert a dictionary back to a Python FlexibleDate.""" - return FlexibleDate( - likely_year=data.get("likelyYear"), - likely_month=data.get("likelyMonth"), - likely_day=data.get("likelyDay") - ) - - def compare_results(self, py_result: Any, ts_result: Any) -> bool: - """ - Perform strict comparison between Python and TypeScript results. - - This method checks not only value equality but also: - - Type consistency - - Field presence and ordering (for dictionaries) - - Null/None representation consistency - - No extra metadata fields - - Error state consistency (both errored or both succeeded) - - Args: - py_result: Result from Python implementation - ts_result: Result from TypeScript implementation - - Returns: - bool: True if results are strictly identical, False otherwise - """ - # Special handling for error results - if isinstance(py_result, dict) and isinstance(ts_result, dict): - # If both are error results, they match if both have error=True - if py_result.get("error") is True and ts_result.get("error") is True: - return True - - # Basic equality check - if py_result != ts_result: - return False - - # Type checking - must be exactly the same type - if type(py_result) != type(ts_result): - return False - - # For dictionaries, perform deep field-by-field comparison - if isinstance(py_result, dict) and isinstance(ts_result, dict): - # Check that both have exactly the same keys - if set(py_result.keys()) != set(ts_result.keys()): - return False - - # Check that each field has the same type - for key in py_result.keys(): - py_value = py_result[key] - ts_value = ts_result[key] - - # Recursive type checking for nested structures - if type(py_value) != type(ts_value): - return False - - # For nested dictionaries, recurse - if isinstance(py_value, dict) and isinstance(ts_value, dict): - if not self.compare_results(py_value, ts_value): - return False - - # For lists, check element types - elif isinstance(py_result, list) and isinstance(ts_result, list): - if len(py_result) != len(ts_result): - return False - - for py_item, ts_item in zip(py_result, ts_result): - if not self.compare_results(py_item, ts_item): - return False - - return True - - def assert_strict_parity(self, py_result: Any, ts_result: Any, context: str = ""): - """ - Assert strict parity between Python and TypeScript results with detailed error reporting. - - Args: - py_result: Result from Python implementation - ts_result: Result from TypeScript implementation - context: Additional context for error messages - - Raises: - AssertionError: If results are not strictly identical, with detailed explanation - """ - if not self.compare_results(py_result, ts_result): - error_details = [] - - # Basic equality - if py_result != ts_result: - error_details.append(f"Value mismatch: Python={py_result}, TypeScript={ts_result}") - - # Type checking - if type(py_result) != type(ts_result): - error_details.append(f"Type mismatch: Python={type(py_result).__name__}, TypeScript={type(ts_result).__name__}") - error_details.append(f"Python value: {py_result}, TypeScript value: {ts_result}") - - # Dictionary field analysis - if isinstance(py_result, dict) and isinstance(ts_result, dict): - py_keys = set(py_result.keys()) - ts_keys = set(ts_result.keys()) - - if py_keys != ts_keys: - missing_in_ts = py_keys - ts_keys - missing_in_py = ts_keys - py_keys - if missing_in_ts: - error_details.append(f"Fields missing in TypeScript: {missing_in_ts}") - if missing_in_py: - error_details.append(f"Fields missing in Python: {missing_in_py}") - - # Field type mismatches - for key in py_keys & ts_keys: - if type(py_result[key]) != type(ts_result[key]): - error_details.append(f"Field '{key}' type mismatch: Python={type(py_result[key]).__name__}, TypeScript={type(ts_result[key]).__name__}") - - context_str = f" ({context})" if context else "" - raise AssertionError(f"Implementation parity check failed{context_str}:\n" + "\n".join(f" - {detail}" for detail in error_details)) \ No newline at end of file From f8317f05915232dad42424a6f7b67e13ec5b8397 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Mon, 30 Mar 2026 16:14:34 -0600 Subject: [PATCH 03/41] more setup stuff --- .gitignore | 3 +- PyScriptTestBridge.ts | 29 + test_bridge.ts | 100 +++ test_bridge_entry.ts | 126 ---- ...class_methods.cpython-312-pytest-8.3.5.pyc | Bin 0 -> 11578 bytes ...lexible_dates.cpython-312-pytest-8.3.5.pyc | Bin 0 -> 22421 bytes ...compare_dates.cpython-312-pytest-8.3.5.pyc | Bin 0 -> 18939 bytes ...flexible_date.cpython-312-pytest-8.3.5.pyc | Bin 0 -> 21131 bytes ...er_edge_cases.cpython-312-pytest-8.3.5.pyc | Bin 0 -> 16094 bytes .../test_utils.cpython-312-pytest-8.3.5.pyc | Bin 0 -> 18695 bytes tests/__pycache__/test_utils.cpython-312.pyc | Bin 0 -> 15373 bytes ...st_validators.cpython-312-pytest-8.3.5.pyc | Bin 0 -> 12561 bytes tests/test_class_methods.py | 311 +++++++++ tests/test_combine_flexible_dates.py | 612 ++++++++++++++++++ tests/test_compare_dates.py | 416 ++++++++++++ tests/test_create_flexible_date.py | 334 ++++++++++ tests/test_helper_edge_cases.py | 322 +++++++++ tests/test_validators.py | 242 +++++++ 18 files changed, 2368 insertions(+), 127 deletions(-) create mode 100644 test_bridge.ts delete mode 100644 test_bridge_entry.ts create mode 100644 tests/__pycache__/test_class_methods.cpython-312-pytest-8.3.5.pyc create mode 100644 tests/__pycache__/test_combine_flexible_dates.cpython-312-pytest-8.3.5.pyc create mode 100644 tests/__pycache__/test_compare_dates.cpython-312-pytest-8.3.5.pyc create mode 100644 tests/__pycache__/test_create_flexible_date.cpython-312-pytest-8.3.5.pyc create mode 100644 tests/__pycache__/test_helper_edge_cases.cpython-312-pytest-8.3.5.pyc create mode 100644 tests/__pycache__/test_utils.cpython-312-pytest-8.3.5.pyc create mode 100644 tests/__pycache__/test_utils.cpython-312.pyc create mode 100644 tests/__pycache__/test_validators.cpython-312-pytest-8.3.5.pyc create mode 100644 tests/test_class_methods.py create mode 100644 tests/test_combine_flexible_dates.py create mode 100644 tests/test_compare_dates.py create mode 100644 tests/test_create_flexible_date.py create mode 100644 tests/test_helper_edge_cases.py create mode 100644 tests/test_validators.py diff --git a/.gitignore b/.gitignore index 40b878d..417c6ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -node_modules/ \ No newline at end of file +node_modules/ +.vscode/ \ No newline at end of file diff --git a/PyScriptTestBridge.ts b/PyScriptTestBridge.ts index 60e49a5..669692c 100644 --- a/PyScriptTestBridge.ts +++ b/PyScriptTestBridge.ts @@ -38,4 +38,33 @@ export class PyScriptTestBridge { }; } } + + /** + * Read one JSON-RPC request from argv, write one JSON line to stdout. + * Call this only from your entry script inside `if (require.main === module) { ... }` + * so `require.main` refers to that script (not this module). + */ + runCli(argv: string[] = process.argv.slice(2)): void { + if (!Array.isArray(argv)) { + throw new TypeError("argv must be an array of strings"); + } + + try { + const request = JSON.parse(argv[0]) as TestRequest; + if (!request || typeof request !== "object" || Array.isArray(request)) { + throw new TypeError("Parsed request must be a plain object"); + } + if (typeof request.method !== "string" || !Array.isArray(request.args)) { + throw new TypeError("Request must include string method and array args"); + } + const response = this.processRequest(request); + console.log(JSON.stringify(response)); + } catch (error) { + const errorResponse: TestResponse = { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + console.log(JSON.stringify(errorResponse)); + } + } } diff --git a/test_bridge.ts b/test_bridge.ts new file mode 100644 index 0000000..c7d525a --- /dev/null +++ b/test_bridge.ts @@ -0,0 +1,100 @@ +/** + * Consumer entry: register domain handlers, then serve JSON-RPC lines from argv. + * Adjust the FlexibleDate import to your package layout when publishing elsewhere. + */ +import FlexibleDate from "../FlexibleDate/FlexibleDateTS/dist/FlexibleDateTS"; +import { PyScriptTestBridge, TestRequest, TestResponse } from "./PyScriptTestBridge"; + +function serializeFlexibleDate(fd: FlexibleDate): any { + return { + likelyYear: fd.likelyYear, + likelyMonth: fd.likelyMonth, + likelyDay: fd.likelyDay, + }; +} + +function deserializeFlexibleDate(data: any): FlexibleDate { + return new FlexibleDate(data.likelyDay, data.likelyMonth, data.likelyYear); +} + +const bridge = new PyScriptTestBridge(); + +bridge.addMethod("createFlexibleDate", (args) => { + const [dateString] = args; + const fd = new FlexibleDate(dateString); + return { success: true, result: serializeFlexibleDate(fd) }; +}); + +bridge.addMethod("createFlexibleDateFromFormalDate", (args) => { + const [formalDateString] = args; + const fdFromFormal = new FlexibleDate(null, null, null); + const result = fdFromFormal.createFlexibleDateFromFormalDate(formalDateString); + return { success: true, result: serializeFlexibleDate(result) }; +}); + +bridge.addMethod("compareDates", (args) => { + const [date1Data, date2Data] = args; + const fd1 = deserializeFlexibleDate(date1Data); + const fd2 = deserializeFlexibleDate(date2Data); + const score = fd1.compareDates(fd2); + return { success: true, result: score }; +}); + +bridge.addMethod("combineFlexibleDates", (args) => { + const [datesData] = args; + const dates = datesData.map((d: any) => deserializeFlexibleDate(d)); + const fdTemp = new FlexibleDate(null, null, null); + const combined = fdTemp.combineFlexibleDates(dates); + return { success: true, result: serializeFlexibleDate(combined) }; +}); + +bridge.addMethod("toString", (args) => { + const [fdData] = args; + const fdForString = deserializeFlexibleDate(fdData); + return { success: true, result: fdForString.toString() }; +}); + +bridge.addMethod("valueOf", (args) => { + const [fdDataValue] = args; + const fdForValue = deserializeFlexibleDate(fdDataValue); + return { success: true, result: fdForValue.valueOf() }; +}); + +bridge.addMethod("testBool", (args) => { + const [fdDataBool] = args; + const fdForBool = deserializeFlexibleDate(fdDataBool); + return { success: true, result: fdForBool.valueOf() }; +}); + +bridge.addMethod("testStr", (args) => { + const [fdDataStr] = args; + const fdForStr = deserializeFlexibleDate(fdDataStr); + return { success: true, result: fdForStr.toString() }; +}); + +bridge.addMethod("testRepr", (args) => { + const [fdDataRepr] = args; + const fdForRepr = deserializeFlexibleDate(fdDataRepr); + return { success: true, result: fdForRepr.inspect() }; +}); + +bridge.addMethod("test_equals", (args) => { + const [fdDataEquals1, fdDataEquals2] = args; + const fdForEquals1 = deserializeFlexibleDate(fdDataEquals1); + const fdForEquals2 = deserializeFlexibleDate(fdDataEquals2); + return { success: true, result: fdForEquals1.equals(fdForEquals2) }; +}); + +bridge.addMethod("testValidator", (args) => { + try { + const [fdDataValidator] = args; + const fdForValidator = deserializeFlexibleDate(fdDataValidator); + return { success: true, result: serializeFlexibleDate(fdForValidator) }; + } catch { + return { success: true, result: "ValueError" }; + } +}); + +if (require.main === module) { + bridge.runCli(process.argv.slice(2)); +} diff --git a/test_bridge_entry.ts b/test_bridge_entry.ts deleted file mode 100644 index 2490060..0000000 --- a/test_bridge_entry.ts +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env node - -/** - * Consumer entry: register domain handlers, then serve JSON-RPC lines from argv. - * Adjust the FlexibleDate import to your package layout when publishing elsewhere. - */ -import FlexibleDate from "../FlexibleDate/FlexibleDateTS/dist/FlexibleDateTS"; -import { PyScriptTestBridge, TestRequest, TestResponse } from "./PyScriptTestBridge"; - -function serializeFlexibleDate(fd: FlexibleDate): any { - return { - likelyYear: fd.likelyYear, - likelyMonth: fd.likelyMonth, - likelyDay: fd.likelyDay, - }; -} - -function deserializeFlexibleDate(data: any): FlexibleDate { - return new FlexibleDate(data.likelyDay, data.likelyMonth, data.likelyYear); -} - -function buildBridge(): PyScriptTestBridge { - const bridge = new PyScriptTestBridge(); - - bridge.addMethod("createFlexibleDate", (args) => { - const [dateString] = args; - const fd = new FlexibleDate(dateString); - return { success: true, result: serializeFlexibleDate(fd) }; - }); - - bridge.addMethod("createFlexibleDateFromFormalDate", (args) => { - const [formalDateString] = args; - const fdFromFormal = new FlexibleDate(null, null, null); - const result = fdFromFormal.createFlexibleDateFromFormalDate(formalDateString); - return { success: true, result: serializeFlexibleDate(result) }; - }); - - bridge.addMethod("compareDates", (args) => { - const [date1Data, date2Data] = args; - const fd1 = deserializeFlexibleDate(date1Data); - const fd2 = deserializeFlexibleDate(date2Data); - const score = fd1.compareDates(fd2); - return { success: true, result: score }; - }); - - bridge.addMethod("combineFlexibleDates", (args) => { - const [datesData] = args; - const dates = datesData.map((d: any) => deserializeFlexibleDate(d)); - const fdTemp = new FlexibleDate(null, null, null); - const combined = fdTemp.combineFlexibleDates(dates); - return { success: true, result: serializeFlexibleDate(combined) }; - }); - - bridge.addMethod("toString", (args) => { - const [fdData] = args; - const fdForString = deserializeFlexibleDate(fdData); - return { success: true, result: fdForString.toString() }; - }); - - bridge.addMethod("valueOf", (args) => { - const [fdDataValue] = args; - const fdForValue = deserializeFlexibleDate(fdDataValue); - return { success: true, result: fdForValue.valueOf() }; - }); - - bridge.addMethod("testBool", (args) => { - const [fdDataBool] = args; - const fdForBool = deserializeFlexibleDate(fdDataBool); - return { success: true, result: fdForBool.valueOf() }; - }); - - bridge.addMethod("testStr", (args) => { - const [fdDataStr] = args; - const fdForStr = deserializeFlexibleDate(fdDataStr); - return { success: true, result: fdForStr.toString() }; - }); - - bridge.addMethod("testRepr", (args) => { - const [fdDataRepr] = args; - const fdForRepr = deserializeFlexibleDate(fdDataRepr); - return { success: true, result: fdForRepr.inspect() }; - }); - - bridge.addMethod("test_equals", (args) => { - const [fdDataEquals1, fdDataEquals2] = args; - const fdForEquals1 = deserializeFlexibleDate(fdDataEquals1); - const fdForEquals2 = deserializeFlexibleDate(fdDataEquals2); - return { success: true, result: fdForEquals1.equals(fdForEquals2) }; - }); - - bridge.addMethod("testValidator", (args) => { - try { - const [fdDataValidator] = args; - const fdForValidator = deserializeFlexibleDate(fdDataValidator); - return { success: true, result: serializeFlexibleDate(fdForValidator) }; - } catch { - return { success: true, result: "ValueError" }; - } - }); - - return bridge; -} - -const bridge = buildBridge(); - -if (require.main === module) { - const argv = process.argv.slice(2); - if (argv.length === 0) { - console.error("Usage: node test_bridge_entry.js "); - process.exit(1); - } - - try { - const request = JSON.parse(argv[0]) as TestRequest; - const response: TestResponse = bridge.processRequest(request); - console.log(JSON.stringify(response)); - } catch (error) { - const errorResponse: TestResponse = { - success: false, - error: error instanceof Error ? error.message : String(error), - }; - console.log(JSON.stringify(errorResponse)); - } -} - -export { buildBridge, bridge }; diff --git a/tests/__pycache__/test_class_methods.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_class_methods.cpython-312-pytest-8.3.5.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1b6cf5bed54388987e84e5ffd4d94e5c7a8bdff7 GIT binary patch literal 11578 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 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_combine_flexible_dates.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_combine_flexible_dates.cpython-312-pytest-8.3.5.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a0057c6de239de114ebe85d6a43fea4cfd7581b8 GIT binary patch literal 22421 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 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_helper_edge_cases.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_helper_edge_cases.cpython-312-pytest-8.3.5.pyc new file mode 100644 index 0000000000000000000000000000000000000000..365906fb1c522c9cb3db13cb3856e03a1e9f6cec GIT binary patch literal 16094 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 KJ0?Hmmj4Ih-Hu!U literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_utils.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_utils.cpython-312-pytest-8.3.5.pyc new file mode 100644 index 0000000000000000000000000000000000000000..70891de044f21a3082e2405d0add4a4073bd943a GIT binary patch literal 18695 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 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_validators.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_validators.cpython-312-pytest-8.3.5.pyc new file mode 100644 index 0000000000000000000000000000000000000000..93115dddc1175ad24e8a55438b3f8e69bfc56e67 GIT binary patch literal 12561 zcmeHN+iw)t8K0TG&0c)>+J<#tNdSAfI5x%!ghB$ir37PchJ>|YSnrJSn!PwP>xA9f z5^Yh+OQpO8DO5@xD#f7>d8y<-pbx9|Wrmx#qehL?^uceYR+Wl}(C<5EZfh?PQj#XE z$LllS?VLF?=gfD$-*@IuH8lYSj^FHJXSXZCkjck^LQqs3FoD2WXvQoUU5!j<1UdCUGV3=;iNpG7kHng zu_>Ntum-JTQ+}ceSQ?uO5>3d`*i;SC)LI&wszc31Z`>_;CPL0zq4Y%EN|{(Mxy6R7 z%x9LY8=Tl`@oj`(WASSVAF=p$JX+@Hik)1E+$8#gcvQobck6@%(%ZQXHwCxD(%XSEhqsktMAr@143|e+a!eSL+vM>qx<$`4XHNSO zXn)2v<2Kuv@l3m>J(s|d&v>W3%J1_Yv+jh~r0lZ@He;;l!#ZQ!O~c)bd>-$I922HH zF$HioU$eIvCG2Rw%Bwr=x@ioj?6joHpu{WrKG+}DiHQKNok z>tX<&PY|E+2h49ct9Gae60v65mtsjPe7?!7>u9gpl-cKL-fY7>{{j}p%j}F7p1(1g zK)JucNI)IONYp;Fk#L(med0)1mM{{vj_2@fMxyS!8wtmIR3+SiIgf7*nHA>S#4-hD zwti5?6-UN-H0!cy-IK{BFDZI0@l}?m&})!CV>dn+%Q1WsrnHQiNseED9ue%i~ zJtpI8X{-?tAtmGv9G~qd3}RPl-5pM*hcdJ?JDto+#92qpLq@Nl*OR9~#+0nRO7Zz2 zcu=^`S|i)F+y*`W(re)0t}vgk>-gBGt?Qmkl>*wnQ|kE(T65x>|BLlo=GX7i*6%8Z z+RLFxITT*#kUC%r}3ctICfO1x;p_p>F zZSoF33Uw_bweL{LubvdNpI%T8CDdMQ&-uH@kCxjv8P^@NuRAu+kKN^|07&OgLopTH zw#hsE7`d)ts`ecz9ahgIwE_) zZ^{vPk|_bhi6#;gj7sU;J60$$%h8t*7yQ&m7*;eAtHfvy!W%Q*`2768FoKaF5=kfI zi&AA6lZt-iV=B}Y@zK|dI^;0)KyJYT9SV&)$OMuRfjllsIDz&?l(4^)xiv6CZn}DS z@NPwTAaegnU?6g28Y&tvfd78|kH zj0IViAoOO6FBmI2T`{9_BBe-Cy9s-Vm$zcYZCK!XR_t)B=Z~%2g;n-IQEfSEBuiB) z>s-(_K4L+uutGPrw7qgkORJ~f(3;=;I;&YsW@Ic7ktszO&8y6x-8&X}W>ajgxzw+PPn0{h&h?k_TH8=L(tY3O_6Hys zsjBUJtk$l(jkFgYDtC0v<<*mCw6-_Ok)2lMt#dm|q82_??$|yjswYlsZD-1nUQ50C zQ?b;ng$K(Wour3tC(4n{RNJ?{&8xbb7Z0$EH}tFFpAXM_+BHx6^m6WpW61@{1+ZnP*!&>M^%i`u0 z-T0AqV1#tzoZXG{ETzdRPX;3igJB)KK`jlz=)BLYNYZgul8#a|NykB&&eWI`P113q z-_n>=fF#^FkehW(iX`bE#q~4^w-6-UY9Pf`Yhop_&XMBc;TrYEbsEG*NVsuglfgF| zeAwVy48GOi+mL_Cgj-1Ri)$)+v)0j@K!J-z^hR`eBs&I<7c;Anaf_j>YQ`;txm8EX zZEyibhLJJ-QLG`aYLuGv$OwcNFq=}TcxeIOw_kB{q1|W58#p}j6PACxpnm1DvbWW$*ix!Xojhp%J|;HX!#Jlw~u#aO~jR% z2*+?!o2vvnRqW6!$gg98?kpCRk03pO#ZfE)-1G5^+N);?-iX_H zW4HqLG?z$cBO4tMr@2JB7}@kh*+Bw(49FB|py+Gm?of5~<=h>@XBlhMdGm`axfcZb z8i8&HcG0h!;3KInS$YRPi-Pa1ERpUrWO5JGkqMx6k=o90%e$P6>_*EM(I}7*MFF-L z$8)0IOlAmte(wAPSr0Fl5@2qBmu?!vWF?zi1Q3(Sv251Bx3qLabf5e)xHRsqD8Ghc z(Z#atUvY=>mo=X;J)bc Date: Mon, 30 Mar 2026 16:22:28 -0600 Subject: [PATCH 04/41] stuff --- PyScriptTestRunner.py | 8 +- .../PyScriptTestRunner.cpython-312.pyc | Bin 15902 -> 15787 bytes dist/PyScriptTestBridge.d.ts | 15 --- dist/PyScriptTestBridge.js | 33 ----- dist/test_bridge_entry.d.ts | 5 - dist/test_bridge_entry.js | 115 ------------------ ...flexible_date.cpython-312-pytest-8.3.5.pyc | Bin 21131 -> 21330 bytes tests/test_create_flexible_date.py | 3 +- tsconfig.json | 2 +- 9 files changed, 5 insertions(+), 176 deletions(-) delete mode 100644 dist/PyScriptTestBridge.d.ts delete mode 100644 dist/PyScriptTestBridge.js delete mode 100644 dist/test_bridge_entry.d.ts delete mode 100644 dist/test_bridge_entry.js diff --git a/PyScriptTestRunner.py b/PyScriptTestRunner.py index 8f90be3..c9aa324 100644 --- a/PyScriptTestRunner.py +++ b/PyScriptTestRunner.py @@ -22,7 +22,7 @@ class PyScriptTestRunner: def __init__( self, - ts_bridge_path: Optional[Path] = None, + ts_bridge_path: Path, package_root: Optional[Path] = None, ) -> None: assert ts_bridge_path is None or isinstance(ts_bridge_path, Path) @@ -31,11 +31,7 @@ def __init__( self.package_root = ( package_root if package_root is not None else Path(__file__).resolve().parent ) - self.ts_bridge_path = ( - ts_bridge_path - if ts_bridge_path is not None - else self.package_root / "dist" / "test_bridge_entry.js" - ) + self.ts_bridge_path = ts_bridge_path self._by_py: Dict[str, RegisteredMethod] = {} self._by_ts: Dict[str, RegisteredMethod] = {} diff --git a/__pycache__/PyScriptTestRunner.cpython-312.pyc b/__pycache__/PyScriptTestRunner.cpython-312.pyc index 89dd073a646fedc6cd364a4137d0b77bd9a16a9b..d178c360480032d15f73cc8655b6b6fdbb8332c5 100644 GIT binary patch delta 681 zcmaLTO=uHQ5CGtv&F*INPZC2~O^8j^WRq%QVojUW)>ds1N{vtjQ5vL)P;HyCNl>YH z5m6}!buQwe1rLJwQ{)wT@E{gXdJ!oo0Y$uc5G2)uKb7K}?a7OK_?US!Z|1RcmmW+z zUpX9Bfq&T_FUJ;lKXHBm7{%W(4^_M)cG`>KqEu3ga>-Is*=O-Bz-4?UE#aIz1AsB5 zM-+$fnz90q@Ra$4T@gY%uS+ZHmUroP{AAACo_DwPrA^Cexvz`uQy+?fR=!K$M%X!; zqDLu?5pk4ld5E*)wnGrP4?`N%9eQoDP^_IEFH}pFswQo8@1gE?VlOV+cfmonYWGVJ zMT`3w6tLi4aOv}h-V^5g|3=sQ?ULu7b zT4vxLI~)F^_>6zC@j9xxh!9R|WBw740i4Qo^U6Xd#w+#A5`@{Ujx~_=4t^VFv$5eFwD2gWE)AAP&*|GKmk5>62!)V| zO@xm~6J}0buGXf;OL~?LI*ACnay{@KkK}?0s}OBlHGAX(@Ubb-kSS;=f1g d&Ks&=b$+)Pl@z<1n-E>+C2)QdNZro|{soxvrHTLm delta 772 zcmaKoOH30{6o&7aN2eXyp;IWWQekL#O$#j%q|zF(79R&bj}8?tkV!9>15D z-r8&;BHz;YH|fN|XVN!-7~X~ZkjHnJj&ci1dXrqroU-gArX)`*{WG%);zK}a9_3-e zfuqe;_!BoPOfY%K$a#ETa$1uxRze0CN}S=RfyKLgXB8LU;F9TVZmc|)%H{?xXzAp| z)Iche(?$cAvp8Z}fp^$0^fimfUR5Ynw^?MX+?1WGvUgebE}mPK{e?i+vfNb^kj1uQ zt}KeEtsRdDiIOEpQ%qoA34F}17lbE_rMc13m++vYr^HA&N$o9o$>DbzcSLA^ z#JGc^6*Fa1kkDt!Mp>B}vgF~AqPeNqh$~z^7}cY$e!9n^I|5VsNB4V{G*3MVxb^(N z7W+@kPo;mq5i?BZPf;ABh*GrT>owExNdH~)%j7iv{O@v9E2U7dq^AEjKd0V+*ShGN z;X%Rk!6?~dJ(y(}(BmyHOwf(l&{3is5BZ2P6nY8udfnkY;8Z3a$8}+!(m-R*5^Qj1 zh6k@`^|Uura1=f~9Cmx!XekhE@VSv|S5sP;9&}JN;s@c*o-gE$Ceb?Aw^?0V;5=&% zWmcUJ%mqF;yf?dxX5)Lka7_ R$PiOH&VjUpC^Z+!`vd!v!xaDk diff --git a/dist/PyScriptTestBridge.d.ts b/dist/PyScriptTestBridge.d.ts deleted file mode 100644 index f6d82b2..0000000 --- a/dist/PyScriptTestBridge.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface TestRequest { - method: string; - args: any[]; -} -export interface TestResponse { - success: boolean; - result?: any; - error?: string; -} -export type TestMethodHandler = (args: any[]) => TestResponse; -export declare class PyScriptTestBridge { - private readonly handlers; - addMethod(tsMethodName: string, handler: TestMethodHandler): void; - processRequest(request: TestRequest): TestResponse; -} diff --git a/dist/PyScriptTestBridge.js b/dist/PyScriptTestBridge.js deleted file mode 100644 index d749f9b..0000000 --- a/dist/PyScriptTestBridge.js +++ /dev/null @@ -1,33 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.PyScriptTestBridge = void 0; -class PyScriptTestBridge { - constructor() { - this.handlers = new Map(); - } - addMethod(tsMethodName, handler) { - if (!tsMethodName || !String(tsMethodName).trim()) { - throw new Error("tsMethodName must be non-empty"); - } - if (this.handlers.has(tsMethodName)) { - throw new Error(`Duplicate TypeScript method: ${tsMethodName}`); - } - this.handlers.set(tsMethodName, handler); - } - processRequest(request) { - const handler = this.handlers.get(request.method); - if (!handler) { - return { success: false, error: `Unknown method: ${request.method}` }; - } - try { - return handler(request.args); - } - catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : String(error), - }; - } - } -} -exports.PyScriptTestBridge = PyScriptTestBridge; diff --git a/dist/test_bridge_entry.d.ts b/dist/test_bridge_entry.d.ts deleted file mode 100644 index 2866f2c..0000000 --- a/dist/test_bridge_entry.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env node -import { PyScriptTestBridge } from "./PyScriptTestBridge"; -declare function buildBridge(): PyScriptTestBridge; -declare const bridge: PyScriptTestBridge; -export { buildBridge, bridge }; diff --git a/dist/test_bridge_entry.js b/dist/test_bridge_entry.js deleted file mode 100644 index 429289b..0000000 --- a/dist/test_bridge_entry.js +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env node -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.bridge = void 0; -exports.buildBridge = buildBridge; -/** - * Consumer entry: register domain handlers, then serve JSON-RPC lines from argv. - * Adjust the FlexibleDate import to your package layout when publishing elsewhere. - */ -const FlexibleDateTS_1 = __importDefault(require("../FlexibleDate/FlexibleDateTS/dist/FlexibleDateTS")); -const PyScriptTestBridge_1 = require("./PyScriptTestBridge"); -function serializeFlexibleDate(fd) { - return { - likelyYear: fd.likelyYear, - likelyMonth: fd.likelyMonth, - likelyDay: fd.likelyDay, - }; -} -function deserializeFlexibleDate(data) { - return new FlexibleDateTS_1.default(data.likelyDay, data.likelyMonth, data.likelyYear); -} -function buildBridge() { - const bridge = new PyScriptTestBridge_1.PyScriptTestBridge(); - bridge.addMethod("createFlexibleDate", (args) => { - const [dateString] = args; - const fd = new FlexibleDateTS_1.default(dateString); - return { success: true, result: serializeFlexibleDate(fd) }; - }); - bridge.addMethod("createFlexibleDateFromFormalDate", (args) => { - const [formalDateString] = args; - const fdFromFormal = new FlexibleDateTS_1.default(null, null, null); - const result = fdFromFormal.createFlexibleDateFromFormalDate(formalDateString); - return { success: true, result: serializeFlexibleDate(result) }; - }); - bridge.addMethod("compareDates", (args) => { - const [date1Data, date2Data] = args; - const fd1 = deserializeFlexibleDate(date1Data); - const fd2 = deserializeFlexibleDate(date2Data); - const score = fd1.compareDates(fd2); - return { success: true, result: score }; - }); - bridge.addMethod("combineFlexibleDates", (args) => { - const [datesData] = args; - const dates = datesData.map((d) => deserializeFlexibleDate(d)); - const fdTemp = new FlexibleDateTS_1.default(null, null, null); - const combined = fdTemp.combineFlexibleDates(dates); - return { success: true, result: serializeFlexibleDate(combined) }; - }); - bridge.addMethod("toString", (args) => { - const [fdData] = args; - const fdForString = deserializeFlexibleDate(fdData); - return { success: true, result: fdForString.toString() }; - }); - bridge.addMethod("valueOf", (args) => { - const [fdDataValue] = args; - const fdForValue = deserializeFlexibleDate(fdDataValue); - return { success: true, result: fdForValue.valueOf() }; - }); - bridge.addMethod("testBool", (args) => { - const [fdDataBool] = args; - const fdForBool = deserializeFlexibleDate(fdDataBool); - return { success: true, result: fdForBool.valueOf() }; - }); - bridge.addMethod("testStr", (args) => { - const [fdDataStr] = args; - const fdForStr = deserializeFlexibleDate(fdDataStr); - return { success: true, result: fdForStr.toString() }; - }); - bridge.addMethod("testRepr", (args) => { - const [fdDataRepr] = args; - const fdForRepr = deserializeFlexibleDate(fdDataRepr); - return { success: true, result: fdForRepr.inspect() }; - }); - bridge.addMethod("test_equals", (args) => { - const [fdDataEquals1, fdDataEquals2] = args; - const fdForEquals1 = deserializeFlexibleDate(fdDataEquals1); - const fdForEquals2 = deserializeFlexibleDate(fdDataEquals2); - return { success: true, result: fdForEquals1.equals(fdForEquals2) }; - }); - bridge.addMethod("testValidator", (args) => { - try { - const [fdDataValidator] = args; - const fdForValidator = deserializeFlexibleDate(fdDataValidator); - return { success: true, result: serializeFlexibleDate(fdForValidator) }; - } - catch { - return { success: true, result: "ValueError" }; - } - }); - return bridge; -} -const bridge = buildBridge(); -exports.bridge = bridge; -if (require.main === module) { - const argv = process.argv.slice(2); - if (argv.length === 0) { - console.error("Usage: node test_bridge_entry.js "); - process.exit(1); - } - try { - const request = JSON.parse(argv[0]); - const response = bridge.processRequest(request); - console.log(JSON.stringify(response)); - } - catch (error) { - const errorResponse = { - success: false, - error: error instanceof Error ? error.message : String(error), - }; - console.log(JSON.stringify(errorResponse)); - } -} diff --git a/tests/__pycache__/test_create_flexible_date.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_create_flexible_date.cpython-312-pytest-8.3.5.pyc index d3be281c1b4c51aeb785ad450b2f0181d53ba502..76262ab855e31a0381d012ae33189b4c07634205 100644 GIT binary patch delta 2725 zcma);ZERCj7{~AF`|EZYTi34LxJzMM$IG_T0b^xQH?}cj*OAFf6X?3#JLtT9@$?Sa zG!%pc10s_1MT3bZL=%EhlB!5dG{nRpU}PW?tA-d85{MsAVlaO4Jg2l(WMen!ujf4H zInO!wIsbFcJ^C)X_8w}wY_*y=`fPo0DfU7q$NdVG!lQjyxNk!zn(6~8Sr^xb^@tNW z5y^(If&QyyW84@vGMh#=#m!-J+!D4RPQ^(^`qN9+GbkTsqLu`@YsiJ(%8aO;HglY) z0~VUYyu?qq3qzOwC9|kMqsrG2H;a!&G?aW)TpuSI4{@TYU<+3;o0-}y)aGE(!V9O6 z$9f0KK@ZAlLQ!S+i9fbDHDWBSOjjyWT8WI}n7B)dNC^c`t(Zt>`_KUmvXig1r;tAz z*Iie;ZL~YCrZ}y}#d1y?PmVp8#*jd4AY-Pdkv;pNNsnx`)RHr@*kV+P;#TljO|F=G zk((^EY@=ai%K$>2JA zK+Cu)d&Q$e^U&Tg{|a)s#Y93&4g6gQ_+Jfvtj?U~gQar^F9jbR0Ja}q4rl?ebEXYS zK7bw24p;?P4Oj!1bN|}dxitw7-bK&F|BHJcOmWt15m!gDo^{+0ezRnT zyx(D8J`4_}6YVfmg+izNY0M;fklOwtzju6JIMu#c>qkx-c*6CQE0gx7q%lPjBNE0* zjO}Ex-#sshZE$qbS7ZnFe(vIei|ej!+FsbmxBYLJT8o3=SLsL|&A$W(2D}VVQr%JV zL0@1YC@x3u(lN!O$*EIQUc8d1`ah$$=jZ*6Xf?Ur-<(+s9qCsGhmQkZhc4<}l7hRS zx*p&MYybo(a=K_LB_%|>4k~4l@g$B%={229%E=Ud3f#H@J%EiAo(kLtWw>{7KY;B7 zCinqzeA5^7+8rDiL7myN1ADbwV4;GuXxPm1`uKb33`7lYoinb^Pse9mJy-j7%(!-3H|`|en|m}c#6K9!KQjOT delta 2542 zcmb7`e{54#6vy9PySA@ex6!h~Qn$5kV{6G6gB9414#vhM@%__ii>8~GRPqhldZ`hf8jI)gg-wN;6c_vBR6fwzy zoEJAI%n>sYplyg-5@JM5SR+;<b4v@2xFNSv(G^bu**{xID#RIP?N zS_(L4p!01#Kamm|?i=uUXI9q|JMtA_aZ2+9R$KvrW3Bu3<7_(^?i}H!cZJvidyIdF03?(kI zt`Oa9fDkPPPB;>&q&n)BlCpbTk*GVBjE_?X>ss{_aj}5M=PE}BzG5Y`G6dk}>(Zy$ zA-ms|fPn|N=9F+F+<#+F^k#T4ySWKret*5dJdP z0n@@S7B;lznu%*Q#OfMM7W z!Zk2@53dU(1v}y1LLBUZ`*ilO70Vx%edsJ_4=c;rWsg{*;RX&Np5VBj)|}eU9hW;U z#MJGv<#9Pi@vhJz#9;)QE)IuX z%wG;GW3?Sw8AVO6-H}&{g@~e-bS}iGUak9_W*OXwH$;qCt z45Q~tLJM_;deJ5yVMeS+)FXHuX~5PdgcGqD(S-0Lwjh?ing2D6kbBz5#$Iv&g`+*VIa0WY?PxuApG0{GCt)VF$TMT6GflwWJm>sh6=oo6Fdj z&HW=h!Yk;oj4t!#YH4ZSzz{jo@Z@_HCjMqeT5iu)C2eS@j z6-rZ-I@#H_swc^=Z@dmx+$OU2fLPW7dhn_|C0t!~HE=U{VD9z~2Hv-BoL`lmhNZp6 zMY)Fq)Q4C|z&BZ4Plp4OPdCc3VbyEY4e9Ysw2__cxdb1|ZNXaNXM;iCm84AJKM2quI+CIZNsSJrQt?!p?m)8+#7;yfz-yaANPS&k+Ybu=YsD;W>{=yLyB+7c;wEUvWEFTe-VL&zKi+&2(H?&2r& Yb=w`G@vb2KD!Oio<;=Rb%ZPjWAB^$?vj6}9 diff --git a/tests/test_create_flexible_date.py b/tests/test_create_flexible_date.py index d2647b7..700a514 100644 --- a/tests/test_create_flexible_date.py +++ b/tests/test_create_flexible_date.py @@ -1,8 +1,9 @@ +from pathlib import Path import pytest from PyScriptTestRunner import PyScriptTestRunner from FlexibleDate.FlexibleDate import create_flexible_date, create_flexible_date_from_formal_date -runner = PyScriptTestRunner() +runner = PyScriptTestRunner(Path(__file__).resolve().parent / "dist" / "test_bridge_entry.js") runner.add_method(create_flexible_date, "createFlexibleDate") runner.add_method(create_flexible_date_from_formal_date, "createFlexibleDateFromFormalDate") diff --git a/tsconfig.json b/tsconfig.json index 0b8a2b4..d3a9e59 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,5 +12,5 @@ "skipLibCheck": true, "types": ["node"] }, - "include": ["PyScriptTestBridge.ts", "test_bridge_entry.ts"] + "include": ["PyScriptTestBridge.ts", "test_bridge.ts"] } From cb841019fa64ec947fe70b0cc34c8645780afc06 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Mon, 30 Mar 2026 16:30:41 -0600 Subject: [PATCH 05/41] fixed path thing --- .gitignore | 3 ++- PyScriptTestRunner.py | 2 +- .../PyScriptTestRunner.cpython-312.pyc | Bin 15787 -> 15778 bytes ...class_methods.cpython-312-pytest-8.3.5.pyc | Bin 11578 -> 12155 bytes ...lexible_dates.cpython-312-pytest-8.3.5.pyc | Bin 22421 -> 22644 bytes ...compare_dates.cpython-312-pytest-8.3.5.pyc | Bin 18939 -> 19091 bytes ...flexible_date.cpython-312-pytest-8.3.5.pyc | Bin 21330 -> 21355 bytes ...er_edge_cases.cpython-312-pytest-8.3.5.pyc | Bin 16094 -> 16317 bytes ...st_validators.cpython-312-pytest-8.3.5.pyc | Bin 12561 -> 12408 bytes tests/test_class_methods.py | 10 +++++++++- tests/test_combine_flexible_dates.py | 5 ++++- tests/test_compare_dates.py | 6 ++++-- tests/test_create_flexible_date.py | 4 +++- tests/test_helper_edge_cases.py | 5 ++++- tests/test_validators.py | 8 ++++---- 15 files changed, 31 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 417c6ce..2525745 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ -.vscode/ \ No newline at end of file +.vscode/ +dist/ \ No newline at end of file diff --git a/PyScriptTestRunner.py b/PyScriptTestRunner.py index c9aa324..e235cf5 100644 --- a/PyScriptTestRunner.py +++ b/PyScriptTestRunner.py @@ -25,7 +25,7 @@ def __init__( ts_bridge_path: Path, package_root: Optional[Path] = None, ) -> None: - assert ts_bridge_path is None or isinstance(ts_bridge_path, Path) + assert isinstance(ts_bridge_path, Path) assert package_root is None or isinstance(package_root, Path) self.package_root = ( diff --git a/__pycache__/PyScriptTestRunner.cpython-312.pyc b/__pycache__/PyScriptTestRunner.cpython-312.pyc index d178c360480032d15f73cc8655b6b6fdbb8332c5..cc7a01f914cb922c16fd9e16d40c6e45162bff68 100644 GIT binary patch delta 83 zcmZ2oy{MY^G%qg~0}wdA1SsSSQdT&`$(Uf}S&!0$O(nSB9Ty05wJ(9{>OV diff --git a/tests/__pycache__/test_class_methods.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_class_methods.cpython-312-pytest-8.3.5.pyc index 1b6cf5bed54388987e84e5ffd4d94e5c7a8bdff7..78469e8e863e6b460766f3197f16d8ffacde10db 100644 GIT binary patch delta 2054 zcmb_dOKclO7@n~=-d%6vSDKd}!ERo4Ud^M8DikOUkR}8|NT7-gWaVVHjh%HIX4i;V zAQw>qJs^ShhPd>CQUxKQJ<${56ySo}g3HnhA?Se%iqr#wN{IP)W4pCTl|xtZH~)O| z|NqQ<^UwH7;_lLs?;0B1h<=&BzfJvk6rtasQ~v7qj60v>kB-D1b11G1n_w}LkmOXj z44>dF8!PUFn{tdIWQ2r(kpsyB{jqZWHnvlwL;5LKusIzrr>?f?u>LmFc(g7rASAf} z%RvdR>`lk2o@Gns(}y9Zi`{nE97g+kc3Pg?0X)0Gv%pt99q2TY+!v4}l)1zqi>s$x zLxpRyI1lB#6|ULhe3bK7xRy;E5BvcOo{eLZnM-P`w5^|1`a*5n!15H%qaD06aCK_Y zo;jwU>{>K#hLg@hWw)f0`q>hRszwM$yQBvo4q)x953E6eMgV#v5E%j(1{eVt1$csj zHE*>1S*tR=mXb8Pn31*Btdx)FOHC2HZM@kujTfUdD$bK>+K^`eW&ob0V0s|N3SLR7 zvPx!QXkh60!0>Uh0Kq;A zvw5z}lQZB}XLZR-`ZvK#IH9kF9>tRWX-GN1i7bZ0zP}*xBLGo=>OrJy4#F-vB5x1^ z2ly~gLjNo}wqh-0s=SbWWcR}S573=Je2wmFzyVw_o{mlI>Aq%saCqT=Dtfo0TL76; zw^cHB<{RQ$5H=fB#IYoY)T%@_-%~HRn zu@Nj+ih-W3;B|ED269!|?qZ;Oi@%OW?qe&y M`oH}rm?&M;e=`%e*8l(j delta 1630 zcmb_cO-vI(6rO3>ZnvcrM9XgpKhjW2f%0RF5=4+c2?Y5`uqsVqH`v%xoGmdB2_zB| zZ<@JskeC<^H#aecn>QjiNIWzWB}$?({zPsPjq_${jg4?(4qx7W^WK}8@4dHsqpSXk zb(_tC*n0ByRcNUiq4)5SSZVvl?tS#2qGTS2O@5P%WJ6eth<*_x15(75eILKsfI5+E zTtsr(ia{s+66XZQnUb7^b0Xu+NzTeSiE)-Bmp+5t)=!YA&yC~9`e*Ee<0Lc{8&cHR z!1Uy#qO}-i4A@3j)9&I1?YyvQ^jVoVNoVlXNRoD3h(yP4sBtOAA_HU5XgF>Lvz1x2 zMe4#f?VcoJM;43Qcanp#ASs7rRrIl?1Lx2$mQz9lBX;RKM}^jJbz+=PC({E(t>ASW zARizHKuCZ{Kd0}Q_=cdpw(a4}4A`;*WCEDzw!IKrwBPm&1J0sFuF8RI;CaFm%u+Ff zVm)mxGiM#}{=pdORbtnpa&=QU7#Wj;&5yuWVz7YL@S1m{Yec_K0i(20;kj`>sRt=y zoGw8?jghzrVz7*)l=0*w0Drm*EH{7+pc23XPzB%xU=4%FSqA**;>6|6xs0gwSw)b0fipiP_0 z{eX43d$o!DdEAyNH}jgcdg^NHYQ43MUSA{WgJ76|zFBjECzrrY;Gd+Qx(k=_FkLS^ zg9Fs$4C`eoIHXMUg|jgG8zjyH;N?HC4tiG-Ws2w}8DQ2?S0a-O3TFf&G^f-x&M()N zxZDAyu=9X}YY~O`lv+#x2k=aLSlYT@iwW&x+2DU#zo*<~1?fXOB8$3AZ70s%i+<=JWhP|kiO5-k3XdKZ-KNaaueVHej#nCDg)>6UIW5yi<`%$L*ZCxQYH2D zrMF)3fis^cBUm*6{7=OPh*ys1`}d5W=B7wgo(?O`WR69^??{EA`EJA*zeQ*f-LY*U T-?mh+DLEHR^jyv9G`PhdYQ#^; diff --git a/tests/__pycache__/test_combine_flexible_dates.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_combine_flexible_dates.cpython-312-pytest-8.3.5.pyc index a0057c6de239de114ebe85d6a43fea4cfd7581b8..5a493cfe83dd2996926b1f888598a25f423742c1 100644 GIT binary patch delta 2918 zcmb7`UrZE79LIO=*89r=2X_Y?aCa4p94Zm8T7_B;=m}a65Q<2Z^|&2yaqkXxmntL; zB${fQG_9HTslGIQY2t&#ZJNH7nkEuW8aUG$uQ81=X=3^yjrF0twDa5hqoHRHHo5Q4 zes|_KbD#Om%zl0y{k4F$-?7;&485lQzAZRo4D$#cGS`-L;pyy^?G253(G)cgn-Rk^ zJQ6L#7J6qzYt%YyMU0NYR{AsIf@>(%YNC=nY<24oDhDXJn5x0cRrnYA(uZvf3y z8!o~{6R%`?e16Af-gr%ydWP_a7~XV|;msLu*sggN>RGd1iRKkh&zAK{HLs9*MRQrx zp_#?hv}Zl1=9N&dH0!xE&p|zB)+^II7xl`rUil32mp_KlhWseW_J)-+^zMqpqoYC$ zb7LYtFN})h9prJcydW#Hg$kAxZd4NZaojK=s}lN5haAMn5_CZQg1xJ+DWSonQYwDE zlr1N>MB|aOvc$qmBe`Naf*k4tlM%VfsFO5nQTebEmTI9!Jy|mcQ8fuz+G*H}mTrW+ zX5Vn`N=P9Gx`fY``dQJujK zMJK5%K8DK3mo4>`6<}!LD``oFjPRv8@sP#h%F46gLX>%Cx$oF=|LH_uI5iTVTGjfk z-cv*0NjpGZhpI3%U*?jk$!$l#3NfTM5HE2#@8s*#@fEGA1)67;(9*6%*O27hP-p*C zbcCRBx-(kxTcrRP?I0Z>om7%#lUN$V5rvDyV=|7((=rzc%UG6zP~{Lv7syfasoUY{ zhvK>^I1C<0w|djvq)U-MtgcnvLeBN%p=yZy;yIAQ-$Pv97Sv0Qd7o`y5;hhBX@oq@;I z+qDbmzZv*Z-5z8o-`5HB-~0V-sE17WV;f-LsrOVg(bUW^2rNYtZ=c)@1Ajy77(~d+ zpky*EDMDD}=+eZwF^uNGz<~#{rrv3oPGjKI;O?sk*~p{D+G1_xy^tBCqA7?%y!)Nu){ih&8UOr|; zcuD?D<-j9?2Xa~cpsg;AM^wKF{D|z3G$9GL`t){AKI}M1*U?f(EnP=XI$G&E+S&P< zAp|v|WW00F2A0&rPM`Z3kbvlPpPgFn8%gwVn?doUt9=eaEx{l;5cp$9$ zUH^!#OdAqB`zw;B(P=?cgqSStCc6fkiyEk%G)~elBT*QY^dvm!onxWBb~?hRX41rQ zV}gh|E@_mo92eiiNdw(EaZHhR!h1`Y=ea0W&cylD*UDjNuN@Y!OKLlTmhal^Y6Dw0 z;x$uiG|o?p_@G1>W{9D4&&bPER`m#>-yjoyX38Hht-mt12TbFi2F6l#pEX@BzYw~| yx>s2DUAF2TTeHH}++*uk*!r&rSJ>v|y}=bW_*Ad6=;jTpCI%Tv;qePB!1xy%-d?=` delta 2672 zcmbVOZA@EL7`~^4(%V8mfWk^Cv@$+Q}HNSIw8E7HBwY3PrxP@(sm*@7@l2-2y$@dVk6s+ z3PqmHLnQ{nB-kh)iB5CAs82WuGwSdKRW~x>uT(A)HecO~kQtBXKVNYSyaxcx049LE z6sW{o`87yEJ$cySGK$3-uS}-|KMMeb71a2Gu1=u^vk0&0Mvwvf>g$K0R8nLku_B0o zMT%ce%;l0Xl3`ak6yZaWSftSr@C9c$U&mRPt0u6*Yz>cWjh>DV`=rz=#FN&~KSl;6_;(Zth6C7Jc~q*>Aii4JjyyO~`9P|b;i%;16M2dA=CmO`}Y#~Vy(;kB0YxNbzCeqF+9h{rN_`nPM z(P6#OVTA~L>VP#(2O2W=eR`LDPXPz;P#kTFCHFlg-fVskAvIoTsnOA+xuYlWhb`S` z0N-h`DF$J&2bbC{Y%wvHjht@@TkNG-$nd_s-1sut#C>SA6=uV^5sbUX_9Mf!wpK4h z*i!})JfRB30)gat$WUYsHjvQ?v%mp#h)17}AeP==FvdDriTKSCQsfyrI3FFIUbAU(+7k#;X#7g+D^Acx|lyM`Q!b8ZJY6rZ@CQVc+h1^k=) za287Vk6X&_ffDd`AeZM$Tf^S?(DZ(!IM?g4LWn)3z=>W{<^hR8q8S1RzyW+NKG7FS z3LuDY_g|JZ&^{O^zPpt$i}t|;v6l6Gh_i&xSeAI7U?J~OauaKDpQj21aL7}kNUHUw zXN>6dvuB9t)HY<#LY%cBlWu~1tq43kk9KBuFSgZDyO%DMUTkEwZTGuV#dCB2(H@uJ znHlOV0tbM^&M~j7l%@n@TX8};6Y~e6{!m0{#Gj7a(UbV$c;$Ke$w7}{4y>&wkWl$J zZaTEqPEj0W@M7sR6Zj|Z33}s<|$epE; z##mzeV0Dh_@Kx`V}0?Sfd$*fE>5!F zoO|wf&Nt`GJ+pr^%r4^K&4L0a;@2yGeHC3iiqOxn$XR2!J20+FPl!0*@xP?D)zq5GnseB zg^*$c%+ex-7SxLr`+1}Y*DV{N5x41caG!I_S7iEJ+?T)QD_+2%g1Znj7{Ue_l2g-5 zKQxpOC90*)Jdskhlr%x3$|?1!8NCdDXu%~cV|^2c^q0tOYf~|gqaF^|taQsZ^5V(K zS&c#;jQq)Dd`_n7QJLprqe34p(Ju-4xU3v@9j1UHr)0VpQnj$p96i|2?9Ksuh&!~y znElr7)90Oa7>n$ZYboFbUL}AJ2+%5#`e7a5zzz{V8RCY%eO&vD0wtO%AVD1Zgv(npJ@vCZ5O z^e;T`5jRLtI3tw5DMa(x>lGcih<#k~GJ(ojrZwn$OeswA27Iq_q6XoMD19uP9zMBp zOr|1V(`wxx`XFa{G5?Soa?k+)Gq98``#;JhjIXlK10`5AMQR`4L!Q|Pu%BuI>{d;) z>*}@|bJSkQB~*5U^Vc^k3=gMAR!&UsP;MP87U=+u--)kJD@&cZ+$1{eX5 zfb0cwJef#MKSEbwzXJ9bRGB`wP59W(u*;qGILVefcVo)dIs<{zkkkx* zalP^26|ynN$MtUycVRmW@*tnzU0wU!MZlW{CPCo`n-HmO=AwHZ>oO6oJD{++Q5X-z-flOa9pR!gB}(REK? LU-yTI$dmp9&@HFp delta 1829 zcma)6UuaWT7(XYu&CR_xwOJceYulUHXp`8cw$@^UVQyWiZRbiG+p1wUrnxsJTmHzo z5$2GsV}04!!}M$qV~>5YbRc+T4;u~!;)5c@C^!OT6hZJ|Fi{a4!p?V_w9+w?^YG(4 z-~D~(cfNDZ@6zw_KNNd!*Vl`Pjrr%dQpbl8dI*!s;L5?%pDubk0yu4nSQMm~(tL)G z@EEb!oVI3!h=7p^sr*Wr9uZZMF0?zQRYBoaOzT;o96?ImB2p}sT*PMNc$Twn%4)+-C!3dqysG7+N-Q5EejxQympFpkXhJ;7ds#wz z2h;IjtA1bfVeF!>q>HUPfY$=B69|}ro3z2aoi5wl(gcJufJ?vN|Jf{iAl?sfn1LoR za90LtqnGW2ctT&ZkAXl}V`Se5Ba}o{Iip77F-_IT7}OeyE{F^*9wGoizt)(<7UK)A zcQ~$ab}M`X83OlAB)YaEi0!nsU3yM59Q#bg}7*p`EK$-h=p#7orea^Uqd^St@Y*vciD9`?wz+tVj4ZK~0v=TbEx z=MBbxT5c_TtS}ubhUaU%t9D;_JCtuW_xL8e1hSnj`j)E-|HUdRXF3-xoX!;IvNZ}% z20pSw`KB;wptk~rs+ux;pB2p$Ckv4?#S_UIHLvfVlc2n;sdx7G;c9wP6bcfsQ3G_g?51<#I58(ez^kguMqxx_#Xc~qm z=WinKL%~si4*gV>>2E^= zSfI~_W@~fix9{zAeD~UoPR|r(+o(IR99F z0^|U)^yKKHX2a(&^*5BP^XF3Od@8GvgLHPR|MLN$8cUj4RB9PXtDmIi)5f2?=k>|@ z)VVIu@iqkn{jK?;>ZR*>}0;J_^3zXky&YK^Ol5CvR)q diff --git a/tests/__pycache__/test_create_flexible_date.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_create_flexible_date.cpython-312-pytest-8.3.5.pyc index 76262ab855e31a0381d012ae33189b4c07634205..f52690bf3cd7ff07f696ee6362aa12a4e4d33534 100644 GIT binary patch delta 309 zcmcb#jPdm{M&8rByj%=G5dZU3X2V2YNk*-S>Jv9E;b7$9D@iRbiBBrZOi54OypK_X zmCKV^PMn-|nJNRxhGZt)~BIM7)STNZ@PGvHOxG$s6<}mT8OpKE^zmari zVVn#U&t|OKJV#E4nQa@;qN0S&A0;0#Pu`%~!nh5rDN-$tapB}0YPT49H&0f#XJLE; z)Ov(@@^t-kKxUf3B1Xm&n>meEGPB7sLiHbDo?Kzs%L(Q;01chI-cV+;mX!nJug%$3 zcbFJ$H+$GVJulP`647zl3H96pH!5YlAao$npaX( zx%mvE1}me@=C5r3K-3qWi;RqXn~(9`W@gOWyhzBOg)wikot(;KF>zl;ugxjqQ<)ei zZhokJgn9Bl*?Pu_K)Gzjn$4@^beP$;0BtNv*!*Ae5l~XKg>eg5W4c-#vW1lcb7T#xB8MP*}1@2@#y!k+&1S4bpQ{0R!7)jbLX0u^DM* diff --git a/tests/__pycache__/test_helper_edge_cases.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_helper_edge_cases.cpython-312-pytest-8.3.5.pyc index 365906fb1c522c9cb3db13cb3856e03a1e9f6cec..9928b6939ee6b5c3f865f27775c9f37cded4d00d 100644 GIT binary patch delta 2377 zcmb7FTWl0n7@pIe-R^dG+r7~HZF;fnLfN*KmIk3|11VUfv|Lm+z&N`zZKup$I5QP! zux%nn6ZHX(Pb4PzppuXn%^F_R7vn=gAKEG*tN~&)Bn^b1KIwz;|7U0^bt`3)`S#2? z|Mi>y|Ih6=zM1lW@AVdN@SFPUn)-1U$NkD$I*ZAJbL%5*HqDuEMO}oGIhkk$(E|AA zHFv@tbrUX+Q{3?6E1t^)^*L^=l1bGSC1X|A>v9~cVp4ffv`@G>PIe$JDn*NwqOrQ= zvesI0$@!P_tTj5@#mUaIoa|cMi~3KKaKQo!41`IRAC!#Ib5~l5d0Ewsi9$os4KYSl zc|?hf>0kU7;z^Ks?H9<7%q9DETZ13;XaFE=qf6_oJ&}qZ*C~&O`Rt-=4+&=OyF8?% z26nP8)2J*N5{;n5^Q_R_M;h57cR!fD2uRkmd!E?FU3iK)(e((809oHbh^RLiSCyo( zQvypANZ@>#Q{GzQFvW&49~OQ_8cJb%p$PRn026-jrwAuiV^q*JNgoA8Unc3Jgp{*i z{5_9-zMI(`N9C|LlR+=7%IJ#85`6 zzhbM|2xqh%p$9;B0SJpf+Q>dF>7POybTI85g`{z2SInoBUO3;q=w+pk3lwPxtp1E>5)f#NtyYwMpA9ER4glT zNmhj66irBm5Nhk{Xw}2i$AT3n)YkH^9!c(d7 zSdr%DWCSg&;3SBJv(rqkH!1Izk|V%n90WgtN5237v^cjubh1xsKd>rE&GZQCNa!G1 z!*ZgB5LTAc;9kK{6AF-YrY^b$lB(*P%Ws3BMUu$Orup5kJ_|`5P5nb?;W0@As}xXy zlt?FXbCg5_%Q#}QO(pi@uz8i;YkJorAkBJ&FIj77xH%UG#b!&XNkLAH$23JaF`mMH z^s#S4`z;;>VadsVItflDnps<@p**rplM*pm>Lvt2YC;vWk=NEtsZECun~F=#v>F)! zJ*~u5NfYCv63tD@_)@%P1dXgNQfa5Kd7t%0-nU3OnfWOaAf9z7S`08iJ?xK`Uat?< z6ZVc)Ar`0AtfzIy8fxKcYfD8fWUb@X0S$9vcIeprKJi)dzq|R+5j63*7*?qTe_Eot zLPZ5;dG74bU`lleR`#&U2ARJj=%A-zUBo``7`Hf>%xvs*5Fb{>0`|j#7XWzZ-O}Sg z9_Z5t=?`1lN!R(Vb(X=ked`A=yb|krBG=WvbxAyxNNdU|apZr8=UW)-ii}Iyoh_vf zIt@pg*mv8G9yQ5$8(s&(?p$M<;$Wo`f{KtvIEgTcfSN0OP@In@c-iE3wbb;0d*MLV z85>tMLrvXo; zVm6;DdP;j;$vU76;O?ZG@w`BiWig=`qbb>n=}9jBc5aI&Qu4T_bkp-757cuy&WMLL zLdYE)nb*0pTin*4Iqxm5?RN)PRCkkizEk$*zzx1)j<5KUue!lE%<&C3_ziRXhD$Hc e@$IvneRF)@pSHZBylMAC2S+^Y>+TvJIrtX@Fa`$z delta 2172 zcmb7FT})e596x9MpuPQ|ErSX)^ip87+koN(7sDzYZo@i0hD=>EY%aZ5xUMaDZjnhg z%M|ydam$H5Xktu!(j}U#KFo)`nodnbba8gseDH|}<9yH=AB_M1Enku{;UxX#od4%J zzxz9<*K>EzyT5ig1OnsjKfY3<2MPIwU!c`=;c{!cL{pZyMJBQ#WldY-R!Vje#d-yU zVZ;SRc+p#$Z$za+HeT6PvS7wBBAXV7Y+kj+?XS>K#WDsA@*wTVr)jH^y7@b%_h@fE zY`$q|sstZ%0fY=}ZC*5`r;=whW8EBrYAD}Q z`6;zkgLPTok-+b|ywsWh)5R$Da@EsIYxyUhX6obLdiIJ9SXyr)!c3z8qTY%%!E~;b zpK~<`?L8?eJuXX;OFUY&+p0I(1Md8g`KrOsLNH~W2oV4ccO3d6PM z;Xn)Ir~F^u(<=le^6}bcN(G*)tEW!>e%(tYCh8x+1V7OU%+){SyM{IKeDkExSQuD} z#Y^92`($MN(Gjfjzhu~DB`L{Dc@`9`uz>|m#afdD#t*n({HUaaiF`raO@kvCigI92 zBj631bA$R6aaPSvio*k9R!u9+$&G>db~SOnv7u#66Stw_Dkrqiw$%UJPC2>0Cziu1 z>zoX3p%6hzPp8XMlEMNTD1qh5Z5bFI=$A5b3N*!3Fn~zg41OCV#raL+u_6tpXTi0-p;<9Zs0f znYzN_c#_rgLbz{RH>U%lE_7Dy^wr38oy_^*_O6R!3r)DEC8jAXp~w?k+?F{!$c8h+ zvRH)=>9C%81QPr7w`LYRcIc_0m=*{>+bh9AgJszP0NmVo zwAYN>F#Sn=;6pNc@IXTscCbPGJXst)x%f> zI&O3m8D9QjkJrpz1=n7F{Lt8#e&{z~IT*5)d#c$GR!JaCAY>3`5RfT`nVWjB3bY{z z+#FH8dIwyF97W6cjGD@-8I5)Gi;)BL5nhP+bNb(>zIh|2J`7M4Bw0?RmF(n{Tq^kA zgZypjNl(c$DJ8<*gmB=8Xompq87QSc<0rjI9=lB(cSz^&W>VSsy)!UxyG3k2lA7;G Z&7ybTVq5eFa`?V^)=2qhj|)cJ<-c`)!3qEX diff --git a/tests/__pycache__/test_validators.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_validators.cpython-312-pytest-8.3.5.pyc index 93115dddc1175ad24e8a55438b3f8e69bfc56e67..d5d006ed0cad469432aea68a99e093e6097023ed 100644 GIT binary patch delta 1614 zcmb7EO>7%Q6rQm+_O5^GzdErUD_zI^$)>eQ;(}CYsY<8`P(mnaC5(u>?k;qrwG(F7 z74d<9S`kFliZXCOO7AV@K(!~-3kOcX0Si@87ZoJ9&>K*RzXM|4I1Z%(qmH!SzW2TN zX5M}?vlnW=of!E%91b8hR(}7?7#u<9dvLm=Z^O9#GrBO6JK!;Wb$`W=k%lyEiWQN) zy{1%`DiY@eGf)p!f*5&_F0se2hpynx6!U{L=gWBJvlRCyaF$-(mRRm<1rgGGfZeo8 z#Dg9~n*S`)#4DaoX(h@ziE)8VF2=ba<3gKUdpILhLBrz84 zS>3XaHX99{Fg8>+>8m)H6g0!KTU%@vtJa92y`&fCE$37Giw8%YYeGecZ(;M12!qRB z(y{3->17suUkc&=I19KEFV|FCB|9N|54{#BE3~Y6oV=d zj=nAI%#vPuoX^Vii-82|?Gt6l`E%f+cW4Nz|MOGm$AgD_k3dT1o$OXTNt4;Ro{FE( zmNpe%_2$bxXJ@0Az&cWQ&hO)EHx#5p+>KO1`+F(aoO8FT$=VL-dF3_af}| zk%#togoPI1Znpfe_yKv0Ofv7543yZRN)zOTJKVxJnWIOP`+M%hnv(ZD$BIbz0<3z6 zdT$3^DI8q>zg>uQc7gTv?|U%MmULs?1OIng16sLjS9IV2K46cvk6qof&f(%dJire> z1eAm;)|!TC8x4z0(D&{uEsrtU^)Ir&k!jS3mwALA0|EGS+0=l}o! delta 1658 zcmb7EUr1Y57(XXTlid8NCefrZVy=IfiJEE*qo%f7w`y%WwJInqwBBBOZ!ztSVJEQ= zyWxh8J#>S~c^(u>JEkwfed5skXIJe9ECZ=x<$h7FD=$jAFabZCNuFeHiV#8o297wacUmD|hQA6zjgj zgocn}TSkh#%%y5LCos;j$JKJq$vD>@=PBT*dmD<6(k|TI@4Y2!X+_Sf!|9u1_T!4S7`sQo}0#sZ1rzKt~O4(gBEelj>MFtD@B9-VR)Bjn<;zXi5kq-|% zg)`AsoT7Kzn)JoSJLaZC4xuyl@#0~JP*WUe*3$nU!A;hp;VIAkepr_A)rWE&IDl35 z%O%;(UDmf_NgUua2ac%Mu+Pk=wS0OmM-nvIop8n>iJ!Zfe%l=$fVY-(GcX*oqDUDv zKbuvII^HIGtt5>8(3*>x`{F})C Date: Mon, 30 Mar 2026 16:50:13 -0600 Subject: [PATCH 06/41] set up stuff --- PyScriptTestRunner.py | 9 +++- .../PyScriptTestRunner.cpython-312.pyc | Bin 15778 -> 16196 bytes package-lock.json | 23 ++++++++ package.json | 3 ++ test_bridge.ts | 7 +-- ...class_methods.cpython-312-pytest-8.3.5.pyc | Bin 12155 -> 12640 bytes ...lexible_dates.cpython-312-pytest-8.3.5.pyc | Bin 22644 -> 23154 bytes ...compare_dates.cpython-312-pytest-8.3.5.pyc | Bin 19091 -> 19641 bytes ...flexible_date.cpython-312-pytest-8.3.5.pyc | Bin 21355 -> 21872 bytes ...er_edge_cases.cpython-312-pytest-8.3.5.pyc | Bin 16317 -> 16822 bytes ...st_validators.cpython-312-pytest-8.3.5.pyc | Bin 12408 -> 12886 bytes tests/test_class_methods.py | 4 +- tests/test_combine_flexible_dates.py | 10 ++-- tests/test_compare_dates.py | 50 ++++++++++-------- tests/test_create_flexible_date.py | 10 +++- tests/test_helper_edge_cases.py | 42 ++++++++------- tests/test_validators.py | 31 +++++------ 17 files changed, 123 insertions(+), 66 deletions(-) diff --git a/PyScriptTestRunner.py b/PyScriptTestRunner.py index e235cf5..404d2d4 100644 --- a/PyScriptTestRunner.py +++ b/PyScriptTestRunner.py @@ -24,6 +24,8 @@ def __init__( self, ts_bridge_path: Path, package_root: Optional[Path] = None, + serializer: Optional[Callable[[Any], Any]] = None, + deserializer: Optional[Callable[[Any], Any]] = None ) -> None: assert isinstance(ts_bridge_path, Path) assert package_root is None or isinstance(package_root, Path) @@ -32,6 +34,8 @@ def __init__( package_root if package_root is not None else Path(__file__).resolve().parent ) self.ts_bridge_path = ts_bridge_path + self.serializer = serializer + self.deserializer = deserializer self._by_py: Dict[str, RegisteredMethod] = {} self._by_ts: Dict[str, RegisteredMethod] = {} @@ -183,7 +187,10 @@ def _call_python_function_with_mocks( mock_context.__enter__() try: - return rec.py_fn(input_data) + result = rec.py_fn(input_data) + if self.serializer is not None: + return self.serializer(result) + return result finally: for mock_context in reversed(mock_contexts): mock_context.__exit__(None, None, None) diff --git a/__pycache__/PyScriptTestRunner.cpython-312.pyc b/__pycache__/PyScriptTestRunner.cpython-312.pyc index cc7a01f914cb922c16fd9e16d40c6e45162bff68..0da5d3c95336eb5d81b16e74b0ac6e0c2e70d7b8 100644 GIT binary patch delta 2649 zcmaJ?U2Gf25x(W|j^rIl{ZJHtM2e*TPnPA_mMvMbEhsV_*>PppvfJ2!%c$B@O@$$q zy%S_AveYI<5%({Sw`r8tNrWN~id#PvOp*4jb!-%9UaGo4g{vP7v?!1~v2lQ+Eedqz zC`Ps$WP$r`X7<~eo88&DpT2Nw$^W6xrwA~v|M&OVk9PgS|Dr?jq7LK!JN(Cveajxf zBhWVL)ZJ9lJ-RyrQ&a{?(baDdU7dEI#kRUW*XpL?v_d_Xq_bY?pvpBu)rZ$oukNEh z-A|F}rv6Lv*&12{wpw_W>vVWk!%(+gr>j*B$n}?;XX|MIhz;<3F*gX@0Bxs@@NNK4 z{%HpdfgA)|7~YMLXa|jepQx_Vrb^u9N`5qk-v@ye8i!0mpGW;aW-~CuGy%?%@L)ih zrXWy+c7A4UfpC?)TY()didvUdf1)@s#j>;JDc#^nXK|lm=qx*#%P#25@>2Z)O=h|| z$MRDWp4jG);Vr^*|3`$+^t5oxdF{}mxF}>?RmP$dGqx?5gh{XAbJ6cNwO17vOA9u+8q=pa5dtEFJ9#}JfPCnHlSx!UGO|vF6#Ski! zAQ_lJGRgle_qpE(@teY@ejadr)gNtp+4;Sijab*q&Of;$JnNcJZz?^H4~sX4rBQe3 zcdnm1V(sXv0|ARylUX;)JqX+RE6NG7qjXP6lTM9{YsB)gxqODsP3AJDZkU?0!rqJQ zeuM#j%-c)GN-uan7JDBDi=|}qvvX#KPMVW!1P!&b#SGJpxty6fU&v9NvHiHRoAcV! zWQu=O`zv=E44T7g<9D^TIC}y$nV~l}hJbD=fe-MH>k8yNKU<%U;-YsQECH`^&CTq* za4Fl#e_g*VVW(wBI)KIjgg%6$2*F*{(QJSWJkjv1tD^ieCfR+;vMfjSS5)hG8sGzc6JuK}y1?d{zdug$|yo z5AeUn0_1AxpRqdP-`YXUmQS`elDnnt%@NXkQCSpbYQgx(_*Ng->-eN6pejGZv|*S5 zK9-1+AWziXJGkm{kogT;A6YIiCJUNvu}{ANLZpd{_`>Eq{9``RmqLUaQ!6&^`- zRF38_SQmENM{_5Yl&^zoQ2wt}A8FvdEysFXzWp1K*xQjeBCCUUBE47SjX>!2;a7+M z5a``#O1wS%#_-zSJ59Sjm4){0{6Nh<$hXxdlslyKw$d}Ld1u(Z zbd%uqlqRxD?SjH)_}@E=K@-FU-|76b`NDiY30Jznn8D4i7Lw+#cb)8+hj}ejX)~2O ziEC>RQUI1Smo?1Ivgbi9x%?bX*>^!l2{+x6a)n81Sh8W#!ko!2fpwwZJ}+oVO*&;&1Ip#=Zm7mYCD?mbj3eh4y4Oh>8Pl?HD1i z@z`UX4vCb~kG&_8asKbU-N#=;|3!q)GPK>Zt7zj0KLoJ+BL;lFnJwhg_RgOMV|6Pk zA&~keH~L4(3V*+!60H;)_z$Vm#PRWQ_GP4#!x~|$ylb#|xeM7R0W9aS!ql^@6LoAY z%#9!;NC?{y><$$~X%B)6z>>_l*__UX&@hOgA)G_Nro^!P8SW{wQ@nu^{v25dfZrZ$ za6}yZFN4YDjXnD|`iK6ZHcF%9UN}~kP$dbn4 z=X%4gvd5lMka=M(uqLe;Ysz|J_j+Sr+4ffU3X1=}&t?q@iMCJFkkow-92qP6>cq;- zdQdA%{NT`gqY3G0axWAuODI6>V7TnEMYmAfaAkBQaAoZBQrTmh6d@8XtF{RKefNE~ lc#QAae^vc1CTMr|ll+hS=gBYm(E~?nmYl@@iGXM)|35(WNSy!x delta 2218 zcmaJ?OK?+F7``X><|fUn&8unBv`w3olBQ70%NA0=3*9&_s?~+#^Z##O!VI37eE0hw=l{?7 z&woxP4!`}9>r0zlg&;tjwu~3n3|68Dz0c&Zq^DkYY^33<#NBu zEl15eY}~}H7a1?m?3!aQ(;WRK!^5UIxm|M^o>uNSs_rP{PSm;ZSEvL&o8Q&w1cJ`vPmq)n|zdo*Nu6`5uBln}O7{A2@5|8l!1}f1UJorH3 z5G@ZA+juE%B=Bg@c0TVKM%NNvhQZ45M+sG4fr)~=dQKa`bW|ZLuSDcD{$RW zTx1xRk#RY}3b^Z7%sO+b^GA7c8*1{lWO{H&@8x^+J))HqMS~-~Ldy=N^xl1$6xW1U zMKc#dxqA!i1>OBV>x4y~x^OqDF+D{Qs|i{OjD*cF^RHd4~EvL^DIaomXe*hN6rvMa z$^z;xw2UsGTrgPXW+!svi1zs+w!xd_LDM!9d>7jZ{)!Jwx*IM>%9s~kDe*!}#IHo9 zhg9CGVd0p%-M;+o)6L8Dq?Lm5POfD;MQH({_nV zhir#!nqHA&tbEp4_`qTQQ&|Ivs>dn^a8BGl-3J-u z|LG0m9GAdYRZ~TtX<2&gBJ*SVyt+_1x)e(jgsG~K`VNZO7^D(WSP^wIH>`>Fcob*z zSAo#Ek<%lWLSF{zzK_+xN6~UnV#BS}MDMjZI|9jfVmHF(xGbj6pEOD6iI4cRNJpJp z(gW$ta5{o_BqM}?e7p+p#$WM`buXel9+@18o)R_L3a#tH}sBAUtZmfOgEG0TfFt;ru zyf{s|GX#K;ceQ14&3ZDE?lQ`~8I`m9pIfZB2wyjLuyat>#938tyy*|-7L62RJ-M^) zI1_KdiB;uC7LalSLSE^~^bLq=k`)3gL7YG(FcTP8+fQsM0sT?r&HB(_N)szdLGO^L zA)seO(ES$F$)bnA*y1%}#RLHaNHqIQ_ZYM_M-o3GY(b zpR`Ig*HnRFSppw7f7TH;KgaF|LX&19=4PX75PqeocEqubNujRfuXWV!Lmua rK^>CKIps8HH{5;VjQw?5jPZPJ0KH|H-GHB3))o#c%=L>zXe{(!p%&-E diff --git a/package-lock.json b/package-lock.json index 6513fed..bf65ed8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,30 @@ "name": "pyscripttestutils", "version": "1.0.0", "license": "ISC", + "dependencies": { + "flexibledatets": "file:../FlexibleDate/FlexibleDateTS" + }, "devDependencies": { "@types/node": "^22.10.1", "typescript": "^5.6.3" } }, + "../FlexibleDate/FlexibleDateTS": { + "name": "flexibledatets", + "version": "1.0.5", + "license": "ISC", + "dependencies": { + "any-date-parser": "^1.5.4", + "date-fns": "^2.30.0", + "edtf": "^4.9.0" + }, + "devDependencies": { + "@types/node": "^24.7.2", + "nyc": "^17.1.0", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } + }, "node_modules/@types/node": { "version": "22.19.15", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", @@ -23,6 +42,10 @@ "undici-types": "~6.21.0" } }, + "node_modules/flexibledatets": { + "resolved": "../FlexibleDate/FlexibleDateTS", + "link": true + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", diff --git a/package.json b/package.json index 7090561..5cd81b3 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,9 @@ "build": "tsc", "test": "echo \"Error: no test specified\" && exit 1" }, + "dependencies": { + "flexibledatets": "file:../FlexibleDate/FlexibleDateTS" + }, "devDependencies": { "@types/node": "^22.10.1", "typescript": "^5.6.3" diff --git a/test_bridge.ts b/test_bridge.ts index c7d525a..164fd14 100644 --- a/test_bridge.ts +++ b/test_bridge.ts @@ -1,9 +1,10 @@ /** * Consumer entry: register domain handlers, then serve JSON-RPC lines from argv. - * Adjust the FlexibleDate import to your package layout when publishing elsewhere. + * FlexibleDate comes from the local package (file:../FlexibleDate/FlexibleDateTS) so + * imports work from dist/ (relative ../ paths would break once compiled into dist/). */ -import FlexibleDate from "../FlexibleDate/FlexibleDateTS/dist/FlexibleDateTS"; -import { PyScriptTestBridge, TestRequest, TestResponse } from "./PyScriptTestBridge"; +import FlexibleDate from "flexibledatets"; +import { PyScriptTestBridge } from "./PyScriptTestBridge"; function serializeFlexibleDate(fd: FlexibleDate): any { return { diff --git a/tests/__pycache__/test_class_methods.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_class_methods.cpython-312-pytest-8.3.5.pyc index 78469e8e863e6b460766f3197f16d8ffacde10db..68fa8a0cebf4da882cdb21a3834ca9937f07ce62 100644 GIT binary patch delta 2028 zcmb`IUrbw79LMkJZRuZHx-bk4=bSa<>bcf6N6dUyh*{1Gm#wSfx!YaD z#vG)Z*nv%KnpB1zs|MU92t+UlFAK492zM5=x-CD8MHW_wPGam!V|1KugY*{oDmwtn!ut&$1XcoBa>IGe zc~gG%j-0+Fr`K!g2CNE?6=(v17kC^7=LNM`Je2yVL@1f1%~ea6$tniJ2dh0SiPQ;W zT8?l4BqPNqrgj82h*}XYfP$IpsT=25EeEbDKESeY@x+J2ABV1lvgsR(*A{Qi%-)$f zb!+C-H~!Pq0|Amjw<*;!jYkskj0~p4WTiOMb{U3=iJ}481+k7C+zF!<0sVQs^xRDK zsH~{_Q>ny|te#B8g1w1Iaw-<-?}t4bSkSVNb!Yq52aZvXHfs7@l>8vlI!r0j;#fB& zQ_)#Px2ljXJO%k^HS4nulAYSyRx|N7L41BO7EvQ~FDB_>@7Z1=ZuX1q852K`-Sh6& zj@bQ#c-WjHTz3ScS`hfqIk#$YgmAN;9QVukLalbY`~j(`$E5~DBf`SIcln4<``hI- zz>TkKX*=4CI#1XNqyL!UMKeZJTH?hT#^${s*}(e2iCjHxplg$+kHn>-4>SOpZs9H{ zDy20rr5%V)1b^(Oany~##?WUG&mnpc&m(#f0|0((ec{6$&|QV>;Uw=_#?LokJh*88CKzBk#VZJ#UQDlXVVn|!8`GMr@4r$Xn zmdJs6T>F>ZFwNN8J3Bf%0v%m}Jzew!2H<@@oUs&i8pbdapVCPt`7V(tyYA~DNmk}h z_Zn#j~CarMs zU7Ppf4cuSS3l%lQr`o%X`}4g}wI7>C{^#lsHn-T(`uKe_?3!k0nlI(d9oxcOh|g>v zg?He^^n4_tl)MIA@S9b54fbSTSsx5NvY$T2s59gtoZ%?%eTPT>ZDu+7i3pL=9oqEdAT&mG6Q}gkJ8lO|>KK4am zFL{Ce5$Gi2Y0?8|KzA+f=5G&g8&CH50e@L~ld0HzLhh&UKrsG8P_pbw@PO;C Yo8I-D{@rY}8J5d_b+CuQj1f=&2V={+*#H0l delta 1647 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 diff --git a/tests/__pycache__/test_compare_dates.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_compare_dates.cpython-312-pytest-8.3.5.pyc index 0ca82963bd40c56e0d89163a434d211f64f87626..1f75c414840ab7cd2b64f41139acf9768a5e88c0 100644 GIT binary patch delta 3035 zcmcImZA?>F7(VB=rJs~ysUS#Or~(xc5pke=C^`kVF)0-Vm0GyB(Ap0@x1bnx#PDN3 zwq$s+7&Dic?T-XAw{^+3EL-r$7C)9)ni%5UmW7|&FC7}<5|^F#QVOy$wuCk5bI*Ct zIq&>$^G0us0~2@plJ>J>IAwW%fiuC%r`Q zL{CRNJd=n{ETxhDa9ALclJ|?zvqmP2db)|9?-e!$#Ga%BG6f|=AUM6B!DVy~1rW_l zGb>PlgQpb*V8@~#kFP4)kdtenEsCN|PKBfo`cDYKLC>JzC6XaI2}Q!uK1r9H@!r8v zEMcBjD0>c(ET%D?{l#1cL+E%$O^IIlX1Gym151MPH`tR1gz8Mlt}544g^=3JFjl1^|{oL^2tOpxI8$ zFbB3Wz!`BZgCTVF=*O*}ANl0S?Nj&9-#H%-oR|y@OaulVk5kr?m`*(D<(LP-2gpSm?LP z27Su)02j+p)hQMTR?87MR-IiqYHA^Nc9;|}7*))VB~>WWb3!C(^g~StE8pju1{G1W zO|)4%zIr_yNLsmWSWS7!Hc(MVZMwbKOntgn)xb*JiRu2DJnfIR^^>;B30ox%*Q}<~ zy6J4=q{cC!aa?P@KKO-m{I!Ygt_PYnM*F#bB(E4$l>j(F$HFj!zs7c0Dg}Tz()-G) z1&%H4sf_?7(g5%Z0}(=IpZgIry>6<;?R3iIP|vfq(G8iuF3GlZRcE=2X_0gSdviQJ zUQrOqK`ZctTL3BnssQ8$_!5k^0~FG+>NRCM zU|0)K2T%{NbOU-^?ZUluPfe-;#lkhuaOV=elIe1q$stgA7@z|HE48>09^ondTWObN9fcJD~?{O-GB#aY3(|EjvlDBT8CiAqBiCQ830e; zs{P0`+q8OE8-u3x$WG(+`Lwt0VzR#pvkUZmeK{SdbI_*xlH>}XTKOO;IhmAfY`B+H z8<Bl$7o<6c+FQ~5E&Yqqf2ZwOFM;h%yogMI)2 delta 2626 zcmcImTWnNC7@pbt_H7PHSm*=6c$@$1_5uVd2~M)#nfd3R z|NmzGIp5jOU#IXYU2@)L%OkuUy>Tuw)^cjeGTk+D%CobS5*?AP{AZT)&rtLrBN~&Djbxo4{@P zbGE``)N6w}TIr)|K{yc`4ly~Hd@>P~LP=Rs4N^o&jxOQdWU!w_q(OO2R7w4!|CxeG zw+g;9ekv66jN@dSs`M!I>ca4*sl3?BZ8KI5V`O6ML@d+NIYZ!EQxj*MYf(LJ2c~PQ zQh)jX)-9MQgXi472gdEKtdRS%A`avF`Mzq5CBlalW`qy&dT22igeIFRXaw0vJT5cX zDNK}>@l~o-v+|9Em>)x}16Tf|v>fF8y=G*nG*j4B@3dZY`=;D=lkU3I#e6F*E1I%+ zCM}-#mw&SBvtwVEPc|L6VCiNe{AoMoT#j)Z2u=)uWfNIBj$LrtBns`=bt8!Il)rAl zw(h!Lzw1FYY4_nkVzB z1;ZglR@fd)pwSnxrxCYc4?%!b@rSk^I-@71{d8JqHTJ+z$=fBfma96!*h87N(vqqu z{n%hfnhb9@!=18xxZ>DM3&7!g+<+B;>e|(JJ+dciUC!g&QNiV7B)u6bh)@DP@r%U>mcGkfX9;$6rn~wx9pQT{@5;NEmnTqN&{*jLe`Sq; z`En#-u=15f`JLtdGr0o{bfh~&nLz(N@>}fst?&g(FwOkg{d(5Shox}BH@+yr-}%bf z1MCZ?55_Wm@%v1$tvYJQH1h2}x?YwpEWc&$g&0n$LO*7lwupm?q&O;6iSp55Ih>Rwk*&q)8iaKS>k;m#QT%nkwxykhV0-FmWa#z19U7XMFs|BCdZRG{R*H<2SO)85GESjRE8@JE*gb@ z8hmiM!3b*_MarP9(d9aVPPfS20kk3X!HbO_*|Xvn=5mMPHZ8*U&E+uA^n6YSS%z~K|JL)lu+jOjYqp87YTJ#Rm`Ze^m>x)N*uIULQA}I8 zhCbc0)?R=t4K6Fd@PT-_$2b9aYmety)e;O2M26&GP&G4INems4RTKWti6_}cbkn|o+P%=S z-^}O_%fqpRG%_SNvX{6s-zQNN4%2!{>19Grk_%+nPsDbK)LbH)uA0cwiXRP@7nYr9 zoiaEk4UQi`+Pl1VMz33{8xyWu_EFkHuM+KXeL&Y{(1zOMx@1|$N7o3VR;YgggAPR$ diff --git a/tests/__pycache__/test_create_flexible_date.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_create_flexible_date.cpython-312-pytest-8.3.5.pyc index f52690bf3cd7ff07f696ee6362aa12a4e4d33534..1a28fbafef45ab5c091024932e5a647112dcc18a 100644 GIT binary patch delta 3039 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 diff --git a/tests/__pycache__/test_helper_edge_cases.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_helper_edge_cases.cpython-312-pytest-8.3.5.pyc index 9928b6939ee6b5c3f865f27775c9f37cded4d00d..b1807556e41d5a4ad159acaf41d9e5a7871e09c3 100644 GIT binary patch delta 3023 zcmb7`du&rx9LLY;bG>axAH%|)9Rs$48w}-PEQ5hOGTj_wz$i$$>%C)lYddysd29hY zgb?CCW;w>0NTUCkfDlD$6iJBwWjY^WTO^YvX!O7RVI0m_V~p|lyY0rd6oe-Ip6Bm( ze&=(3_uP9c{^?no|Bc-)5cn1S`?cbEZZLll{b#UpZB0Z72$V=fqG4;m3Z)@zi`W7- zN_0fF!QU*~uTV53q^pEG&4s$IQeIo`rls6j;vO8f#wGJPBITVUS1nicnTY|1?C7e< z)Mu+KNWv9erUw_;K_n~K+NS#gu0HCsPvC?mA59sW#aO3Lm$GdS%O{ogu>67;lT!sD zCPNwQ$aeDQX{|Su4N>!L0L4hA8bF4~DbhvwpxLb_UAk;(z}U_7qJB5=>G+hsvc{)R zS;I=V9PSJD$s$YHvW-YjG}f6CvMovMyG&D5;_n)d>|<_lZ-u{F2r@t}m48%yxjIR2 z=MU$P$Q|Qy@2K26R>Y=Yl*|dx2pw900jLa-*i7&c>Vps=WZt`QVf-qc=8Dla`Sff7 zn6Z2mJBZJ~aL3Gsg7GjsXyyP>(X*Giu#M4bg7eg3kO6Y>;I&Q)(&V~co|@FMl{n6R0e_)X!fr&dCD$`X^+SWpsUBHMt|{QNEJcIx8ytPOMrud-E` zed87DM=RDRT5JOKxW;XjqqfRRmTS-5P?Gl1y1jR7`&a>2?d>afp?wVsV$k@d9IfEK zJ{nTwXbclkWyrALdcK1$Y@<_pBDB-{I;*9PW{eIbmM!>@T9?A43GKiv{6~j}mL&dk zFiMwm#kHAM@ef=}=}LavwZdDCD`{)4f?5uOryQli48b8@?=G-t(XD~$iI+{+^<``= z`q+qqsjuSMc&C5N#aDS6;zKyvf})+Uh3&?AE6N^}y&x%fOg#%W%mIlQ_PfyGNHweWah5iQ~G z6!vDo6g>hA-&O13O3|X?FK`Lm?f^-aCmY8a0+~a2d^XXt`WE!^zii+k2#J!M8%i&F zm<5GX?_y&FI^XAI-Z!&(a&XSR)kM-;K&uX+B_<0CqTpWoosi73_kEWCepy}bnL{l>Mcz3m~kf+tsR zeXPf96|LZBYTZwI<3!i)iB}-O83+%_80S%u!Mi0WOGh5^Xr0H6%t8RolpF8lX3cUN z7;e?>sk1QRhBehNRL&8;W!=HWnG0H+lQI`zI5g;7$iH3ptClqr_-352g8-`G^_xwI zifOm=BQK%W4bPPNalz~xt#!}H-q2X9^rRVc+`n%L`gs5=SH|$){KZS>-NkQUp!3*x z_?q2=twON9__<#f*MhU)Z%Z~mDA1qX5E^&_7$NaG~v9wV6a08 z%fVpE%w)AEd|XbMARXa7&+2eo5G5%Xkz<`bQf7;DN&lVakMu~#!g3wE2-D#JiHgJ~ z^_0?|kii|YD6P{|-Bcc-`S*yS_Ge2G@7z@R>|H0j>pt;^*&yf!Op^kk=5gX2CC(Ao V@{ub4_he(*bizP!uUesP_CH5kpP&E$ delta 2454 zcmb7FUrbY196qQ2pzSR!6sWc6TG|N}M8TNjS;eG z+~e>pdtLT0Gh(ubT{TP0mhEN1En5niA=bEM+Y^b~U_LZkvSjCX3&S>DgeLvYxxats z{J#60bML;5Kc6i5)?zUs_M80co7hLq(tLWaiMuL|K_W@TswYOmi1$ji0iB0*Xj9~1fedR&y zqVJdc@K$=Se7nmHS?a3QY$n4?bQ2#KU%XLK@1kIYO-!Lw&#~0;!TBB`yL7E$t-q_? zS_h%K0fG!U z-eYyucBas*XSQp;GrW<$!=Bb-P~o|KbD|U#M5%DY!}$&W=5gwy%=jTSSHHKSh|@X! z#^{p&HXZQ}*7ia&D1-C?o_g`M&D4fI5 zxU%HmjBWV@(am)rk-c@t)>z}0fyfehMI4se)Jx^b1sT0xpUU4HdL;|l@Lya{W3aa8!pekP#*I<)*JUD1z zzr#SLm}>`ZJ{X$JemjP8I={}mA-}oMZA^Ngr~nWH7zdaDxB>u~c}o(w_*DS@$AjP9VmA5ShL7&3=6A1a>H}b=J@=tX^#EpC91QKFtk+%)~&i20eH$y%}tpR8^#hkYb)6ip3?KPZtqM z9*e&zr48c(kw%l`CGeXBQREX+a%4=*c9%=?-^DG7G4Xs{3XmHtoV`0F!?m-d#Tfqt z?LR=99-{+~kmWIITs9!9=b^5c4z&6jo@zB#&6Ih`fH1d!D(6sT%2Aiv(fR{Alrc=` LFf^zRYKQ*9jyh%xdqlbCPp7j z_GHe3S;CTKOEw5@W9h@(%i_zHIv=D>9D1KzVrE>n1jjPwT$Y{h+*=ejbT+xad(QXW z@B4oD`+ev3kG}7NrmH5Co})kE@9)CzoHyCfU*}z)`huFXTu^(KyQrJx!OL--c`9x< z7t~X`VOFKs@|n#@ZKf5Qh1ty1mbYRnh$DACaiA6_k|P1oICIBM^w;Dr2Yyg0sV;w*q1rag}*aCZC%K*G$ zDTPUHs(Y^O%dRiFQX|(Uu1w5FPAo*m(vh+28^^JgKHyeKda*c!!+~fx5u!~AhQyU! zasb^yIT#v1M)|uXka32C_Pn*KEcw~!Ec8;R2!$uPR9UJuv$qplllRq^`2y%2wnHqB z0%)<)*B4O0=Nc3>PfHuRb(L_>l=}m+|pNQHZM5q z($2c1-k?Lp<^^pq(YD>aze*G+jlO`q_!kVMiS4n#*ouV6fkaV2D_0@w=hEI>5?qgzXU&v)lTk8mBp z^8odv#^SMUgXK#USRo<%!xl1XY1#q|mONC)N$!@zHLXcIYA;3S4;@JtcAV4Qb}BDT zZde|y`_4ZV^fkaykP5;>V^qo@8}Z&_0Wl=vHmJ@z0E)|_QUWUg6FF`D#pFXvJeS*z z7FC8EA30f=D3UEb=pI-Qn-yF_yyex&7mD6RMKBWy6DuXK10;B{jojGa(QSmxpeI?h zyB^#91I2>vBzApAR&$M*Or8W_#$(Ho8E`wSeE^35 z4pWdUvNnvFB2EW5j{`jumX-LI&63c;G)4%;_0Rkll(R8wZp(WezGUb$@-WLYbi21qfvsPnEs zA)%E`bf&FOl5#Oe^AcDdN6Yo!BBDNB=5uA;xW$wEd&uXe>M=;ulF{ zO*z&HcPDTYA%0jcMJ{(N28&g_zZm*(Mybz z2Q@bS2c4bRYF#^TTJTNl_-`7uUNxD!Y;HryhwgFgwmhKPEpORv*(Yy*<=B=@$v=Jr B+)V%g delta 1709 zcmb7EO>7%Q6rS-CXYKVm{!Q(qaW+YtAGdDew9vF_Xack)NkfQ804H_gEw&U_P6A^G z5vzzGRpNkpsNS*SL=U6|RMi*g38bnr;X;f`!KmT_NIf7m%_00;m^Ztwnn+32J$yU! z?Y#HCdEd9rPey^%_!k>@+SxMJlQiJs2rM52yuTkU(nW(XlQzTh^fh6T?7L#)c z&c-?WBhD>2g>#NaoM(}CsL)P_3SUcqQuu|o!j-D~ii@XNCd;%;--C!H0pB@y`?@%n zwQ*P?#irtLX>ekl6n?gTBYB*hWHlHfF=Gr@?e9<*>{j}$N-mYnO+#F{UF+dX$~G}i z>N%aYV%&Z>;y6hIu;e&GyWpmyr8<1Kv1hfhr|`SOMt6Dd%I&Li`)9%H179r`8&;3M zaYufW)xk4P%_?fSAng3G-iN9D2(=ut*cy!ma2|x)&PLxTvZna{;%w>k8CF;53wzRer3C89bYsAPVgl2>mguMttavS{M>2P6>SQud+!hW!MBf$s`k8%*j zw93N*Fyif{qj1q1s_YMLc>mjWd$)Xv!~Hx>*^{1oSI?xA(~BvcO~0jQQ(5*h77?4R z!;#pgj3WdGO#1&)V{~02)-&{hMX8Fxcx~QqFjI658OlVY49cDZ1<$UJsULfjS4Amp zc7855qeYntHv$ox8#AMIu+q@BW1}4*EtuyE*Ud&pi}BLXq*1CK`a;3N`QRr~D1-^O zZ`9$2V;W{*FCYk=c&hU@LsqZW#a;wxn0k^F-(?Zp0UFBW0tKRR--84Apdw@R>mEKQ@g-*%YjVpWV@9u2uPi2eB9e-sdNK zTh)2ANyO~rEhIYqC$>0wj@Kx=D^7CJrr~o~dy9`Lx9^#cN~Cm_8(8Dh|m1TCaN3 diff --git a/tests/test_class_methods.py b/tests/test_class_methods.py index c9645da..2698500 100644 --- a/tests/test_class_methods.py +++ b/tests/test_class_methods.py @@ -4,7 +4,9 @@ from PyScriptTestRunner import PyScriptTestRunner runner = PyScriptTestRunner( - Path(__file__).resolve().parent.parent / "dist" / "test_bridge.js" + Path(__file__).resolve().parent.parent / "dist" / "test_bridge.js", + serializer = lambda d: {"likelyYear": d.likely_year, "likelyMonth": d.likely_month, "likelyDay": d.likely_day}, + deserializer = lambda d: FlexibleDate(likely_day=d.likelyDay, likely_month=d.likelyMonth, likely_year=d.likelyYear), ) runner.add_method(FlexibleDate.__bool__, "testBool") diff --git a/tests/test_combine_flexible_dates.py b/tests/test_combine_flexible_dates.py index 004bb93..64919ae 100644 --- a/tests/test_combine_flexible_dates.py +++ b/tests/test_combine_flexible_dates.py @@ -1,11 +1,15 @@ from pathlib import Path -from FlexibleDate.FlexibleDate import combine_flexible_dates +from FlexibleDate.FlexibleDate import ( + FlexibleDate, + combine_flexible_dates, +) import pytest from PyScriptTestRunner import PyScriptTestRunner -# Initialize the test runner (will handle environment setup automatically) runner = PyScriptTestRunner( - Path(__file__).resolve().parent.parent / "dist" / "test_bridge.js" + Path(__file__).resolve().parent.parent / "dist" / "test_bridge.js", + serializer = lambda d: {"likelyYear": d.likely_year, "likelyMonth": d.likely_month, "likelyDay": d.likely_day}, + deserializer = lambda d: FlexibleDate(likely_day=d.likelyDay, likely_month=d.likelyMonth, likely_year=d.likelyYear), ) runner.add_method(combine_flexible_dates, "combineFlexibleDates") diff --git a/tests/test_compare_dates.py b/tests/test_compare_dates.py index 7c20ea5..fd821b4 100644 --- a/tests/test_compare_dates.py +++ b/tests/test_compare_dates.py @@ -1,11 +1,17 @@ from pathlib import Path import pytest from PyScriptTestRunner import PyScriptTestRunner -from FlexibleDate.FlexibleDate import compare_two_dates +from FlexibleDate.FlexibleDate import ( + FlexibleDate, + compare_two_dates, +) + +from tests.test_validators import runner -# Initialize the test runner (will handle environment setup automatically) -test_runner = PyScriptTestRunner( - Path(__file__).resolve().parent.parent / "dist" / "test_bridge.js" +runner = PyScriptTestRunner( + Path(__file__).resolve().parent.parent / "dist" / "test_bridge.js", + serializer = lambda d: {"likelyYear": d.likely_year, "likelyMonth": d.likely_month, "likelyDay": d.likely_day}, + deserializer = lambda d: FlexibleDate(likely_day=d.likelyDay, likely_month=d.likelyMonth, likely_year=d.likelyYear), ) class TestIdenticalDates: @@ -41,14 +47,14 @@ class TestIdenticalDates: @pytest.mark.parametrize("test_case", test_cases, ids=lambda x: x['description']) def test_identical_full_dates(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = test_runner.run_dual_test( + py_result, ts_result = runner.run_dual_test( "compare_two_dates", "compareDates", test_data ) assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - test_runner.assert_strict_parity(py_result, ts_result, test_case['description']) + runner.assert_strict_parity(py_result, ts_result, test_case['description']) edge_cases = [ { @@ -81,7 +87,7 @@ def test_identical_full_dates(self, test_case): def test_edge_cases(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = test_runner.run_dual_test( + py_result, ts_result = runner.run_dual_test( "compare_two_dates", "compareDates", test_data @@ -89,7 +95,7 @@ def test_edge_cases(self, test_case): assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - test_runner.assert_strict_parity(py_result, ts_result, test_case['description']) + runner.assert_strict_parity(py_result, ts_result, test_case['description']) class TestSimilarDates: """Test comparison of similar dates.""" @@ -125,7 +131,7 @@ class TestSimilarDates: def test_one_day_different_cases(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = test_runner.run_dual_test( + py_result, ts_result = runner.run_dual_test( "compare_two_dates", "compareDates", test_data @@ -133,7 +139,7 @@ def test_one_day_different_cases(self, test_case): assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - test_runner.assert_strict_parity(py_result, ts_result, test_case['description']) + runner.assert_strict_parity(py_result, ts_result, test_case['description']) one_month_different_cases = [ { @@ -166,7 +172,7 @@ def test_one_day_different_cases(self, test_case): def test_one_month_different_cases(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = test_runner.run_dual_test( + py_result, ts_result = runner.run_dual_test( "compare_two_dates", "compareDates", test_data @@ -174,7 +180,7 @@ def test_one_month_different_cases(self, test_case): assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - test_runner.assert_strict_parity(py_result, ts_result, test_case['description']) + runner.assert_strict_parity(py_result, ts_result, test_case['description']) one_year_different_cases = [ { @@ -199,7 +205,7 @@ def test_one_month_different_cases(self, test_case): def test_one_year_different_cases(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = test_runner.run_dual_test( + py_result, ts_result = runner.run_dual_test( "compare_two_dates", "compareDates", test_data @@ -207,7 +213,7 @@ def test_one_year_different_cases(self, test_case): assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - test_runner.assert_strict_parity(py_result, ts_result, test_case['description']) + runner.assert_strict_parity(py_result, ts_result, test_case['description']) year_only_five_years_different_cases = [ { @@ -240,7 +246,7 @@ def test_one_year_different_cases(self, test_case): def test_year_only_five_years_different_cases(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = test_runner.run_dual_test( + py_result, ts_result = runner.run_dual_test( "compare_two_dates", "compareDates", test_data @@ -248,7 +254,7 @@ def test_year_only_five_years_different_cases(self, test_case): assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - test_runner.assert_strict_parity(py_result, ts_result, test_case['description']) + runner.assert_strict_parity(py_result, ts_result, test_case['description']) partial_date_comparisons_cases = [ { @@ -289,7 +295,7 @@ def test_year_only_five_years_different_cases(self, test_case): def test_partial_date_comparisons_cases(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = test_runner.run_dual_test( + py_result, ts_result = runner.run_dual_test( "compare_two_dates", "compareDates", test_data @@ -297,7 +303,7 @@ def test_partial_date_comparisons_cases(self, test_case): assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - test_runner.assert_strict_parity(py_result, ts_result, test_case['description']) + runner.assert_strict_parity(py_result, ts_result, test_case['description']) scoring_boundaries_cases = [ { @@ -354,7 +360,7 @@ def test_partial_date_comparisons_cases(self, test_case): def test_scoring_boundaries_cases(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = test_runner.run_dual_test( + py_result, ts_result = runner.run_dual_test( "compare_two_dates", "compareDates", test_data @@ -362,7 +368,7 @@ def test_scoring_boundaries_cases(self, test_case): assert py_result == test_case["expected"], f"Python failed for {test_case['description']}: got {py_result}, expected {test_case['expected']}" assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}: got {ts_result}, expected {test_case['expected']}" - test_runner.assert_strict_parity(py_result, ts_result, test_case['description']) + runner.assert_strict_parity(py_result, ts_result, test_case['description']) class TestBadDates: @@ -407,7 +413,7 @@ class TestBadDates: def test_very_different_dates_cases(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = test_runner.run_dual_test( + py_result, ts_result = runner.run_dual_test( "compare_two_dates", "compareDates", test_data @@ -415,4 +421,4 @@ def test_very_different_dates_cases(self, test_case): assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - test_runner.assert_strict_parity(py_result, ts_result, test_case['description']) \ No newline at end of file + runner.assert_strict_parity(py_result, ts_result, test_case['description']) \ No newline at end of file diff --git a/tests/test_create_flexible_date.py b/tests/test_create_flexible_date.py index 12d45ef..cd50f2f 100644 --- a/tests/test_create_flexible_date.py +++ b/tests/test_create_flexible_date.py @@ -1,10 +1,16 @@ from pathlib import Path import pytest from PyScriptTestRunner import PyScriptTestRunner -from FlexibleDate.FlexibleDate import create_flexible_date, create_flexible_date_from_formal_date +from FlexibleDate.FlexibleDate import ( + FlexibleDate, + create_flexible_date, + create_flexible_date_from_formal_date, +) runner = PyScriptTestRunner( - Path(__file__).resolve().parent.parent / "dist" / "test_bridge.js" + Path(__file__).resolve().parent.parent / "dist" / "test_bridge.js", + serializer = lambda d: {"likelyYear": d.likely_year, "likelyMonth": d.likely_month, "likelyDay": d.likely_day}, + deserializer = lambda d: FlexibleDate(likely_day=d.likelyDay, likely_month=d.likelyMonth, likely_year=d.likelyYear), ) runner.add_method(create_flexible_date, "createFlexibleDate") diff --git a/tests/test_helper_edge_cases.py b/tests/test_helper_edge_cases.py index 696c0c1..41d770b 100644 --- a/tests/test_helper_edge_cases.py +++ b/tests/test_helper_edge_cases.py @@ -1,13 +1,17 @@ from pathlib import Path -from FlexibleDate.FlexibleDate import create_flexible_date +from FlexibleDate.FlexibleDate import ( + FlexibleDate, + create_flexible_date, +) import pytest from PyScriptTestRunner import PyScriptTestRunner -# Initialize the test runner (will handle environment setup automatically) -test_runner = PyScriptTestRunner( - Path(__file__).resolve().parent.parent / "dist" / "test_bridge.js" +runner = PyScriptTestRunner( + Path(__file__).resolve().parent.parent / "dist" / "test_bridge.js", + serializer = lambda d: {"likelyYear": d.likely_year, "likelyMonth": d.likely_month, "likelyDay": d.likely_day}, + deserializer = lambda d: FlexibleDate(likely_day=d.likelyDay, likely_month=d.likelyMonth, likely_year=d.likelyYear), ) -test_runner.add_method(create_flexible_date, "createFlexibleDate") +runner.add_method(create_flexible_date, "createFlexibleDate") class TestEdgeCases: @@ -68,7 +72,7 @@ class TestAncientDates: def test_ancient_dates(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = test_runner.run_dual_test( + py_result, ts_result = runner.run_dual_test( "create_flexible_date", "createFlexibleDate", test_data @@ -76,7 +80,7 @@ def test_ancient_dates(self, test_case): assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - test_runner.assert_strict_parity(py_result, ts_result, test_case['description']) + runner.assert_strict_parity(py_result, ts_result, test_case['description']) class TestTextCleaning: """Test complex text cleaning scenarios.""" @@ -148,7 +152,7 @@ class TestTextCleaning: def test_text_cleaning(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = test_runner.run_dual_test( + py_result, ts_result = runner.run_dual_test( "create_flexible_date", "createFlexibleDate", test_data @@ -156,7 +160,7 @@ def test_text_cleaning(self, test_case): assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - test_runner.assert_strict_parity(py_result, ts_result, test_case['description']) + runner.assert_strict_parity(py_result, ts_result, test_case['description']) class TestDecadeAndRanges: """Test parsing of decades and special formats.""" @@ -188,7 +192,7 @@ class TestDecadeAndRanges: def test_decades(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = test_runner.run_dual_test( + py_result, ts_result = runner.run_dual_test( "create_flexible_date", "createFlexibleDate", test_data @@ -196,7 +200,7 @@ def test_decades(self, test_case): assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - test_runner.assert_strict_parity(py_result, ts_result, test_case['description']) + runner.assert_strict_parity(py_result, ts_result, test_case['description']) class TestAMPMHandling: """Test handling of AM/PM markers in dates.""" @@ -223,7 +227,7 @@ class TestAMPMHandling: def test_ampm_handling(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = test_runner.run_dual_test( + py_result, ts_result = runner.run_dual_test( "create_flexible_date", "createFlexibleDate", test_data @@ -231,7 +235,7 @@ def test_ampm_handling(self, test_case): assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - test_runner.assert_strict_parity(py_result, ts_result, test_case['description']) + runner.assert_strict_parity(py_result, ts_result, test_case['description']) class TestSpecialCharacters: """Test handling of special characters and quotes.""" @@ -258,7 +262,7 @@ class TestSpecialCharacters: def test_special_characters(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = test_runner.run_dual_test( + py_result, ts_result = runner.run_dual_test( "create_flexible_date", "createFlexibleDate", test_data @@ -266,7 +270,7 @@ def test_special_characters(self, test_case): assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - test_runner.assert_strict_parity(py_result, ts_result, test_case['description']) + runner.assert_strict_parity(py_result, ts_result, test_case['description']) class TestParserEdgeCases: """Test edge cases in _parse_with_date_util that trigger fallback to gleaning.""" @@ -288,7 +292,7 @@ class TestParserEdgeCases: def test_parser_edge_cases(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = test_runner.run_dual_test( + py_result, ts_result = runner.run_dual_test( "create_flexible_date", "createFlexibleDate", test_data @@ -296,7 +300,7 @@ def test_parser_edge_cases(self, test_case): assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - test_runner.assert_strict_parity(py_result, ts_result, test_case['description']) + runner.assert_strict_parity(py_result, ts_result, test_case['description']) class TestComplexDateGleaning: """Test complex date gleaning scenarios with multiple possibilities.""" @@ -313,7 +317,7 @@ class TestComplexDateGleaning: def test_complex_gleaning(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = test_runner.run_dual_test( + py_result, ts_result = runner.run_dual_test( "create_flexible_date", "createFlexibleDate", test_data @@ -321,5 +325,5 @@ def test_complex_gleaning(self, test_case): assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - test_runner.assert_strict_parity(py_result, ts_result, test_case['description']) + runner.assert_strict_parity(py_result, ts_result, test_case['description']) diff --git a/tests/test_validators.py b/tests/test_validators.py index 49d7c30..c436839 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -3,9 +3,10 @@ from PyScriptTestRunner import PyScriptTestRunner from FlexibleDate.FlexibleDate import FlexibleDate -# Initialize the test runner (will handle environment setup automatically) -test_runner = PyScriptTestRunner( - Path(__file__).resolve().parent.parent / "dist" / "test_bridge.js" +runner = PyScriptTestRunner( + Path(__file__).resolve().parent.parent / "dist" / "test_bridge.js", + serializer = lambda d: {"likelyYear": d.likely_year, "likelyMonth": d.likely_month, "likelyDay": d.likely_day}, + deserializer = lambda d: FlexibleDate(likely_day=d.likelyDay, likely_month=d.likelyMonth, likely_year=d.likelyYear), ) @@ -49,7 +50,7 @@ class TestYearValidator: def test_valid_years(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = test_runner.run_dual_test( + py_result, ts_result = runner.run_dual_test( "test_validator", "testValidator", test_data @@ -57,7 +58,7 @@ def test_valid_years(self, test_case): assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - test_runner.assert_strict_parity(py_result, ts_result, test_case['description']) + runner.assert_strict_parity(py_result, ts_result, test_case['description']) invalid_year_cases = [ { @@ -82,7 +83,7 @@ def test_valid_years(self, test_case): def test_invalid_years(self, test_case): test_data = {"input": test_case["input"], "expected": "ValueError", "mocks": {}} - py_result, ts_result = test_runner.run_dual_test( + py_result, ts_result = runner.run_dual_test( "test_validator", "testValidator", test_data @@ -90,7 +91,7 @@ def test_invalid_years(self, test_case): assert py_result == "ValueError", f"Python should raise ValueError for {test_case['description']}" assert ts_result == "ValueError", f"TypeScript should raise ValueError for {test_case['description']}" - test_runner.assert_strict_parity(py_result, ts_result, test_case['description']) + runner.assert_strict_parity(py_result, ts_result, test_case['description']) class TestMonthValidator: @@ -123,7 +124,7 @@ class TestMonthValidator: def test_valid_months(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = test_runner.run_dual_test( + py_result, ts_result = runner.run_dual_test( "test_validator", "testValidator", test_data @@ -131,7 +132,7 @@ def test_valid_months(self, test_case): assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - test_runner.assert_strict_parity(py_result, ts_result, test_case['description']) + runner.assert_strict_parity(py_result, ts_result, test_case['description']) invalid_month_cases = [ { @@ -156,7 +157,7 @@ def test_valid_months(self, test_case): def test_invalid_months(self, test_case): test_data = {"input": test_case["input"], "expected": "ValueError", "mocks": {}} - py_result, ts_result = test_runner.run_dual_test( + py_result, ts_result = runner.run_dual_test( "test_validator", "testValidator", test_data @@ -164,7 +165,7 @@ def test_invalid_months(self, test_case): assert py_result == "ValueError", f"Python should raise ValueError for {test_case['description']}" assert ts_result == "ValueError", f"TypeScript should raise ValueError for {test_case['description']}" - test_runner.assert_strict_parity(py_result, ts_result, test_case['description']) + runner.assert_strict_parity(py_result, ts_result, test_case['description']) class TestDayValidator: @@ -197,7 +198,7 @@ class TestDayValidator: def test_valid_days(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = test_runner.run_dual_test( + py_result, ts_result = runner.run_dual_test( "test_validator", "testValidator", test_data @@ -205,7 +206,7 @@ def test_valid_days(self, test_case): assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - test_runner.assert_strict_parity(py_result, ts_result, test_case['description']) + runner.assert_strict_parity(py_result, ts_result, test_case['description']) invalid_day_cases = [ { @@ -230,7 +231,7 @@ def test_valid_days(self, test_case): def test_invalid_days(self, test_case): test_data = {"input": test_case["input"], "expected": "ValueError", "mocks": {}} - py_result, ts_result = test_runner.run_dual_test( + py_result, ts_result = runner.run_dual_test( "test_validator", "testValidator", test_data @@ -238,5 +239,5 @@ def test_invalid_days(self, test_case): assert py_result == "ValueError", f"Python should raise ValueError for {test_case['description']}" assert ts_result == "ValueError", f"TypeScript should raise ValueError for {test_case['description']}" - test_runner.assert_strict_parity(py_result, ts_result, test_case['description']) + runner.assert_strict_parity(py_result, ts_result, test_case['description']) From 436dcb5df3a412a2d363b2ba224300c7f257115a Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Mon, 30 Mar 2026 16:54:25 -0600 Subject: [PATCH 07/41] first round of tests pass! --- ...class_methods.cpython-312-pytest-8.3.5.pyc | Bin 12640 -> 13130 bytes tests/test_class_methods.py | 11 ++++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/__pycache__/test_class_methods.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_class_methods.cpython-312-pytest-8.3.5.pyc index 68fa8a0cebf4da882cdb21a3834ca9937f07ce62..0f90e866acf89fbedd3d456c75feef1ede3a7607 100644 GIT binary patch delta 1436 zcmbW1O-vI(6vy|qKr4k}KPcM@<*TJ7Ewq4u5)czjIT$_*fq==TltmkBOSg-WMng>0 zGi1DZk)xWBkcnQvgC`Tb&`1JlVq%OO{5WvNgU;KACauVUP4<_!|9Sh~?0;tV%k1{N z{$qK0DaWqTKiiROZL4}74mVs8AX8ukH`&mdg!0!aOyD@+RNMeJr+>`NRUB*6m5O$r zX@;C;Q8XjdOgXJe(acP%%xU}*G+XF5AgZY&b08|-kIY8J>mgZey%&luio^4%$~;w$ z2W2S|yOqjMj>M7+VpvX@@{+~GVknxV&x$5hP(yx|bU;6?(|l5QTwtFfRWrO)OSw2x zd^aAxlPqW`2($5cR1kY-hV56T6dQ}njBkxO^ z%A`gdQ%X5Wz1ainWYAnymn%a!l9&?l!7=ij+j8i5CtAg zqJ|ivH*MbmCTXC0Ip2hx8cV|zrr9tprS-J?+x;#6PHBccr;3$FX_{#5YaozU_8y3k z1AFwphMCFpTHbVo1ziYbJ#y{|1@2{6SP~bctdV0ZIF>JPGw6lwmsLXyF(iQT;*8#m`*Vhg~qVYSRhj{&7d!|GX#DpMZkE*g)6_1sI z4ifY8rK_wKusivctoXw>cAEQlxvTqJ$sT7&bA}b~p0O!yY}#@EBfHW@*9qCqw9&ca M>fPn~h@-9dFVcxoz5oCK delta 1323 zcmbu8PfU_w9LL`WL_o$XASx&dra_=6e`FOIO*@ct{~dw)Op@?-Vl$8r9R zR$Icc*S9~b$yc6-ya_#j=18I;85iazw0F4){;3S^Ic~%#xl-nIc~>quJ#&V7vzLsk-1`ca4yHIQQRlake5 zBK6M2J#x~zs?7nky?oyExwWp*&A@28>xF!%AS{iv35nUcSW4JWQoSfZ7n=K!t}qw! zx24iuuw!?2$>AnAfRc82jK}~-rzxu%pG&61XLGo1cG(}uyGG1!;X>zr?g Nbz}? Date: Wed, 1 Apr 2026 12:20:49 -0600 Subject: [PATCH 08/41] added executor compatibility for python --- PyScriptTestRunner.py | 22 +++++++++++------ .../PyScriptTestRunner.cpython-312.pyc | Bin 16196 -> 16787 bytes test_bridge.ts | 20 +++------------ ...class_methods.cpython-312-pytest-8.3.5.pyc | Bin 13130 -> 13422 bytes tests/test_class_methods.py | 23 +++++++++--------- 5 files changed, 30 insertions(+), 35 deletions(-) diff --git a/PyScriptTestRunner.py b/PyScriptTestRunner.py index 404d2d4..a73e85d 100644 --- a/PyScriptTestRunner.py +++ b/PyScriptTestRunner.py @@ -9,8 +9,9 @@ @dataclass(frozen=True) class RegisteredMethod: - py_fn: Callable[[Any], Any] + py_fn: Callable[..., Any] ts_name: str + executor: Optional[Callable[..., Any]] = None ts_pack_input: bool = False @@ -34,8 +35,8 @@ def __init__( package_root if package_root is not None else Path(__file__).resolve().parent ) self.ts_bridge_path = ts_bridge_path - self.serializer = serializer - self.deserializer = deserializer + self.serializer = serializer if serializer is not None else lambda d: d + self.deserializer = deserializer if deserializer is not None else lambda d: d self._by_py: Dict[str, RegisteredMethod] = {} self._by_ts: Dict[str, RegisteredMethod] = {} @@ -46,15 +47,16 @@ def __init__( def add_method( self, - py_callable: Callable[[Any], Any], + py_callable: Callable[..., Any], ts_method_name: str, + executor: Optional[Callable[..., Any]] = None, *, ts_pack_input: bool = False, ) -> None: assert callable(py_callable) if not ts_method_name or not str(ts_method_name).strip(): raise ValueError("ts_method_name must be non-empty") - py_key = getattr(py_callable, "__name__", None) + py_key = getattr(py_callable, "__qualname__", None) or getattr(py_callable, "__name__", None) if not py_key or py_key == "": raise ValueError("py_callable must be a named function (not lambda or )") @@ -64,7 +66,10 @@ def add_method( raise ValueError(f"Duplicate TypeScript registration: {ts_method_name!r}") rec = RegisteredMethod( - py_fn=py_callable, ts_name=ts_method_name, ts_pack_input=ts_pack_input + py_fn=py_callable, + ts_name=ts_method_name, + executor=executor, + ts_pack_input=ts_pack_input, ) self._by_py[py_key] = rec self._by_ts[ts_method_name] = rec @@ -187,7 +192,10 @@ def _call_python_function_with_mocks( mock_context.__enter__() try: - result = rec.py_fn(input_data) + if rec.executor is not None: + result = rec.executor(input_data) + else: + result = rec.py_fn(input_data) if self.serializer is not None: return self.serializer(result) return result diff --git a/__pycache__/PyScriptTestRunner.cpython-312.pyc b/__pycache__/PyScriptTestRunner.cpython-312.pyc index 0da5d3c95336eb5d81b16e74b0ac6e0c2e70d7b8..dc7953d8da6a2d7ca8e365d29075e1a3344f314f 100644 GIT binary patch delta 3487 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 I2~lL>zedohwEzGB diff --git a/test_bridge.ts b/test_bridge.ts index 164fd14..27ed0e1 100644 --- a/test_bridge.ts +++ b/test_bridge.ts @@ -49,37 +49,25 @@ bridge.addMethod("combineFlexibleDates", (args) => { return { success: true, result: serializeFlexibleDate(combined) }; }); -bridge.addMethod("toString", (args) => { +bridge.addMethod("FlexibleDate.toString", (args) => { const [fdData] = args; const fdForString = deserializeFlexibleDate(fdData); return { success: true, result: fdForString.toString() }; }); -bridge.addMethod("valueOf", (args) => { +bridge.addMethod("FlexibleDate.valueOf", (args) => { const [fdDataValue] = args; const fdForValue = deserializeFlexibleDate(fdDataValue); return { success: true, result: fdForValue.valueOf() }; }); -bridge.addMethod("testBool", (args) => { - const [fdDataBool] = args; - const fdForBool = deserializeFlexibleDate(fdDataBool); - return { success: true, result: fdForBool.valueOf() }; -}); - -bridge.addMethod("testStr", (args) => { - const [fdDataStr] = args; - const fdForStr = deserializeFlexibleDate(fdDataStr); - return { success: true, result: fdForStr.toString() }; -}); - -bridge.addMethod("testRepr", (args) => { +bridge.addMethod("FlexibleDate.inspect", (args) => { const [fdDataRepr] = args; const fdForRepr = deserializeFlexibleDate(fdDataRepr); return { success: true, result: fdForRepr.inspect() }; }); -bridge.addMethod("test_equals", (args) => { +bridge.addMethod("FlexibleDate.equals", (args) => { const [fdDataEquals1, fdDataEquals2] = args; const fdForEquals1 = deserializeFlexibleDate(fdDataEquals1); const fdForEquals2 = deserializeFlexibleDate(fdDataEquals2); diff --git a/tests/__pycache__/test_class_methods.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_class_methods.cpython-312-pytest-8.3.5.pyc index 0f90e866acf89fbedd3d456c75feef1ede3a7607..8e7627423f80b1824d73f58c4cb198872c548228 100644 GIT binary patch delta 2379 zcmbtVO>7fK6rNeH?KrkW;y6F1A%6rXPU8GH7>NRjNCHWk5-^U0!{2HXZ?MT?JL3&X zXlSB}9;zM+nggneDshYg6=<(L^hjH^vWcajy_8E;(L)7lD{|e zy*F>(e)D$x!}K39=Z{WjF=Efp;!p9vt~os%&s}S|!`~`e<*q#p+HBF{E9jPe)kKfs z2D%r!ZH}mOlL^{NqT4oS4Ri&&mrA2$c?Pl~gb{~^P}Fq=Max&YJa@7}=iH2|*yJj8 zu99(8n_QL7RWr`B$yG04-}Xn4Xv~K-{+Ou5RtQytKwd{Ecj|jBcF16T0)!-LskF;mo+0JwBO9&1T5Q z+0@dX2-P0sHlPk4rZ@Q}de>B0)61r>zJ&5ZMug6r=@XN?dG*En88(bo@EEI)pG+oY z%|hl935gIl{gB@=Y-h(J%61tQ88(av?9FO3r4>Rz`g~pc9Bi02>N=5&oL!I1WFs>f z`NCQxksD9ybv<`)sU@CBr=%(6txEA4B^dT)DLj!}iQI?{!2k*VmRBjb&s9vfb!1gPdv6`^z*&-~=DRsrk59M3^XHOnQ_7JDkvo^-we$ zie{wg%(+-rim!#v=Mu`_2qkep_atu~BQ(Gba?Bj!4g)#B4MG1T@i6xkq2tWx@1R=E zyK%{iiNNs__~406Jz#0TxQzzc{s14k$i_KC@mf z?Qnco*~#KGn{WM{{)T&2axX%Eb1rQi$C9t8c;FOq_ z(nJK$B)}BF0ziRo5^w-V)L*tg!dOzjuUWzag~G9bOZK)vSD?$^74&xp$uz8E{fEXb z6&XBOxIa}ZJ1lc!lv}pIeGxtYN6)GqeD7f|V5d>+L>DLIwy@P&7 zxge%}o;JSWfulih&(cEO=zpD6{(^q|47M%M=~LhUtZILk8@piW^Jqnrb8%T=7c?2A zvF;u`LT_~Y+sqKGCxw4#tw<22$X!ZfJymWfAE^nrVIg`3GMIQ#-{?~`@%DQ%Cn)kC?c9`9(Ec_X(VT7Y4J^TL$ DFcUBj delta 2086 zcmbVMUrbY17(d^=h5j$Jl-5#FszSLy6^np6ltqosEp{Lp8Mu*3FQc^rJuSL6HLygp zhxs5m4`!LmVzP%3jMLl)w`68}8@dOJa{-$@EKByF4~v_78GG2ycM41z;u24CfA_n; z^WE<|-|su;{5E`V$ojq2Y9f4?{<#<3KV|huPhqP5rXgauK&~5S1@gMCaB7&pEeGT3d(l&I^*}hKD!{s=_6i;Mk@@rNbq-fZmc$7AS?82u z622y4IA1g(a8>RhiWHib^QL(8v>H#prG{zVT(FOfj--b3#=^&ea9WX|5tt6l!hPFaTTUICR}*t; z;(?bw!_~1wiObo?jVmOD0mR_7h$5tz4r^gg&CZtVFoV_dAu3R0YU4JwB3L*mI*Juj zGXnOP8riMV>YYYzCf9SABv((~?7rQ9tA93e|NPzax!CFX*x9+**&nOV(F*RMNO@CI zrO|Lano%kHQr@yJQ~A8`TT`PwDHR>8Uv5Hj_28U$bIq#vEpQk=JjZC1!({SZbJ|?Y)5McLN!7wLK{Lm!cK%;2nSic_aNNB zT6L}kOQrUyw+6aHIQ1q%7eY6Oyp5+D5P1i~Ni|7(aON1oafA_sB8^TUhY-@rs~-cL z&=xmL!2V*9xI@O=7;FwU2bx<0+goWb{)Zbqlrb#xv>)9{#3$`zU)NlN0rrk>H$>S- zzW5rdzOt#Qd=Ufr5k%t(VxBA#v(SF3j?sdyN4fR#aR^{D(p2ZMe3EKz}rBg*N7EbVEI} zx_d##a+?mjOR}Xy5Gl>d*^(h?M#rP@5z({e%@d$!IgbJcHnz@RZOlT%goof`@`*IF zbj?J}8T7WWN%O^(NV-*#tgOp30oLat+5RU|h+V3xoe;T}uPY#FqLhV}jyO+QI2LBH zJqwq_OLve*69=(od5H7QqT6;!ZNSF7jXKYQ#UmPfY^}!H3;AEr+V^q6MXk*uhhWeS zZFhnLO~sQpoEneDQ~WZdd)b*_3v{q+!GIaNQaFDl>``zd>}G!jTX%{ZNgO$`PejDk zSr!c)XGSCAadj`9;vukU$zKQ?YT38vk)1xOIQOSsXObpMpIQj$=ZSre*e^HDJN$DF re@^LqNM2narT@`&bB?-PJ<{y=7H3uQbnTp@Hs|knNcOPcj*fo;nquqC diff --git a/tests/test_class_methods.py b/tests/test_class_methods.py index 6102c12..1a1205c 100644 --- a/tests/test_class_methods.py +++ b/tests/test_class_methods.py @@ -5,14 +5,13 @@ runner = PyScriptTestRunner( Path(__file__).resolve().parent.parent / "dist" / "test_bridge.js", - serializer = lambda d: {"likelyYear": d.likely_year, "likelyMonth": d.likely_month, "likelyDay": d.likely_day}, - deserializer = lambda d: FlexibleDate(likely_day=d.likelyDay, likely_month=d.likelyMonth, likely_year=d.likelyYear), + deserializer = lambda d: FlexibleDate(likely_day=d["likelyDay"], likely_month=d["likelyMonth"], likely_year=d["likelyYear"]), ) -runner.add_method(FlexibleDate.__bool__, "FlexibleDate.valueOf") -runner.add_method(FlexibleDate.__str__, "FlexibleDate.toString") -runner.add_method(FlexibleDate.__repr__, "FlexibleDate.inspect") -runner.add_method(FlexibleDate.__eq__, "FlexibleDate.equals") +runner.add_method(FlexibleDate.__bool__, "FlexibleDate.valueOf", executor=lambda d: bool(runner.deserializer(d))) +runner.add_method(FlexibleDate.__str__, "FlexibleDate.toString", executor=lambda d: str(runner.deserializer(d))) +runner.add_method(FlexibleDate.__repr__, "FlexibleDate.inspect", executor=lambda d: repr(runner.deserializer(d))) +runner.add_method(FlexibleDate.__eq__, "FlexibleDate.equals", executor=lambda d: runner.deserializer(d[0]) == runner.deserializer(d[1])) class TestBoolMethod: @@ -176,8 +175,8 @@ def test_str_method(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} py_result, ts_result = runner.run_dual_test( - "test_str", - "testStr", + "FlexibleDate.__str__", + "FlexibleDate.toString", test_data ) @@ -252,8 +251,8 @@ def test_repr_method(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} py_result, ts_result = runner.run_dual_test( - "test_repr", - "testRepr", + "FlexibleDate.__repr__", + "FlexibleDate.inspect", test_data ) @@ -312,8 +311,8 @@ def test_equals_method(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} py_result, ts_result = runner.run_dual_test( - "test_equals", - "test_equals", + "FlexibleDate.__eq__", + "FlexibleDate.equals", test_data ) From 77a31216ea971394d19840ff6fc74de1b7ed3fc0 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Wed, 1 Apr 2026 12:28:09 -0600 Subject: [PATCH 09/41] added executor --- test_bridge.ts | 12 +++++++----- ...lexible_dates.cpython-312-pytest-8.3.5.pyc | Bin 23154 -> 23428 bytes tests/test_combine_flexible_dates.py | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/test_bridge.ts b/test_bridge.ts index 27ed0e1..8c5acc0 100644 --- a/test_bridge.ts +++ b/test_bridge.ts @@ -42,11 +42,13 @@ bridge.addMethod("compareDates", (args) => { }); bridge.addMethod("combineFlexibleDates", (args) => { - const [datesData] = args; - const dates = datesData.map((d: any) => deserializeFlexibleDate(d)); - const fdTemp = new FlexibleDate(null, null, null); - const combined = fdTemp.combineFlexibleDates(dates); - return { success: true, result: serializeFlexibleDate(combined) }; + const dates = args.map((d: any) => deserializeFlexibleDate(d)); + const fd_temp = new FlexibleDate(null, null, null); + const combined = fd_temp.combineFlexibleDates(dates); + return { + success: true, + result: serializeFlexibleDate(combined) + }; }); bridge.addMethod("FlexibleDate.toString", (args) => { diff --git a/tests/__pycache__/test_combine_flexible_dates.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_combine_flexible_dates.cpython-312-pytest-8.3.5.pyc index 4cd18ab9cb8aa7976ee0cf2c3325ebd9ac35d1ea..fb7943e0ff48148e3b51593144fe69e58972c60b 100644 GIT binary patch delta 2378 zcmai#Z%ori7{~99(m>1Kg0w~c6i}cdvmmIzV2TKd{QoDSAgla-Vohn;wM>dyB_ks- zW(IGGFWi!C*?fapTz zK6lUEJ-I>Wzou!+si_76zdJ{NbX9v7(=7CZ#j=NI72`a)ma>$9O@vUwNE=a7=ZTWG z#KPJklho2d%ZO-MQp*I*6w$J!mIYdNMB5;>4WQ*jv>d6KL9;B?u6@KTMJtH8k&YIr zZ3HbZqFDpfp0C}Ycd1Jc@nPJ5IKW&cej1LKkkdq=R;WP9R?7q|uj$q}`-wfVi>H9b zjc9OX>|vr!w`*@C7#NZSe;KF(&Z~j8kc)JjO^|UmY9{0i^{=i6Atw^Wz2vO3 zbZK>_MRviegMMcsNd;A8`LpNH^*J6Sus@A3adh?h3A$Ke7R_OccRv|hT8Jqh=I zO2`oHXU|uwK3ux z&;U2WAa6n;rJyo8G#lF=L% z-9`0b@DXvbsDX}(<)UXX@w0`YWJhfD0%0p_KJw2C*Vy$V zIC4}Z)!LHFz@4=!&Vb0QHOFDORO^Vxa;0wP;Cj1Z@Jzeit4S1v^==F-jvCj>>Q7T0 z`XZ>y(Y|f)#7nE)jXR8zdzEUgae}JiQKKUc&3IE)JetYo`hoRmlKn09FQYz zwedi{OVJD7zD}Agw{MJ2vw8ex)WMiTZB9$?I7#W8w$Qo2DrJVLpg)qHJm1HlI-99_ z6r1;$EucEN4K7T$Hi|_FSo zXcnIj)~ugYyN4)((r%%58yx8?uN_ z6Jx^8l6{f*#Y~eeMstlbCN5iMqRtRmqCu2iS+pah*=#Zr!bM4>4qOlgYc_>8euYo*Z6q;J8FYep-6 zV!RJ3aiqUmvxi>sRjbg~Zo+m`Jyc86ruVZnwe-qG(t#`WX6&|fKo7oRS(lk)md+Gk z-#KNm{DHC_T7gn_XvBvp?j<=y(ud!dmwOIV=p*STIl?5FKNXyaiIK$o=+R(292cLU zH)-bm#Qf5JYpVkMc(t)ude!ze6mBz51M)})&}VN^@naao6?-cLaLxW_h9l5cx-%U? zX;t?&KY2U-EL9ac()=8gNj*p`C|$045g^F#ZKV1fY`?cNHT%JeEj1CA>Kio&VGw_+ ziDpQ6wYEV%VgH?zg$7G{vXe5&MuP?ZPxnMcdzt%SOIl<_pkz*@V2o6deiGt4bap%{L$MaYr=$2 zKIF<|v17LRa8R_dw#eFn6%Y7!!*LAytg0=)FZle-Y0~!yb6VZsn89AVDH|{Ln>Ew4 zKkuU%n%()wo9}Erf>R>1|Iu%Yl6$@=Pd*1YT`{17?FJ@j3{6zT{w{QD*ZrfBWG&33Y xh^uTxU{CyOJR9h-eq)^bBS)!LEUA9g3XroQ=x+%6bwky9^}#QN`%yb|{9oxe!6N_w diff --git a/tests/test_combine_flexible_dates.py b/tests/test_combine_flexible_dates.py index 64919ae..f655dee 100644 --- a/tests/test_combine_flexible_dates.py +++ b/tests/test_combine_flexible_dates.py @@ -9,10 +9,10 @@ runner = PyScriptTestRunner( Path(__file__).resolve().parent.parent / "dist" / "test_bridge.js", serializer = lambda d: {"likelyYear": d.likely_year, "likelyMonth": d.likely_month, "likelyDay": d.likely_day}, - deserializer = lambda d: FlexibleDate(likely_day=d.likelyDay, likely_month=d.likelyMonth, likely_year=d.likelyYear), + deserializer = lambda d: FlexibleDate(likely_day=d["likelyDay"], likely_month=d["likelyMonth"], likely_year=d["likelyYear"]), ) -runner.add_method(combine_flexible_dates, "combineFlexibleDates") +runner.add_method(combine_flexible_dates, "combineFlexibleDates", executor=lambda dl: combine_flexible_dates([runner.deserializer(d) for d in dl])) class TestBasicCombining: """Test fundamental combining operations.""" From 39c8fc473478ce603de8931b88f225a008aaf9c3 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Wed, 1 Apr 2026 12:31:13 -0600 Subject: [PATCH 10/41] fixed implimentations --- ...compare_dates.cpython-312-pytest-8.3.5.pyc | Bin 19641 -> 19789 bytes tests/test_compare_dates.py | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/__pycache__/test_compare_dates.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_compare_dates.cpython-312-pytest-8.3.5.pyc index 1f75c414840ab7cd2b64f41139acf9768a5e88c0..c123d8135b2d797e2ecef788b446f0cc37bf4189 100644 GIT binary patch delta 1852 zcma)6ZAe>J7(VCT7;A3QX^f)A`Y}JFD=O=p+Rd&srH!>#WBkfCHRfI#tugi_c6F{Z zqx~^xVYu^SopdnRkKq*7abaKG5BI0A58;{$9f3iAZTL^It}q7M``#p0Wy}TgoR9Z? zpZ7h_dC&dkL%K3gEk9W-CIY`cd1ds)IZHl0Hdpn1(^AHwfV+fv4+}&&JXW4q?MB5c zn`G$*Su!t*iG51AnO5fslQ zemQ|u)X+3~QkDiec#kmZ$%csq{^4farz=k*iU)1)>g>Cara3hNx29aGw^qc zv+c=~FDA9vZ=X#nlCq~%1wF(LhWE6Nkq(iexGWtZ2O$$6jHVK1#WkK#2i?#Ct0-AJ zJFw;*e&`LZdV|_T2oAYA!5(>^8I&N1Vn5!wZ@xLr{_G%<`Ak?74ZqcwDsSi!hA=uQJ+Al>~ z&;NE50_UgJCfcVyvbvC|#x|L+lb{X>&Fwftu&SypK#jV^u9oJ0ZpbV|8;Zk@AQbUO z18+@)O^x;X??a=r3Vyw2;lK=kKx9g!!69up7#p0}pW#B?SSI>C&A^KJ z4|UpsF%w)w_i#hg-{W9PF5 z1pT$=@>?xOXc_;k9 z=)Y^udZ`3mrVz^c*UlE2$G4ol|F0k4J6&M^2#QXr;px$O{V|XHul;!2<(AMn)hXMq zw*A$ zjVrBe9^xS@3E$z*x3?YL6o~Dl@HkC02~p#QiBN0Q_>g3+k}Sph1y;wYhib=3Wd!wE1TF2qk~NAR!7?r1}^2&dj~D;7^fYx z3)99BW8#CsGqbSpV6qn*X6AAem+1@oU^f48X=3J@`0B!gPnDSXU_9S#ix$mj(qGRx z-~aD?r$1arcbAamJB!7{&`mn0Y3_kad%teC*=UJjv<#5eY5p!;D84%07(6$epC%#tZxJnW!RE%ck- zO_5_h*?a3i{=hx7X1#A!<#P|@*>!og;KU^=Wnu_e`P9*0E$>mfxW;6*Wb<38?;?1= zWKyqez2O0o!2@iE03ww2LCgEWh#D(4Dc{ zWA|p)uH3((CZ-=G=GGH)Keb%KJXP@Mil%gvjN@W5ep#lSkmPiArv%xbk#Pe8Y&4KQ z!xvAr(Pf^=qau2K=o*W;~qu`V?e zn@(c`JujMLGpP$AmIuU)oF;DW71XAj=U9Z?ByAo;cCuzZfxL>#@+v|+IcHsTc7V@L zfQRBg*>J-k>IU%2xa)I0@{*%|7xiSbKVJvJ5)mvdiRH-8rRwH5|G*J_Uw*B3%5D$9)l(6xQk#6^q z-@5WUML?TG?RKDTH+k2aXxyxEiCc-UN8QO=)6;aMt9>6iqmE7%My6`=HE3OFCt`0q z3HBc1z@U=2@&dxyaeU;jOVf^_ZsxmVAT`%gdsqz13%qB*oN(B=#@UimN@1oK}Zp zg^2XOv#bk*I8d$*STqpWUebGzC18$x9r%^g<{h>ko+z)~@o7&|D_K)2{|jhJaBY7+ taU?Y(T};aTc!_RlBqse9$)5-`Jhb75_DjDTS(7ej_`^(ulO7h#{s#MNhN1uf diff --git a/tests/test_compare_dates.py b/tests/test_compare_dates.py index fd821b4..ecd90f9 100644 --- a/tests/test_compare_dates.py +++ b/tests/test_compare_dates.py @@ -10,10 +10,11 @@ runner = PyScriptTestRunner( Path(__file__).resolve().parent.parent / "dist" / "test_bridge.js", - serializer = lambda d: {"likelyYear": d.likely_year, "likelyMonth": d.likely_month, "likelyDay": d.likely_day}, - deserializer = lambda d: FlexibleDate(likely_day=d.likelyDay, likely_month=d.likelyMonth, likely_year=d.likelyYear), + deserializer = lambda d: FlexibleDate(likely_day=d["likelyDay"], likely_month=d["likelyMonth"], likely_year=d["likelyYear"]), ) +runner.add_method(compare_two_dates, "compareDates", executor=lambda d: compare_two_dates(runner.deserializer(d[0]), runner.deserializer(d[1]))) + class TestIdenticalDates: """Test comparison of identical dates returns perfect score of 100.""" From bcf0cfd2bb5f1fce840d9f9da3868acbaf1b05b6 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Wed, 1 Apr 2026 12:53:37 -0600 Subject: [PATCH 11/41] everything passes --- PyScriptTestRunner.py | 35 +++++------------- .../PyScriptTestRunner.cpython-312.pyc | Bin 16787 -> 15645 bytes ...class_methods.cpython-312-pytest-8.3.5.pyc | Bin 13422 -> 13406 bytes ...lexible_dates.cpython-312-pytest-8.3.5.pyc | Bin 23428 -> 23416 bytes ...compare_dates.cpython-312-pytest-8.3.5.pyc | Bin 19789 -> 19773 bytes ...flexible_date.cpython-312-pytest-8.3.5.pyc | Bin 21872 -> 21856 bytes ...er_edge_cases.cpython-312-pytest-8.3.5.pyc | Bin 16822 -> 16810 bytes ...st_validators.cpython-312-pytest-8.3.5.pyc | Bin 12886 -> 13221 bytes tests/test_class_methods.py | 8 ++-- tests/test_combine_flexible_dates.py | 18 ++++----- tests/test_compare_dates.py | 18 ++++----- tests/test_create_flexible_date.py | 14 +++---- tests/test_helper_edge_cases.py | 14 +++---- tests/test_validators.py | 35 +++++++++++------- 14 files changed, 68 insertions(+), 74 deletions(-) diff --git a/PyScriptTestRunner.py b/PyScriptTestRunner.py index a73e85d..e0a93f0 100644 --- a/PyScriptTestRunner.py +++ b/PyScriptTestRunner.py @@ -76,14 +76,9 @@ def add_method( def run( self, - python_function_name: str, - ts_function_name: str, + python_function: str, + ts_function: str, test_data: Dict[str, Any], - ) -> tuple[Any, Any]: - return self.run_dual_test(python_function_name, ts_function_name, test_data) - - def run_dual_test( - self, python_function: str, ts_function: str, test_data: Dict[str, Any] ) -> tuple[Any, Any]: rec = self._by_py.get(python_function) if rec is None: @@ -101,22 +96,13 @@ def run_dual_test( py_result_holder: Dict[str, Any] = {} ts_result_holder: Dict[str, Any] = {} - def run_python() -> None: - py_result_holder["result"] = self._call_python_function_with_mocks( - python_function, input_data, mocks.get("python", {}), expected_error - ) - - def run_typescript() -> None: - ts_result_holder["result"] = self._call_typescript_function_with_mocks( - ts_function, input_data, mocks.get("typescript", {}), expected_error - ) + py_result_holder["result"] = self._call_python_function_with_mocks( + python_function, input_data, mocks.get("python", {}), expected_error + ) - t_py = threading.Thread(target=run_python) - t_ts = threading.Thread(target=run_typescript) - t_py.start() - t_ts.start() - t_py.join() - t_ts.join() + ts_result_holder["result"] = self._call_typescript_function_with_mocks( + ts_function, input_data, mocks.get("typescript", {}), expected_error + ) py_result = py_result_holder.get("result") ts_result = ts_result_holder.get("result") @@ -196,9 +182,8 @@ def _call_python_function_with_mocks( result = rec.executor(input_data) else: result = rec.py_fn(input_data) - if self.serializer is not None: - return self.serializer(result) - return result + try: return self.serializer(result) + except: return result finally: for mock_context in reversed(mock_contexts): mock_context.__exit__(None, None, None) diff --git a/__pycache__/PyScriptTestRunner.cpython-312.pyc b/__pycache__/PyScriptTestRunner.cpython-312.pyc index dc7953d8da6a2d7ca8e365d29075e1a3344f314f..b10dec3f67cd33ab07306b4984fa3a141fe30dc1 100644 GIT binary patch delta 1994 zcma)6TWl0n7@jk;GrK$6oo;FO+RF~RJ6opq0;~q4P};OCrD|IX7~9fpn$8rqZFbkQ z)0Q?px43KY2iD&ou}~A8UG$g-WzZ@YpOkJ%iun z`vA?0&#p7+rddH_u_gZ~Ku0HwC8ti0ZlNex^@xYo(YM9^3Y~tBKFeJK1^!W>GYU&; znqhSY4a!dBb=XWZ+={ZpVH0M|acahbU#m{xo!nzfS)j8zN7$9tICY6G#L~@|Oe>N_ z;kRQEU2(fOon9sTZBHay3uN7b1G@Otoz3tlCVR@(RGZH0xcz%6xd?qwl6>0WQd8QY zWWwN}njRn13|^Z`sp%mYPiZm-YjJvmIjCxIE0&-@R1rN0iL?*#R|D%hmvE83Jr&re#~NB;~3U8C;BnuLRW== zr5bmAmdX{Ozn$m$?@013spE#!al5EI)3@OK%=?k|^Q~8VZpzJH)h{!2nE%P*$#`y9 z0yzs6uDK<3-jF(P7r8UObNes$p6^}IF;{v5*C}QHbbU;NR2F%+0?>`V_tm(gxPmkp zlqf9p40+h}V(Y+KJ476JgGriO#2ofcV?ghRCEodP)0NN238m<^3$ya81kOY1- z`dnxyh@x*omE0b(lSqCi_z-aCv6`Ml@6-j+Zm9&gP_Ohnu%Zto*-;_B-*u*IA$qeS za-XB5IZn-=>#wo_#U{3)8_097GdOO*u!gIBnmb7)>N66QT-X62cK=stw?xt805|6WA}i zVI5h~P9T3597NiH!7z=*)7r{l(S=&^h(cWXX&)dG>8-(zB?pzX!KF1NIj+H3;?M|; z6DYwT+X`3d@%9rxxleEggJFj1E8{B8M~WVm57NC(x z3pjx;N2=)uRQ8w17aZ7)j&G~!K0)H82&`3=4+rOo?KpuqFc?Bh8gE&PClk9!w++)s zGM-qizpyF7eH$^2t>7fu-Kc;-_HrXjZe3@j>X>`N4dN?Gm$J*nJ2?!_GT=nn{rlS;VHg&E`6$X?(n>S*@i94Q6R8&Inm5| zGukqRKg;V}tUI@j5~X{X&=2moeI#x_z=53}5*UjX%29TzHB)-3eg0J(xt!Q|%3GPs uFUb{AynWf8XLh4-^INu~WZU_l%0;x;JPAHUjV*hMCrm*2haw0)nD8IYn)fOI delta 3051 zcma)8ZEO_B8Q$5wUEk;K8SZ>{KHIlv17;0|0LBK4Fy_O83LI(-3Xt@0z3Xu1xN~OL z1z#?hHblNdNefIXoRSoN^aDa7NG^X`RVopoid02{5D9mMB2rcTQ&qawm69J-+jnM- zu~VvKqcfOzZop)=9tsUC$oq`M7yh}UDl!(gk0y2iulnjVMob%sx`H-bjM@D1@X z*g!&`4x-DGE6I_4_E_(E-J=M5up~~H@y_# z0$1RM5qU}pFXjuvHEM^lm%~j^X~+)X(QHZ|9mu5UVw#Jf>X2yC<;d~sWr;YDQ(VQ* z1w;3hB_muuS?tU^AXlk9exf>?1%`-g5O>h%vf}ikB_$LAr@`Q4M zE0K>IHB=(I^@3HhJkA<$^M{d8v>?9id$i+ARr5Q81!+#FyHG_wZw-+Z^0xGg+y zSek@)!bsbj&W?;ap@w%*LvUm)!Es{l+1;1nyEI?49Z5Oa3Px9u6* zJBZUGRzE~r-)J^#;1%;CaK*+Y$CN9@r z*?0ZG)dM$9++1+4b=%GCw7|E>cl>pe&wS!vf}=3wYXlVjpMboh;MylED9%;TYmJQ< zCc72h1zT!7`D1X&IPRs~5fDEOoGHg7!rP$99`rm7|3^8QO7nR2{JL+d9*8$SZ|X7F zaGrvFceTE$V0`l6mGJf0)!2=VprH0d1tCdD)&PcRV7I%6f~cyzM7C%g`!Eu>V_5}{FK(v+sE&dGIla&p5RBA?ax(GSaituc7W zdvNJtvOo4MDM#rR^2^wRq3=?9l(i{@DP#a_KfE_9eazU7aSq#(Y0+nkX=GpnKyxwA zfV0h%p2p1CwqTqXWM2a+rgN4d7@J?h&hoF?1?WOqiB}`}ae&i=L2fkG@KJ#@$m`4F zbt;N10OME6_uC2_8E6ZU_v-ujf|tk*JIJ#Q|3*FJP~th%OMaagsxx7Z=653IIPf_h z-$^>vF0_lhsMbrnp)0?J9qBzaAzg>2Ss?e-HWVTLy3&3xzM8z2Y;spbF52+tMa#)&$p-W#d64|xE_#c1x&zxfxDPtJecKf7med4| zTP=w4Wt4xYEZ){8eAp&X-r?!-2)DVW9+z;t2~n=af&bnm_IS$I8?&w`&6}8JvVKj# zj_au91+unzEc_zSd|+SpXf}5|tAby080X2Y=0>>TUz+=tpM-ubf@uz%^Wtc%@8Ltz4sXA`aE*1*Z_m~Q4i<#VjZZgm3MlF1ncATd03WaI_&G%%jX+L|SuK}~G zu<^nupaYa#_>BXQ0#c0jO%2#tSP>(9kbdzJqyaxz>d4mG@{ncqs)|e7O9x zWow06sz85_=AF5Zo*uNEo4!K+wnD8t2i>-iF|xLhPme(Ip`RM;EI_oC-RKQ+bY(Li zK;<8=d|yKQ$AzL25PHjJ>a1p?kyG=Mg{j0^^27VTdkEC$6phJI7 zZnbYkmq=BIj#ia39U{L5KSM{N9u47NQ5I(^VjAt3eyQ!MR7%ecrc!t%H5~vT@2uaf zZJ{lij_^|y=<~+(U11u@n2Lqj+21GC8?{$+P4^*LY^|tOVr-He>Rhn3Q9OhmMrtb}C2)ptv{J>W8~!1l-8#AO?2gjO ziierJT&%w0W7N-i1JeOU`^b&X^S<-+iY!STAiFmnN53VvH||uniwM#zMfUCfFXQ$B A_y7O^ diff --git a/tests/__pycache__/test_class_methods.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_class_methods.cpython-312-pytest-8.3.5.pyc index 8e7627423f80b1824d73f58c4cb198872c548228..2579e1c10add5fe21f320473f4e28acf6ea68e9c 100644 GIT binary patch delta 91 zcmaEtaW8}SG%qg~0}w3FJezrLBd<3f3v*Fv-sUtuPew+&$s73Px#m~Rth&LYe1k`6 f^L%M}#?8M3D!Cw1EFh`PGo|@Nkojlz7qS2Vp1&XN delta 107 zcmcbY@h*e+G%qg~0}z}`Je&D#Bd<3fA8%1^yPW3 r%Nt*mH@?B6e1k`6^F(QR#?9{pD!Cw%Y!J!K-O_v_s3Lpy7qS2V;0YwP diff --git a/tests/__pycache__/test_combine_flexible_dates.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_combine_flexible_dates.cpython-312-pytest-8.3.5.pyc index fb7943e0ff48148e3b51593144fe69e58972c60b..53fb7a89e233b8b0670f5f72ff8ee842414222ce 100644 GIT binary patch delta 77 zcmZqK&iG>+Bj0IWUM>b8Sde)(^G4oAzDYbR%tfVno7eG#GOE}yGB7kSJdjgg!G2NB hU`OT&u8VfT7vd5wiX?V$K9N_QUp2F8vnF4(F8}~l8(;tc delta 63 zcmeydjj?4rBj0IWUM>b8IFNKU(|JbocNN|;*!lvctRN&9VY+c Sm1nyyZ+ubSc(W{Dv@Za$Lls^C diff --git a/tests/__pycache__/test_compare_dates.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_compare_dates.cpython-312-pytest-8.3.5.pyc index c123d8135b2d797e2ecef788b446f0cc37bf4189..9ea3bb683d64619f6fb37835d4a6f7a9169ae60f 100644 GIT binary patch delta 96 zcmX>*i*fHPM!wU$yj%=GFfsFN=3SSKd`r1mn2SpDHXr1UXJoXStjH_RGQVnO)n+$d r4mL1fhW&}WDoAMZb!+*}sk|(Vo2QFNsWa&@O%4#2gDAdkT_y+s&u<>$ delta 160 zcmdlxi}CC%M!wU$yj%=G5T0^2)7*6<-%@Tq-lEdH_>|JbocNN|;*!nVx#Jlb9VUzO z%ClXUH@+xuyxEGEgH6qWk%6Is;enj`3igX~20JoOa9y+uz7UsiQ6#a0^NGAFMD66` b*7BPpd07}YcZx`oP$ANZ>XU diff --git a/tests/__pycache__/test_create_flexible_date.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_create_flexible_date.cpython-312-pytest-8.3.5.pyc index 1a28fbafef45ab5c091024932e5a647112dcc18a..5578abbb37cc4651bba22734be01c22eba74b951 100644 GIT binary patch delta 86 zcmeycit)iJM&8rByj%=G(3W{N(|03pGA|2rQEA@hI^JwXM*GRH_~coZTP?KO%+DXC cjLh#hnkU7WJ6X?HA1pt)-$-bF=%*h*hlX>}gi%Rq2Q%Vza;!9GCOE#DAW-~H6PJX~A p&vsqe{Gzh?W@i2xuZ$1wnZ&7Jpd`f9zPJBsfamnV5Jh6<7j+6QM SK}wfOZ?*{4)~W>rT3RUWcHx}L(q*@uU2LIL zsPa(bgEsTfka&%4QbHf7t4ZUFm>9FE(d<%{deR3UOw`20S~MD-od4`DDVVnDN%otW zf9C(^`~Pod_6PCG%jU05COu-`GvRN-P5z7KQmzSKtezJ{{S@l8NQFa{FuVGb3`1U8 zUTSYQNS15HSw1_95r>{bqG<|==2$NMkLT0n9*c^(@%51h3ThS=?+i5lS;+bSC zZo$}2-{Q7TXjm8szqFCTC6vI~un3JK5j)v{wOp>{p}nTc)9FHfbsI4<8!-StmVTZT zLf;Hv0#E~37{np8Kx{C!=b=sqGZhRb(K~%}9dl=|A5C|5rT2$2o&D*Pr_$ur%*lzr z5puV1#6hQdp=^aAb5gON!u}=JxrB6xo^rb<#?fqU(K$ShyHF)cz(VEoBEnL`Q3`O@4R=a*ITz{8m z?Q$F(Un_Q^XI9IWrO{_6$!}`m%aBHgF=oY}*xzORX_cifjl5 z!_jae7$jwEExo1Riz}(i@U)(+V`G_T<)OrIy|@ZSI^Y^)L_?&JjmcAVn{f}e(MjVr z4KP`1qMsW*+V{t%$8H;i70`*)0|WqoIcWfR5)ZpEaw3WPxiy-qclM+f8 zjztrLLVyVL0|0%$fgFVXA%Gr$UIwx?TY809uJ(N}c?94nz#uHM=QX;2VHBp?Yg6ZB?H<3mMcTn&FPCvqGBZsVi1Q`c`V zS9~6(9X(cGw0n(Gr`aB|HLnySofGsd?7c=O0ycn8lAqP@!X?THhq1p{F6bW#M-t&^ zoHSE=V3VVXnPu(CP+~9=?kDh1Li%WLz>76>BH-zS=T3QhWqn8#gTqo{Feb_cE6<~H zZ0(_m^c!Vn#db04tqUN{5|@8U{Byr4mn!5zqBenH>tU2RAbj&Gs4NbXs%L&{8d<+X`~J~TUqh`5^7s!%KFWCj delta 1793 zcmb7FUrbw77{8~5LjT;hmloERuDxt9xMR%Hj^T7T*c|Hq8N^KnW2~%oYgbx5w}7BH zmib^Z<3`SdiJ31NqXZu|?}NUW=z~e8i9(G|?!%(S%$WFwiDoh4_no%5VIb}#_t$g2 zKfm*x?{`l6?wRjm_Af=zBEXRU{vz>m(~R9?_)C~+`n1y^JSM~}uLv>goB{oU(C_3n z8`#7Zo5XE)usK$2RTD&UvLvY|iguZF5mLn#3=c}>z+r@MsUP4CAxqdTvV*+DW}MaM zKOl4Hg3&7|h9ioR+F?#b1RH=-MlqdQ5wKorMsNVA1VE+|uB%FbUeymUEnGcut>>eG z>jSyxzn;1=HJ?0pGdZ@99Q$_TICVjQV$dxr`^4zcZkPQ+NG1Eq^qU7WXN6g!lNaux zXSW=|Kr3?N2Gc^WE_XP8pq~>#v@sD=wVB2bjeBODN2m`%Xe}}s|DEgQBbn3ZRNbD9 ztJ$HkXetqlW;0Y5aeb_`WOuBO6F=K$+p|G8(b04|4iWG>6dR4EhA@=9X1iV!0CL^T znSdoqAHW^kSg+VkB=)Ab%Y>yab+Sb0{G9V-A|#lce3|Vd&Urw=oYAbG`^qG45dxTvK65jVH?7B1YTc= z{peDh*kKw$*n!Z-eC`(SPMq!o;2X)MK_zy|-JxIwOD&-GwD3cq@%@U0K^fkvcQyC>a&-7>P!g9yA(e5sS>LR}ql zh#q2Z2TralEtKNE3Rxv-dva6x!-HJfcZ@;$BvP+kg0#8wJZ4kWrtqAsv=`@3BOFCI z2B1rY=JU#m-S0!^afE(^7>2n@D%~BJME5G02GNEP)#f)pT9j!}^EF%}o|EX~I#~-k z#iA2jrJuHJy&sj7Z2|A+z*TCEl|M*2R(gglHn*)SBh+fW4M}tu0Uzf7P|qEDsE#MCryk3Ms`szbN`*~ysiYNDU6gbj zH(#Yx8f^%3+Ktd&vVq@ITq`cUa(Fb6$|lk(eVAtXDi2&7MsqhAI9b&_0$3 z*OPMgdN|nLjt2ffx%`~@-req0?jFg+MpN-e=tT%7@Tn7QIoxeqbdCRBQD!kroBpt~ Ku}E_n*6|O_E|ZP` diff --git a/tests/test_class_methods.py b/tests/test_class_methods.py index 1a1205c..f7f186a 100644 --- a/tests/test_class_methods.py +++ b/tests/test_class_methods.py @@ -63,7 +63,7 @@ class TestBoolMethod: def test_bool_method(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "FlexibleDate.__bool__", "FlexibleDate.valueOf", test_data @@ -174,7 +174,7 @@ class TestStrMethod: def test_str_method(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "FlexibleDate.__str__", "FlexibleDate.toString", test_data @@ -250,7 +250,7 @@ class TestReprMethod: def test_repr_method(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "FlexibleDate.__repr__", "FlexibleDate.inspect", test_data @@ -310,7 +310,7 @@ class TestEqualsMethod: def test_equals_method(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "FlexibleDate.__eq__", "FlexibleDate.equals", test_data diff --git a/tests/test_combine_flexible_dates.py b/tests/test_combine_flexible_dates.py index f655dee..88defb4 100644 --- a/tests/test_combine_flexible_dates.py +++ b/tests/test_combine_flexible_dates.py @@ -63,7 +63,7 @@ class TestBasicCombining: def test_basic_combining(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "combine_flexible_dates", "combineFlexibleDates", test_data @@ -112,7 +112,7 @@ class TestConsensus: def test_perfect_consensus(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "combine_flexible_dates", "combineFlexibleDates", test_data @@ -168,7 +168,7 @@ def test_perfect_consensus(self, test_case): def test_majority_agreement(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "combine_flexible_dates", "combineFlexibleDates", test_data @@ -240,7 +240,7 @@ class TestProximityScoring: def test_proximity_scoring(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "combine_flexible_dates", "combineFlexibleDates", test_data @@ -311,7 +311,7 @@ class TestPartialDates: def test_partial_dates(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "combine_flexible_dates", "combineFlexibleDates", test_data @@ -382,7 +382,7 @@ class TestNullValues: def test_null_values(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "combine_flexible_dates", "combineFlexibleDates", test_data @@ -439,7 +439,7 @@ class TestTieBreaking: def test_tie_breaking(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "combine_flexible_dates", "combineFlexibleDates", test_data @@ -506,7 +506,7 @@ class TestMixedPrecision: def test_mixed_precision(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "combine_flexible_dates", "combineFlexibleDates", test_data @@ -607,7 +607,7 @@ class TestEdgeCases: def test_edge_cases(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "combine_flexible_dates", "combineFlexibleDates", test_data diff --git a/tests/test_compare_dates.py b/tests/test_compare_dates.py index ecd90f9..7255b36 100644 --- a/tests/test_compare_dates.py +++ b/tests/test_compare_dates.py @@ -48,7 +48,7 @@ class TestIdenticalDates: @pytest.mark.parametrize("test_case", test_cases, ids=lambda x: x['description']) def test_identical_full_dates(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "compare_two_dates", "compareDates", test_data @@ -88,7 +88,7 @@ def test_identical_full_dates(self, test_case): def test_edge_cases(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "compare_two_dates", "compareDates", test_data @@ -132,7 +132,7 @@ class TestSimilarDates: def test_one_day_different_cases(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "compare_two_dates", "compareDates", test_data @@ -173,7 +173,7 @@ def test_one_day_different_cases(self, test_case): def test_one_month_different_cases(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "compare_two_dates", "compareDates", test_data @@ -206,7 +206,7 @@ def test_one_month_different_cases(self, test_case): def test_one_year_different_cases(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "compare_two_dates", "compareDates", test_data @@ -247,7 +247,7 @@ def test_one_year_different_cases(self, test_case): def test_year_only_five_years_different_cases(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "compare_two_dates", "compareDates", test_data @@ -296,7 +296,7 @@ def test_year_only_five_years_different_cases(self, test_case): def test_partial_date_comparisons_cases(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "compare_two_dates", "compareDates", test_data @@ -361,7 +361,7 @@ def test_partial_date_comparisons_cases(self, test_case): def test_scoring_boundaries_cases(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "compare_two_dates", "compareDates", test_data @@ -414,7 +414,7 @@ class TestBadDates: def test_very_different_dates_cases(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "compare_two_dates", "compareDates", test_data diff --git a/tests/test_create_flexible_date.py b/tests/test_create_flexible_date.py index cd50f2f..ec5f59d 100644 --- a/tests/test_create_flexible_date.py +++ b/tests/test_create_flexible_date.py @@ -69,7 +69,7 @@ class TestFullDates: def test_full_date_parsing(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "create_flexible_date", "createFlexibleDate", test_data @@ -119,7 +119,7 @@ class TestPartialDates: def test_partial_date_parsing(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "create_flexible_date", "createFlexibleDate", test_data @@ -154,7 +154,7 @@ class TestNullDates: def test_null_and_empty_inputs(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "create_flexible_date", "createFlexibleDate", test_data @@ -189,7 +189,7 @@ class TestInvalidDatesExceptionHandling: def test_invalid_date_exception_handling(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "create_flexible_date", "createFlexibleDate", test_data @@ -243,7 +243,7 @@ class TestFullDates: def test_full_edtf_parsing(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "create_flexible_date_from_formal_date", "createFlexibleDateFromFormalDate", test_data @@ -293,7 +293,7 @@ class TestPartialDates: def test_partial_edtf_parsing(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "create_flexible_date_from_formal_date", "createFlexibleDateFromFormalDate", test_data @@ -329,7 +329,7 @@ def test_error_handling(self, test_case): "expected_error": test_case["expected_error"], "mocks": {} } - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "create_flexible_date_from_formal_date", "createFlexibleDateFromFormalDate", test_data diff --git a/tests/test_helper_edge_cases.py b/tests/test_helper_edge_cases.py index 41d770b..1343ea5 100644 --- a/tests/test_helper_edge_cases.py +++ b/tests/test_helper_edge_cases.py @@ -72,7 +72,7 @@ class TestAncientDates: def test_ancient_dates(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "create_flexible_date", "createFlexibleDate", test_data @@ -152,7 +152,7 @@ class TestTextCleaning: def test_text_cleaning(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "create_flexible_date", "createFlexibleDate", test_data @@ -192,7 +192,7 @@ class TestDecadeAndRanges: def test_decades(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "create_flexible_date", "createFlexibleDate", test_data @@ -227,7 +227,7 @@ class TestAMPMHandling: def test_ampm_handling(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "create_flexible_date", "createFlexibleDate", test_data @@ -262,7 +262,7 @@ class TestSpecialCharacters: def test_special_characters(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "create_flexible_date", "createFlexibleDate", test_data @@ -292,7 +292,7 @@ class TestParserEdgeCases: def test_parser_edge_cases(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "create_flexible_date", "createFlexibleDate", test_data @@ -317,7 +317,7 @@ class TestComplexDateGleaning: def test_complex_gleaning(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( + py_result, ts_result = runner.run( "create_flexible_date", "createFlexibleDate", test_data diff --git a/tests/test_validators.py b/tests/test_validators.py index c436839..ef8a794 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -6,9 +6,18 @@ runner = PyScriptTestRunner( Path(__file__).resolve().parent.parent / "dist" / "test_bridge.js", serializer = lambda d: {"likelyYear": d.likely_year, "likelyMonth": d.likely_month, "likelyDay": d.likely_day}, - deserializer = lambda d: FlexibleDate(likely_day=d.likelyDay, likely_month=d.likelyMonth, likely_year=d.likelyYear), + deserializer = lambda d: FlexibleDate(likely_day=d["likelyDay"], likely_month=d["likelyMonth"], likely_year=d["likelyYear"]), ) +def executor(d): + try: + fd = runner.deserializer(d) + return fd + except ValueError: + return "ValueError" + +runner.add_method(FlexibleDate.__init__, "testValidator", executor=executor) + class TestYearValidator: """Test the validate_likely_year validator.""" @@ -50,8 +59,8 @@ class TestYearValidator: def test_valid_years(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( - "test_validator", + py_result, ts_result = runner.run( + "BaseModel.__init__", "testValidator", test_data ) @@ -83,8 +92,8 @@ def test_valid_years(self, test_case): def test_invalid_years(self, test_case): test_data = {"input": test_case["input"], "expected": "ValueError", "mocks": {}} - py_result, ts_result = runner.run_dual_test( - "test_validator", + py_result, ts_result = runner.run( + "BaseModel.__init__", "testValidator", test_data ) @@ -124,8 +133,8 @@ class TestMonthValidator: def test_valid_months(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( - "test_validator", + py_result, ts_result = runner.run( + "BaseModel.__init__", "testValidator", test_data ) @@ -157,8 +166,8 @@ def test_valid_months(self, test_case): def test_invalid_months(self, test_case): test_data = {"input": test_case["input"], "expected": "ValueError", "mocks": {}} - py_result, ts_result = runner.run_dual_test( - "test_validator", + py_result, ts_result = runner.run( + "BaseModel.__init__", "testValidator", test_data ) @@ -198,8 +207,8 @@ class TestDayValidator: def test_valid_days(self, test_case): test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run_dual_test( - "test_validator", + py_result, ts_result = runner.run( + "BaseModel.__init__", "testValidator", test_data ) @@ -231,8 +240,8 @@ def test_valid_days(self, test_case): def test_invalid_days(self, test_case): test_data = {"input": test_case["input"], "expected": "ValueError", "mocks": {}} - py_result, ts_result = runner.run_dual_test( - "test_validator", + py_result, ts_result = runner.run( + "BaseModel.__init__", "testValidator", test_data ) From c943d5ab3db13b919c804b681c2d5664e2e4c851 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Wed, 1 Apr 2026 13:13:53 -0600 Subject: [PATCH 12/41] significantly simplified runner --- PyScriptTestRunner.py | 21 +++++++++++++----- .../PyScriptTestRunner.cpython-312.pyc | Bin 15645 -> 16052 bytes ...class_methods.cpython-312-pytest-8.3.5.pyc | Bin 13406 -> 13096 bytes ...lexible_dates.cpython-312-pytest-8.3.5.pyc | Bin 23416 -> 23109 bytes ...compare_dates.cpython-312-pytest-8.3.5.pyc | Bin 19773 -> 19629 bytes ...st_validators.cpython-312-pytest-8.3.5.pyc | Bin 13221 -> 13221 bytes tests/test_class_methods.py | 8 +++---- tests/test_combine_flexible_dates.py | 2 +- tests/test_compare_dates.py | 2 +- 9 files changed, 21 insertions(+), 12 deletions(-) diff --git a/PyScriptTestRunner.py b/PyScriptTestRunner.py index e0a93f0..ffd1403 100644 --- a/PyScriptTestRunner.py +++ b/PyScriptTestRunner.py @@ -25,8 +25,8 @@ def __init__( self, ts_bridge_path: Path, package_root: Optional[Path] = None, - serializer: Optional[Callable[[Any], Any]] = None, - deserializer: Optional[Callable[[Any], Any]] = None + serializer: Callable[[Any], Any] = lambda d: d, + deserializer: Callable[[Any], Any] = lambda d: d ) -> None: assert isinstance(ts_bridge_path, Path) assert package_root is None or isinstance(package_root, Path) @@ -35,8 +35,17 @@ def __init__( package_root if package_root is not None else Path(__file__).resolve().parent ) self.ts_bridge_path = ts_bridge_path - self.serializer = serializer if serializer is not None else lambda d: d - self.deserializer = deserializer if deserializer is not None else lambda d: d + + self.serializer = serializer + + def list_deserializer(d): + if isinstance(d, list): + try: d = [deserializer(d) for d in d] + except: pass + try: return deserializer(d) + except: return d + self.deserializer = list_deserializer + self._by_py: Dict[str, RegisteredMethod] = {} self._by_ts: Dict[str, RegisteredMethod] = {} @@ -179,9 +188,9 @@ def _call_python_function_with_mocks( try: if rec.executor is not None: - result = rec.executor(input_data) + result = rec.executor(self.deserializer(input_data)) else: - result = rec.py_fn(input_data) + result = rec.py_fn(self.deserializer(input_data)) try: return self.serializer(result) except: return result finally: diff --git a/__pycache__/PyScriptTestRunner.cpython-312.pyc b/__pycache__/PyScriptTestRunner.cpython-312.pyc index b10dec3f67cd33ab07306b4984fa3a141fe30dc1..2aad573f3ca598083aa426fdf9acdd4a5387b91d 100644 GIT binary patch delta 2730 zcmai0YfN0n6`r||-DTek?7{*I%geApOdJdt8#$q1Vmpb24W@vO98#%m(zFWcrc$I-ttw3ZRFUFf#mP3R$e;e{i^gf?HvQ3a?gEO5 zl#X=2J@1)0bI#1ZlzeN}{d$FnKDn9I!{JrX3=cS zs^w|6Aeb_nX4f2JRCA0XZiEiVGk<+!$SD{npbmex!X0L=Gf#$Z7eXa4Ck{QtO$M!m|b&^N~{>@ zLXZXF*AEM+V3#)>Oidp@Z zv>IaIUTmxetb^y}NJJRQSpEpbjBgC2W%0RXSERJ>eE$fKO&J54ZXBDA$2GpQb0RwV z1dHx311nttm?Li<6{oF%6EZM&ECA1w6zL~C1SBs)C?=*(RtePiIs?Qat$SEtR&2d2)rHszoX4RA`{FbMvBwn#Rn3j*+ zvprm40epA95AYm)o)}x3PZ2{hBLTd=LGeI}^pGEr5orv*6HuD7L?RbyioCwzcUijR zR6sYop-FFP`aw!3P1OvkCX@`3OTKgR5SgLKr9m?UnaWAbd_|0E*}|?X2mhnv5nWMQ$yLw&%8%t%@~X9yEUB&JS5k*t{X!j#H0U}nu8|gNL zcJYdHkalG5I)5%Z&@L;FX$IdX-thJ}nr087Sr@`L5cUJW0mY}LA@5Nm%6n1zu(;1R zMF%t2eY<2@CEhRW383?rE4~HzWGx&{q1f;D(es%;{~k*F#l=8vnK{uRbTAKhqes4p z9%0ktj{zsWnE7)+lPuo`DL)|wgNH3oAlWZ|61;uJOxlY;<>_^c78t zjQho6C`5~dw>(6Hf;JV1zlXxxc)xwKt&DpO3!F0>{Z&nj60ub7k6`7RBjwm!!8qPf z+lEmo{#IUHfVnjXQ&aQ>!Ym-Ksc54;;_-?>S3Wn(>k+VkviVI#y#u?HyAW!`Un*KE zM}dqub453f;(Ig3_lt+Zd+8zZop8wVDCm z>dCUr+ucc)i>pDj+^O``$~OoF`3;Ja8?`dh^|c3F@`_6a$%;3xw@O|KR`-VFl}-xu zyHY98?}pUgs>~Zzai!Qia-3)&BJ*5T0>hbTtvm0J1DSLkkDrMrX5wL}wgl&sV!5sg z_W7r}W9hS?j}+yaGM75^^&;Sf!5a{^l``HPS!G;L#Iq`!o}ALTS@mORYL-6MI>&5h z)rmxu>1L%TrVajGRLmfpL-3K)FuUVxP6Vyy0J2uDot=iKFV_<`uIW>WINY|gpwt}z z*c}@q*CdaDFxhUNU{j=;zL)8X9F#0aLCTM1t~a*IC77Fm-iY1YX;xve5rdmKI4AzP zGhBvS&&m^8JS!(-QxRKknYjQW(%eO#5oem~BrDB4+x(V=_K8Dn4SmmGd>vt9hd`N$ zdC92o?ktp%uZriULUC!^82t&Z@@r)P%sFJs5r1f5Wa0Q?_rmba!us_*Q)&mJMR{R*abzKRGtjVZ#(=lO?!MzP4Gz=HJA0p` fj(MEfEWX#oW1TZ}Nvw1p@}w2&{)`}+e@FfYEiG?a delta 2260 zcma)7TWl0n7@o5;vzOWKw%c~ME$qEp=q$JHB2wH+gRK=%D}mBTwKQwb6t|S!^~_YD zE&)v+K#4|=2{*->Xl%R?b$#$vDG$bYshHS|K41cGMk`26G+zFHmLd>iJe&FUJKy=v zf6jl-zq214czMG6smG%T(0X&9*1WV5 zpk~rBP)&Vctc9metI<5yr#m&D=3eNHIBET)G*C-hs2{%Ts8jQf*k}NFJ!lQ^?T3vl zp^XryNpsUhH^o|vD?%)V1}Ej61A+f#4MBDdGz`Ha@Zf@W8ij=#>C)?J43@)GoHP#Z zrv0MY%EzQ#{C&CK_9o#wSH$^B`|0?Twvw2)Q8Fo%gdqoB3bv92x&*pyQs5Wu&k&Ip z9i8=3?=YztEJR+%FG(q~f}e7>_&dh3dmf?L^-Dn4C4fW1Ct2Pp2ioeObHxsD43^m@ zux6mF0U-gPW1uL*D_}Tx!U~sM2)7h1GQc5Wwr@LY1lFfkn@Ud4<#i*QAJ%x6D^?Q% zodprX2oY|$`Vz7!>Dt&xMYkN4L2y_hqtN(eS6Vs;3RjhZ@LX&9%hvTLk{{iBSUOrg zx4iw!<=rRl`()W+>1$`x96zaSQ|gy@xx}+BxvRGPtMZ*KxE$A8flvjYI#>$%YJ}VP zPo93#R_^h>ExOTXin}y}wevrG+m>0jJJGftp%Wp49wnDAjvGTXYh+m$s@?pF+9J8X z{6lR@Btaeybo+4fjR)7p^+1vo%cleFglys8G_*8W@q5r`1BhNb;XYp1*g_7K*EMQ3 z`womgz+YRm**=7H3;$)&HzU?TZN>gWmWbd-co@K}+B%*$a(lG987nZ;t{YTij1A)W zb^ukf@;B{N9BftG~PI-ZP#h@YPXY~UY8TKTgP_YM8j zb?%I&IO4`*Ui8GnzyF0~mVFLVI zVlnbgxkoB*PvoUW>#81vb^271Rf-4`$ntMn_xpDPo$zeW@6H$Y=A%#?1;$2sPK`q@ zN7bzd^DwS9RDLm)lfk()2(197G?vqiTT2gbz?4SyLf(|=_@1K9tSY5A%PLi@O@>Em z%43Br)vd}bj2r9_4j2gI2)?@jRrh}!JGu@4m=2~rHm<>5tQvMQO)nPmy2d8>>0}ex z%s)(akeTw;W4#Y5!BBjWKib|!p5t$~QxY%x*8NVJRI#_W*LpjvHWmd_ zuS$AU!jAE1CQMfG&WwL>CC-%)oCtOV{JpX?f*&D((1C#eD5h+T7soWV1%)<*K7@XR zO$eJ2tSnyV?`2}-FMd7~J2dhToeiaC>(lctYe+%ozUjJYd0L-VW+SU-1Gmjv(dMfJ#e2nLx!Q&F?enh1 yauUQy;<5+W1qeAjRUYjq5cgBKHR~n#2ETacUUG&%yna*l;}Y>+77(p}qrU;{Dh0p* diff --git a/tests/__pycache__/test_class_methods.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_class_methods.cpython-312-pytest-8.3.5.pyc index 2579e1c10add5fe21f320473f4e28acf6ea68e9c..ad8c66388d5e54e2f1f2141ac411a1dd0096f480 100644 GIT binary patch delta 754 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 P9b|aK`7mL($Cv*Hli|>I diff --git a/tests/__pycache__/test_compare_dates.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_compare_dates.cpython-312-pytest-8.3.5.pyc index 9ea3bb683d64619f6fb37835d4a6f7a9169ae60f..b15fc3f9b04e4d713057eeaf74e1cb3f2ba34209 100644 GIT binary patch delta 124 zcmdlxi*fBtM&8rByj%=Gz?pM4bMr=CWo9l`AdeY{KdVi?$0#qonh7M$z!1!!$?R7I z;=E)85}J%fY?DthE6GU#ISmX~JR&bdMPG=Cy%-gDAtCWXQu4)wlq(*oo9$SlJQ$}< Tp6QdxcF`{NDnr_2K3{zRe%&LI delta 261 zcmZ2GlX34XM&8rByj%=GFfsFN=G~3F%FL6`uyM*10Y#=WlrVy1K%j&fLNhRAu}o&< z5SC_0VUz?4t%j;Vmp5TZVM3N)%>>aO%%I8aR|Mj`1nJadE@A^xMeHC#baDx^Qji~z z*}!ncBl1dA!u6=!i&423^71bf6kg0Lx*Sz}rL_7x149|3C(}m=^?`xGi^-b_Nck}N yf&{&o{Fpw2gK&HucKKcOH?>UkH diff --git a/tests/__pycache__/test_validators.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_validators.cpython-312-pytest-8.3.5.pyc index f449cf7df8939494b3005502c492c0fe05dce21d..1e0890f590eb59eff160a35a7f31474a1b1ee42d 100644 GIT binary patch delta 19 ZcmZ3QzBHZdG%qg~0}w39*~m597yvw%1*-r6 delta 19 ZcmZ3QzBHZdG%qg~0}wc8ZseM63;;Sp1xEk? diff --git a/tests/test_class_methods.py b/tests/test_class_methods.py index f7f186a..41e330a 100644 --- a/tests/test_class_methods.py +++ b/tests/test_class_methods.py @@ -8,10 +8,10 @@ deserializer = lambda d: FlexibleDate(likely_day=d["likelyDay"], likely_month=d["likelyMonth"], likely_year=d["likelyYear"]), ) -runner.add_method(FlexibleDate.__bool__, "FlexibleDate.valueOf", executor=lambda d: bool(runner.deserializer(d))) -runner.add_method(FlexibleDate.__str__, "FlexibleDate.toString", executor=lambda d: str(runner.deserializer(d))) -runner.add_method(FlexibleDate.__repr__, "FlexibleDate.inspect", executor=lambda d: repr(runner.deserializer(d))) -runner.add_method(FlexibleDate.__eq__, "FlexibleDate.equals", executor=lambda d: runner.deserializer(d[0]) == runner.deserializer(d[1])) +runner.add_method(FlexibleDate.__bool__, "FlexibleDate.valueOf", executor=lambda d: bool(d)) +runner.add_method(FlexibleDate.__str__, "FlexibleDate.toString", executor=lambda d: str(d)) +runner.add_method(FlexibleDate.__repr__, "FlexibleDate.inspect", executor=lambda d: repr(d)) +runner.add_method(FlexibleDate.__eq__, "FlexibleDate.equals", executor=lambda d: d[0] == d[1]) class TestBoolMethod: diff --git a/tests/test_combine_flexible_dates.py b/tests/test_combine_flexible_dates.py index 88defb4..bc4621f 100644 --- a/tests/test_combine_flexible_dates.py +++ b/tests/test_combine_flexible_dates.py @@ -12,7 +12,7 @@ deserializer = lambda d: FlexibleDate(likely_day=d["likelyDay"], likely_month=d["likelyMonth"], likely_year=d["likelyYear"]), ) -runner.add_method(combine_flexible_dates, "combineFlexibleDates", executor=lambda dl: combine_flexible_dates([runner.deserializer(d) for d in dl])) +runner.add_method(combine_flexible_dates, "combineFlexibleDates") class TestBasicCombining: """Test fundamental combining operations.""" diff --git a/tests/test_compare_dates.py b/tests/test_compare_dates.py index 7255b36..996d983 100644 --- a/tests/test_compare_dates.py +++ b/tests/test_compare_dates.py @@ -13,7 +13,7 @@ deserializer = lambda d: FlexibleDate(likely_day=d["likelyDay"], likely_month=d["likelyMonth"], likely_year=d["likelyYear"]), ) -runner.add_method(compare_two_dates, "compareDates", executor=lambda d: compare_two_dates(runner.deserializer(d[0]), runner.deserializer(d[1]))) +runner.add_method(compare_two_dates, "compareDates", executor=lambda d: compare_two_dates(d[0], d[1])) class TestIdenticalDates: """Test comparison of identical dates returns perfect score of 100.""" From 535f26b9a169432779e76fb1a3bf3d8f9b130428 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Wed, 1 Apr 2026 13:16:58 -0600 Subject: [PATCH 13/41] validator implimentation --- ...st_validators.cpython-312-pytest-8.3.5.pyc | Bin 13221 -> 13213 bytes tests/test_validators.py | 7 +++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/__pycache__/test_validators.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_validators.cpython-312-pytest-8.3.5.pyc index 1e0890f590eb59eff160a35a7f31474a1b1ee42d..3f743c7c90b5a49443c5791ac65e17c7c110d018 100644 GIT binary patch delta 1220 zcmaizT}V@57{~XzIo*zHI;V5y+?h`6xT$lZVr@i*#4y7TvQ*sR%q=~oTfFBODME{G zqHctjsEafRyO3^*a}m^4cSaPYf_5+{@4AUTprDKBea>8{7j|Gjo;@G`=Y9X%c{nyd zW`1R{n053)sVC--`rJMm-XY)gzCJk@((ue8*_erRx<@&gNJ7(S7WM16oYSg#K$w_H zbYh&soIy3>sbn&Nv4b|MW>FH8k}M=g6WB){INdMb6STJ(g8paPzh)Tv;Z{OHu9;MD};OEBY>lT5Gl9U8^aJr z2nskz+}=)w7worDTMx~uHZge_?#WLl1k8^LQbNK5(5@W>%)mAPn?;Foq4WkqEGe&P zP&{S#5%Q21&U#Y~G{AkyTwZMf-gz0u9dKT!l^-UGv~Ia+d1R$)SaTZtNl~Fwxm7-| zXBt7_-%(wpr!vUG814mVN6DeljA=5Jl*idH8^C?!L1lE8ada12R6_2jdgjb7JqIbN z4qaV@+}W%i$KZ1i4+4e&!xU;!#u07Jwk0D_i~@Lo2yMk#>vlG#puESs7~}w=^0qpH zP)sSWd5p?NAq|$yFxo;kE=;pc*h98z>-MwL>2Gkpreo5!okh#xmB`?K>~xUxzMv7t za4%W#IlS=deP$_U@?me7Z4Tm#JHFO8;R*8I*Rso9y4v`KHsLX)+rPBe;%<*t+s|H- z3Y2v2G4l!)REx4^(!*?fJ`0QXSSdjcpeQc`Cy@<8u2jvBO^FjSefRNkQWb2rAERkC z?~)*oPlz$BhoKQ71|vQf6wE?A&QB)f@l;&R=Xo(H$~>R>higBTwuB=ef@SC?`5BDn N8qk5S1!S=K)L+--BOd?& delta 1174 zcmaizT}V@57{|}Mv+sFYn`?7(Zb#>`HMMPiKx$%zq!_7KmiaNxa=F=@@ovkkpe0?{ zWxP}r1O<8_UB!u_?gODR%!~BkMYkoS6i9g0`XF(yXKdu`pEQXk`69Q zMc#8Uq?Q{!q``R0PyxvU03RuJSjd8=81;*r#y6}_uChbw5FiLB0fYdx09m@8tQ7k8 zdcZ#nXaF=4v!zPi1ZEp)wRBO@`<8nMb&^VJk?Rb^<*631%mDXaSjC8S8X>p1X8V8; zPhQ)rbmfqQ7bA;nGenne(LDQvaVkob-ZK^a5(tSqxfB&YADBD zBP^R}##;6$lUmjEeJ~c7z17W~&xkyi=$s0x5XKYX*4$KP*i0 zLCh0r|G|H3$5dI37D%$fhIZM-34i<11<-}RM{Z*Zn4kgeBYXgNQ|~JeA#E;H6T+k0 z+^QhHL>B#ZI~GVss@G`|z9_a<&h6Zy-&I1afxt~U|G!NdbDN}-ci-a#9U__H$^5eG zFg$Ow7-E1I^iGLy1ILjWT%T3aMl$h4I-X477UBriSej{A(p(nOu|zzAS?b{dB81wJ qlROJKeDY^1AC#=QbIz8wX>5ErlZZCsJLG$4NTou~jXk8h=HzdudJnDu diff --git a/tests/test_validators.py b/tests/test_validators.py index ef8a794..8a11db6 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -10,11 +10,10 @@ ) def executor(d): - try: - fd = runner.deserializer(d) - return fd - except ValueError: + fd = runner.deserializer(d) + if not isinstance(fd, FlexibleDate): return "ValueError" + return fd runner.add_method(FlexibleDate.__init__, "testValidator", executor=executor) From 96f556a8864f4d147e537d63383206e541ca6098 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Wed, 1 Apr 2026 14:30:02 -0600 Subject: [PATCH 14/41] simplified test bridge add method calls --- PyScriptTestBridge.ts | 25 +++++- .../PyScriptTestRunner.cpython-312.pyc | Bin 16052 -> 16052 bytes test_bridge.ts | 80 ++++++------------ 3 files changed, 47 insertions(+), 58 deletions(-) diff --git a/PyScriptTestBridge.ts b/PyScriptTestBridge.ts index 669692c..ee290bf 100644 --- a/PyScriptTestBridge.ts +++ b/PyScriptTestBridge.ts @@ -9,10 +9,22 @@ export interface TestResponse { error?: string; } -export type TestMethodHandler = (args: any[]) => TestResponse; +export type TestMethodHandler = (args: any[]) => any; export class PyScriptTestBridge { + + constructor( + private readonly serializer: (data: any) => any = (data: any) => data, + private readonly plainDeserializer: (data: any) => any = (data: any) => data, + ) {} + private readonly handlers = new Map(); + private readonly deserializer: any = (data: any) => { + if (Array.isArray(data)) { + try { return data.map(this.plainDeserializer); } catch { return data; } + } + try { return this.plainDeserializer(data); } catch { return data; } + }; addMethod(tsMethodName: string, handler: TestMethodHandler): void { if (!tsMethodName || !String(tsMethodName).trim()) { @@ -30,7 +42,16 @@ export class PyScriptTestBridge { return { success: false, error: `Unknown method: ${request.method}` }; } try { - return handler(request.args); + console.error(request); + console.error(this.deserializer(request.args)); + const result = handler(this.deserializer(request.args)) + console.error(result); + console.error(this.serializer(result)); + try { + return {success: true, result: this.serializer(result)}; + } catch { + return {success: true, result: result}; + } } catch (error) { return { success: false, diff --git a/__pycache__/PyScriptTestRunner.cpython-312.pyc b/__pycache__/PyScriptTestRunner.cpython-312.pyc index 2aad573f3ca598083aa426fdf9acdd4a5387b91d..d00387e653ea58383a84f84ff544e4ee66244b79 100644 GIT binary patch delta 19 Zcmdl|yQP-vG%qg~0}xEC-pIAe4gf)+1`7ZH delta 19 Zcmdl|yQP-vG%qg~0}u%2ZRA>I2LL``1%3bk diff --git a/test_bridge.ts b/test_bridge.ts index 8c5acc0..6bef8b4 100644 --- a/test_bridge.ts +++ b/test_bridge.ts @@ -7,83 +7,51 @@ import FlexibleDate from "flexibledatets"; import { PyScriptTestBridge } from "./PyScriptTestBridge"; function serializeFlexibleDate(fd: FlexibleDate): any { - return { - likelyYear: fd.likelyYear, - likelyMonth: fd.likelyMonth, - likelyDay: fd.likelyDay, - }; + if (fd.constructor.name === "FlexibleDate") { + return { + likelyYear: fd.likelyYear, + likelyMonth: fd.likelyMonth, + likelyDay: fd.likelyDay, + }; + } + return fd; } function deserializeFlexibleDate(data: any): FlexibleDate { return new FlexibleDate(data.likelyDay, data.likelyMonth, data.likelyYear); } -const bridge = new PyScriptTestBridge(); +const bridge = new PyScriptTestBridge(serializeFlexibleDate, deserializeFlexibleDate); -bridge.addMethod("createFlexibleDate", (args) => { - const [dateString] = args; - const fd = new FlexibleDate(dateString); - return { success: true, result: serializeFlexibleDate(fd) }; -}); +bridge.addMethod("createFlexibleDate", (args) => new FlexibleDate(args[0])); bridge.addMethod("createFlexibleDateFromFormalDate", (args) => { - const [formalDateString] = args; - const fdFromFormal = new FlexibleDate(null, null, null); - const result = fdFromFormal.createFlexibleDateFromFormalDate(formalDateString); - return { success: true, result: serializeFlexibleDate(result) }; + const fd = new FlexibleDate(null, null, null); + return fd.createFlexibleDateFromFormalDate(args[0]); }); -bridge.addMethod("compareDates", (args) => { - const [date1Data, date2Data] = args; - const fd1 = deserializeFlexibleDate(date1Data); - const fd2 = deserializeFlexibleDate(date2Data); - const score = fd1.compareDates(fd2); - return { success: true, result: score }; -}); +bridge.addMethod("compareDates", (args) => args[0].compareDates(args[1])); bridge.addMethod("combineFlexibleDates", (args) => { - const dates = args.map((d: any) => deserializeFlexibleDate(d)); - const fd_temp = new FlexibleDate(null, null, null); - const combined = fd_temp.combineFlexibleDates(dates); - return { - success: true, - result: serializeFlexibleDate(combined) - }; + const dates = args[0] as FlexibleDate[]; + const fdTemp = new FlexibleDate(null, null, null); + return fdTemp.combineFlexibleDates(dates); }); -bridge.addMethod("FlexibleDate.toString", (args) => { - const [fdData] = args; - const fdForString = deserializeFlexibleDate(fdData); - return { success: true, result: fdForString.toString() }; -}); +bridge.addMethod("FlexibleDate.toString", (args) => args[0].toString()); -bridge.addMethod("FlexibleDate.valueOf", (args) => { - const [fdDataValue] = args; - const fdForValue = deserializeFlexibleDate(fdDataValue); - return { success: true, result: fdForValue.valueOf() }; -}); +bridge.addMethod("FlexibleDate.valueOf", (args) => args[0].valueOf()); -bridge.addMethod("FlexibleDate.inspect", (args) => { - const [fdDataRepr] = args; - const fdForRepr = deserializeFlexibleDate(fdDataRepr); - return { success: true, result: fdForRepr.inspect() }; -}); +bridge.addMethod("FlexibleDate.inspect", (args) => args[0].inspect()); -bridge.addMethod("FlexibleDate.equals", (args) => { - const [fdDataEquals1, fdDataEquals2] = args; - const fdForEquals1 = deserializeFlexibleDate(fdDataEquals1); - const fdForEquals2 = deserializeFlexibleDate(fdDataEquals2); - return { success: true, result: fdForEquals1.equals(fdForEquals2) }; -}); +bridge.addMethod("FlexibleDate.equals", (args) => args[0].equals(args[1])); bridge.addMethod("testValidator", (args) => { - try { - const [fdDataValidator] = args; - const fdForValidator = deserializeFlexibleDate(fdDataValidator); - return { success: true, result: serializeFlexibleDate(fdForValidator) }; - } catch { - return { success: true, result: "ValueError" }; + const [x] = args; + if (x instanceof FlexibleDate) { + return x; } + return "ValueError"; }); if (require.main === module) { From 2b577e01b5e08f9c018259dca5cd2104000cd35d Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Wed, 1 Apr 2026 15:49:02 -0600 Subject: [PATCH 15/41] Final tests fixed --- test_bridge.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test_bridge.ts b/test_bridge.ts index 6bef8b4..6785aed 100644 --- a/test_bridge.ts +++ b/test_bridge.ts @@ -18,7 +18,12 @@ function serializeFlexibleDate(fd: FlexibleDate): any { } function deserializeFlexibleDate(data: any): FlexibleDate { - return new FlexibleDate(data.likelyDay, data.likelyMonth, data.likelyYear); + if ("likelyDay" in data && + "likelyMonth" in data && + "likelyYear" in data) { + return new FlexibleDate(data.likelyDay, data.likelyMonth, data.likelyYear); + } + return data; } const bridge = new PyScriptTestBridge(serializeFlexibleDate, deserializeFlexibleDate); @@ -33,7 +38,7 @@ bridge.addMethod("createFlexibleDateFromFormalDate", (args) => { bridge.addMethod("compareDates", (args) => args[0].compareDates(args[1])); bridge.addMethod("combineFlexibleDates", (args) => { - const dates = args[0] as FlexibleDate[]; + const dates = args as FlexibleDate[]; const fdTemp = new FlexibleDate(null, null, null); return fdTemp.combineFlexibleDates(dates); }); From accf3eb7d8efd50423461979bdaff19965414a52 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Wed, 1 Apr 2026 16:25:57 -0600 Subject: [PATCH 16/41] final clreaning --- PyScriptTestBridge.ts | 4 - README.md | 112 +++- ...class_methods.cpython-312-pytest-8.3.5.pyc | Bin 13096 -> 0 bytes ...lexible_dates.cpython-312-pytest-8.3.5.pyc | Bin 23109 -> 0 bytes ...compare_dates.cpython-312-pytest-8.3.5.pyc | Bin 19629 -> 0 bytes ...flexible_date.cpython-312-pytest-8.3.5.pyc | Bin 21856 -> 0 bytes ...er_edge_cases.cpython-312-pytest-8.3.5.pyc | Bin 16810 -> 0 bytes .../test_utils.cpython-312-pytest-8.3.5.pyc | Bin 18695 -> 0 bytes tests/__pycache__/test_utils.cpython-312.pyc | Bin 15373 -> 0 bytes ...st_validators.cpython-312-pytest-8.3.5.pyc | Bin 13213 -> 0 bytes tests/test_class_methods.py | 321 --------- tests/test_combine_flexible_dates.py | 619 ------------------ tests/test_compare_dates.py | 425 ------------ tests/test_create_flexible_date.py | 343 ---------- tests/test_helper_edge_cases.py | 329 ---------- tests/test_validators.py | 251 ------- 16 files changed, 96 insertions(+), 2308 deletions(-) delete mode 100644 tests/__pycache__/test_class_methods.cpython-312-pytest-8.3.5.pyc delete mode 100644 tests/__pycache__/test_combine_flexible_dates.cpython-312-pytest-8.3.5.pyc delete mode 100644 tests/__pycache__/test_compare_dates.cpython-312-pytest-8.3.5.pyc delete mode 100644 tests/__pycache__/test_create_flexible_date.cpython-312-pytest-8.3.5.pyc delete mode 100644 tests/__pycache__/test_helper_edge_cases.cpython-312-pytest-8.3.5.pyc delete mode 100644 tests/__pycache__/test_utils.cpython-312-pytest-8.3.5.pyc delete mode 100644 tests/__pycache__/test_utils.cpython-312.pyc delete mode 100644 tests/__pycache__/test_validators.cpython-312-pytest-8.3.5.pyc delete mode 100644 tests/test_class_methods.py delete mode 100644 tests/test_combine_flexible_dates.py delete mode 100644 tests/test_compare_dates.py delete mode 100644 tests/test_create_flexible_date.py delete mode 100644 tests/test_helper_edge_cases.py delete mode 100644 tests/test_validators.py diff --git a/PyScriptTestBridge.ts b/PyScriptTestBridge.ts index ee290bf..849fc4b 100644 --- a/PyScriptTestBridge.ts +++ b/PyScriptTestBridge.ts @@ -42,11 +42,7 @@ export class PyScriptTestBridge { return { success: false, error: `Unknown method: ${request.method}` }; } try { - console.error(request); - console.error(this.deserializer(request.args)); const result = handler(this.deserializer(request.args)) - console.error(result); - console.error(this.serializer(result)); try { return {success: true, result: this.serializer(result)}; } catch { diff --git a/README.md b/README.md index 0039778..15d1a12 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,24 @@ A package that runs pytest-style checks concurrently in Python and TypeScript so ## Architecture -- **`PyScriptTestRunner`** (Python): register one **named** Python function per operation with the TypeScript RPC name your Node bridge expects, then call `run` / `run_dual_test` with the same `test_data` shape as before. -- **`PyScriptTestBridge`** (TypeScript): register handlers with `addMethod(tsName, (args) => response)`. The compiled **`test_bridge_entry.js`** is invoked by the runner via `node`; it parses one JSON request from argv and prints one JSON response. +- `**PyScriptTestRunner**` (Python): register one **named** Python function per operation with the TypeScript RPC name your Node bridge expects, then call `run` with the same `test_data` shape as before. +- `**PyScriptTestBridge`** (TypeScript): register handlers with `addMethod(tsName, (args) => response)`. The compiled `**test_bridge_entry.js**` is invoked by the runner via `node`; it parses one JSON request from argv and prints one JSON response. ## Python: registration and `run` ```python -runner = PyScriptTestRunner() +runner = PyScriptTestRunner( + "Path/to/test/bridge.ts", + (Optional) serializer = function to serialize objects into consistent Json-like structures, + (Optional) deserializer = function to deserialize objects into an expected custom class. +) -def create_flexible_date(input_data): - result = my_create_flexible_date(input_data) - return serialize_if_needed(result) +def create_flexible_date(arg1, arg2, ...) -> FlexibleDate: + some code... + return flexible_date_object -runner.add_method(create_flexible_date, "createFlexibleDate") -runner.add_method(combine_flexible_dates, "combineFlexibleDates", ts_pack_input=True) +runner.add_method(create_flexible_date, "createFlexibleDate", (Optional) executor=lambda args: create_flexible_date(args[0], args[1], ...)) +runner.add_method(combine_flexible_dates, "combineFlexibleDates", (Optional) ts_pack_input=True) # ... py_result, ts_result = runner.run( @@ -32,16 +36,47 @@ Rules: - **`add_method(py_callable, ts_method_name, *, ts_pack_input=False)`** - `py_callable` must be a **named** function (not a lambda). The registry key is `py_callable.__name__` (what you pass as the first argument to `run`). - `ts_method_name` must match `addMethod` on the TS side and the JSON `method` field. - - **`ts_pack_input=True`**: for this operation the runner sends `args: [input_data]` to Node (single array argument). Use this for TS handlers that expect one aggregate argument (for example `combineFlexibleDates` with `const [datesData] = args`). + - `executor` is some exeutable that processes the test data if neccesary. + - `ts_pack_input=True`: for this operation the runner sends `args: [input_data]` to Node (single array argument). Use this for TS handlers that expect one aggregate argument (for example `combineFlexibleDates` with `const [datesData] = args`). - **`run(python_name, ts_name, test_data)`** checks that `ts_name` matches the name registered for `python_name`. -- Registered Python callables receive a **single** argument: `test_data["input"]`. Shape the input in your tests so each callable can implement the operation (unwrap lists, deserialize dicts to domain objects, etc.). -- Return **JSON-serializable** values from Python (or the same shapes your TS bridge returns). The runner no longer auto-serializes domain types; keep that logic in your registered functions. +- By default, registered Python callables receive a **single** argument: `test_data["input"]`, and returns the (optionally) serialized result of running the callable on the argument. If more arguments are needed, or if other post-processing on the output is needed, provide a test executor. +- Serializers and deserializers are optional. If a function is meant to return or take in a custom class, the runner must be provided with (de)serialization function(s), otherwise the data will be treated as raw types (bool, int, float, str, etc...). By default, the deserializer is capable of deserializing lists, so the provided deserialization function only needs to support deserialiation into the given class. The serializer does not support this. + +## TypeScript: `test_bridge.ts` + +The test bridge contains all that information neccessary for the python runner to call the TS functions under test. It is instantiated with optional serializer and deserializer parameters like the runner. + +**Example:** +```ts +function serializeFlexibleDate(fd: FlexibleDate): any { + if (fd.constructor.name === "FlexibleDate") { + some code... + return jsonLikeObject; + } + return fd +} + +function deserializeFlexibleDate(data: any): FlexibleDate { + if (can serialize to FlexibleDate...) { + return new FlexibleDate(serialization logic...); + } + return data; +} + +const bridge = new PyScriptTestBridge(serializeFlexibleDate, deserializeFlexibleDate); -## TypeScript: `test_bridge_entry.ts` +bridge.addMethod("createFlexibleDate", (args) => new FlexibleDate(args[0])); +``` + +Rules: -This repo ships an example entry file that registers FlexibleDate operations and imports the sibling **`../FlexibleDate/FlexibleDateTS/dist/FlexibleDateTS`** build. For your own repo, point that import at your published or local TS package and keep the same `addMethod` names as on the Python side. +**`addMethod("TSFunctionName, exector)`** + - `TSFunctionName` must exactly match an `add_method()` entry on the cooresponding test runner. This is how the runner knows which function to run within the bridge. + - `executor` must be the test executor function which runs the function under test. Unlike the runner, the bridge has no default executor, and so an executor must be provided with each `addMethod()` call. -Build output goes to **`dist/test_bridge_entry.js`**. The Python runner defaults to **`package_root / "dist" / "test_bridge_entry.js"`**. +**Serializer and Deserializer** + - Unlike the runner, the bridge has no automatic serialization handling. This is a result of differences between Python and TypeScript runtime behavior. Thus, the (de)serialization functions must recognize whether the objects being passed into them can be (de)serialized as intended. + - Alternatively, it is possible to have multiple bridge instances/files with and without (de)serializers as needed, though this is not recommended unless neccessary. ## RPC argument list (`args`) @@ -49,11 +84,56 @@ The subprocess request is `{ "method": string, "args": any[], "mocks": object }` - For most operations the runner sets **`args`** from `test_data["input"]` as: - `[input_data]` when `input_data` is not a `list`, - - or **`input_data` as-is** when it is already a `list` (so it becomes multiple `args` elements, matching the old behavior for `compareDates`, `test_equals`, etc.). + - or **`input_data` as-is** when it is already a `list`. - When **`ts_pack_input=True`**, the runner always sends **`args: [input_data]`** (one element), so the TS handler uses `const [x] = args` (for example a list of serialized dates). Align Python `input_data` in tests with this contract so both sides see the same logical inputs. +## Test Examples + +```python +class TestIdenticalDates: + """Test comparison of identical dates returns perfect score of 100.""" + + test_cases = [ + { + "input": [ + {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 15}, + {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 15} + ], + "expected": 100, + "description": "identical full dates" + }, + { + "input": [ + {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None}, + {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None} + ], + "expected": 100, + "description": "identical year-month dates" + }, + { + "input": [ + {"likelyYear": 1995, "likelyMonth": None, "likelyDay": None}, + {"likelyYear": 1995, "likelyMonth": None, "likelyDay": None} + ], + "expected": 100, + "description": "identical year-only dates" + } + ] + + @pytest.mark.parametrize("test_case", test_cases, ids=lambda x: x['description']) + def test_identical_full_dates(self, test_case): + test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} + py_result, ts_result = runner.run( + "compare_two_dates", + "compareDates", + test_data + ) + assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" + assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" + runner.assert_strict_parity(py_result, ts_result, test_case['description']) +``` ## Developing ```bash @@ -61,4 +141,4 @@ npm ci npm run build ``` -Requires Node.js and npm. The example bridge import expects the FlexibleDate TS package to be built (`npm run build` in that repo) when using the default relative path. +Requires Node.js and npm. \ No newline at end of file diff --git a/tests/__pycache__/test_class_methods.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_class_methods.cpython-312-pytest-8.3.5.pyc deleted file mode 100644 index ad8c66388d5e54e2f1f2141ac411a1dd0096f480..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13096 zcmeHNO>o>sb_V7bGaUYkLs29pL8ACGl11}Jv@NYAM=~v~EQzv2+AEQiu)_gJf&yp8 z4N%q~R94wI73a1$?^c}3R!&yQsZtK9(jf;QS+%uQ16y8lvAdOQWpmI?3Y~I=hiu;K z1~9;o^om=m)E)?O8jY`CzixCl9=`X`KdY@xa&WZW`njqMaNK|3g?mJu%vvPGaUXLs zC-WLN!P9eSA)E{G+(Kj`%411Li!H<_;xr%DgoVUJg64TGIgy0&h*q;uGf~5HAx^1* zUtCE&;F7liCe(W3YCJJIBdQA9C_8 zZ!FRjYX&UsVJ#GE0c^L2?WR~OV0%2QwZv!Io?yeH8QzY(lQrh<^LFa2rrc4dHDx4g zD1bM;QyR+=wP=hhx^ZkL4oX`;%rO5ePmb7r63NeogP@wI+PKIROs zYzfxf9>Cxna?W8qe-?3PXpGB5N9}~B-cq#ENVa6xuc=r^LkOq8Gn2?B19>EOFwF%9|YGYYkXH=5R~^9vs!T7SItWGYQF`~IjrySC_!WP z1XtsIZtGE~gK{0m>mFCWazg>W~|i2wJ+&cDYtT1{?Rlk(v-np-kGYLyL31B{2`FD1+$}iDE30ye^)F zIacn6?WmeBE*khm;EGdZy9Sks+FrWB-eS)_$ZnmJhprUcIsF*EikivX|_lCUc4Ic(BB zVDwn?KZZ^~^Stwx;MrS3<#0{j4H>RHH;X1QZhL_`5XD0L@!ir_V!26-cy!))V0G!pZKbyBAMr*{YtMlla0Ds<-{i;6YlG7+*hNaN;O={Nf@6Rv~YjN{e8%* zoieekb}6p%REuEX3xOU!ea$XbIWGyW4)^>+;Fov!yD=F5%1lK5h?#&ozL}_dVKWhN zd-?}w!jpuVsPm1(iUbn@hV-fZgXEtdK-x_I)Wh}ofxZYPQ*i75J_X50_iVJOCA zXdS30@TUg&UW(8iY00fv&3$ca#>9S1x1o8EEKauu~SeiO)sjN0X)NwX$6qb?N&*jD=ca< z6_sYS!gN-X^4SH&PDpxoMnMgr4Z6FKH6+&21%1}8J5BoxZ;Q7GDG+;?D*|s6Et}68 zP+~VTc?sB&nlmKmhH8}VC+vu>Xfxzx7)i&1!#gTVPU38gAXtN4WpxMr7X$2qb>^7= z;uRfDndlyr6xt1RH0axty*thQRmQzqfExKzz+~=m|Guy1)3~*-|B_09pUPg)jrYtYjYxGqhbtv^;!a`F#1koOMwyUr@?J0S!?37sAZ1g}N1? z(-Jy=dj8YWqs-@*tP@wi62|D8F_W+bi7q8cvXc_HGV-FPKt3f&$Te}1!Bt$6m`>jNl6$mMB}!c4Q$-ZrL@kdY}fZBycGvaH$-^1T8sp0nle9n4m+TBW3)M`(eg%=)rfm`bSAqDy2E_lk`{I#L1c-kk^BubuO%nK@4)5>9ONaM^+1#4;lb2wc z$x%%DFzJEBtQi;>uo3Pk5}IU_@rrjxg9G?WW+#)&&{68Dok}hpfBgyEk7D zQ8HtPp{kjzPja)z=nt&pG?a&J?(oHLkMGvo>z zgwVw^h0(%oCX|NUCaRE}o5F~abEF`*QHs8KpU+_;5OsAd?I7efcuOsT)$2J+|Aex5 z_uwq?%SXTX?loqwh5_1fm0%Fy_=5!9bE3Q$~pT9S*-YXmYBGspWY- z1`qTdNc;|i{}u*Mrf_O*U~&@^2@<;wCxZ?<<>@4gCDWK76(uE1s<^p=7$%qCr~eAL zDRT2c#?4(u+_l=??IX%A3EvllUABF2wf%@s+EpU1w);uAD-FEA$0zOM=3N1pc?oS> zHaXyvc6CEff&`y=2>`#mx%3ur-{#U=!2O#```wtPnkxJ$Q#3S6Vyte6ES>h+s57=&ZP|Nga@L=48~% z4OFJlLtIA$wFDgzbVC^8cSOR}#qm2LdnvL{-cL1)UabT2L5iSb#Pm3h$ZXBFoP#cK z4!XfP=<%F`-YVxH!@de!g%FJ69r(WrU}R>y?!giC0FL$#9~~GbGcYsEF)#%eCn|tu zlwOiK$g5w&WC74W+~03@P$%I7)tD2DSy_fJW@7JVL}5UJ3PDcp<#z-`-1!E`Boj?$Dy51 z5->V{Vm!P0BLp-0WFO4oizeDET232qm^O`;`-~0cd_DncbSAMYXmm1(P1#~St*|v+ z{2|t_qKiw2Ve+r=(~moJ(Fx_cOs|SA`q-GC7rHERzn{|eK8h?Kv(oRZwgPXA6Zi$#ANcj@4sy;$D}7CQZAT0Ur{FHzVD{6Yx2q8n&#r%&3w zodaG6XZhD?oEWwrZ97hkV;LZ07@NUk?Q2N3j1$Ag2o~3!4f$fmZaf$^qX9L8$S{;v zC|MoEiCMJR$7{nTXfrtL$8h?;sjsWewghMtZ{R{@j*dUWhHN@@Oh207TwvhAmY{tO1RYj zgzzFM!pJ{qhUBa%Vl7#n!Ov}ZBUp>>*cdh2P{a$V26UQ7af^p8Qhrm>GFpJ3Cf#i1 zNmE^Bk53#vsu-|MON5VmxjE3mc2}@%A}^4(?5Q4VkGKrO!|g^22bOPqae?jDB0CIb zhrw)nd&>@k`O{-C!Ci)uq`VJ-!UJITUNnDc0vl+6dfdk7#tJ((*qx0+)B-(U#PGms z3jTK@hTDx8HafFH2Y=I`;ostdsA7SCjTk1dZ=cg+4htN9n&iYo4^Mx7#%egXy1&!O zJ&lH2>L8xD2O@p1KdX(y7yjmNl>@$Y(kD0axnVV2M13sh*Z1nCa;HBZu^P^Ma-i>R zpWK%H1Gy0<_a>j&6Dvp3Pv5I%bmha}u})9Y_}Ww@zIG!JU&DdP?6C!RM-RCGfW1dz zn}A3|9I)T1pxstr%LeQ;${XYymM0-bPw03qrcNOsJz3f-t*!Gt;h zc!yPM0(PdeZ6v42PoNZ*1gEb<@-)oz{J&vT`75sVH{9T_xZ2-v1HVnQ|1u(c)cRMW zE0H!U()MMfV+Q)h*Z7#5TIFJu!fvanyW9(f6SQzLQ24DiyR4?Ja?k75xf`_R q&2M7I`EmXkhv_$Wc#Wr5^z_Zm@OgGgPv5*3Ist|M#bHWaum1xcZg<@P diff --git a/tests/__pycache__/test_combine_flexible_dates.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_combine_flexible_dates.cpython-312-pytest-8.3.5.pyc deleted file mode 100644 index 6c78a33ff63ccd0b3ef2d14ecc109ff999c0d026..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23109 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 UvXY-CuX|>3_df;lL!iq40%F3`YybcN diff --git a/tests/__pycache__/test_compare_dates.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_compare_dates.cpython-312-pytest-8.3.5.pyc deleted file mode 100644 index b15fc3f9b04e4d713057eeaf74e1cb3f2ba34209..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19629 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 diff --git a/tests/__pycache__/test_create_flexible_date.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_create_flexible_date.cpython-312-pytest-8.3.5.pyc deleted file mode 100644 index 5578abbb37cc4651bba22734be01c22eba74b951..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21856 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 diff --git a/tests/__pycache__/test_helper_edge_cases.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_helper_edge_cases.cpython-312-pytest-8.3.5.pyc deleted file mode 100644 index fc6ed3e73cadcab5f82256633e56bce5d5d83da4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16810 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 diff --git a/tests/__pycache__/test_validators.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_validators.cpython-312-pytest-8.3.5.pyc deleted file mode 100644 index 3f743c7c90b5a49443c5791ac65e17c7c110d018..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13213 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 diff --git a/tests/test_class_methods.py b/tests/test_class_methods.py deleted file mode 100644 index 41e330a..0000000 --- a/tests/test_class_methods.py +++ /dev/null @@ -1,321 +0,0 @@ -from pathlib import Path -from FlexibleDate.FlexibleDate import FlexibleDate -import pytest -from PyScriptTestRunner import PyScriptTestRunner - -runner = PyScriptTestRunner( - Path(__file__).resolve().parent.parent / "dist" / "test_bridge.js", - deserializer = lambda d: FlexibleDate(likely_day=d["likelyDay"], likely_month=d["likelyMonth"], likely_year=d["likelyYear"]), -) - -runner.add_method(FlexibleDate.__bool__, "FlexibleDate.valueOf", executor=lambda d: bool(d)) -runner.add_method(FlexibleDate.__str__, "FlexibleDate.toString", executor=lambda d: str(d)) -runner.add_method(FlexibleDate.__repr__, "FlexibleDate.inspect", executor=lambda d: repr(d)) -runner.add_method(FlexibleDate.__eq__, "FlexibleDate.equals", executor=lambda d: d[0] == d[1]) - - -class TestBoolMethod: - """Test the __bool__ method of FlexibleDate.""" - - bool_test_cases = [ - { - "input": {"likelyYear": None, "likelyMonth": None, "likelyDay": None}, - "expected": False, - "description": "fully null date returns False" - }, - { - "input": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "expected": True, - "description": "full date returns True" - }, - { - "input": {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None}, - "expected": True, - "description": "year only returns True" - }, - { - "input": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None}, - "expected": True, - "description": "year and month returns True" - }, - { - "input": {"likelyYear": None, "likelyMonth": 5, "likelyDay": None}, - "expected": True, - "description": "month only returns True" - }, - { - "input": {"likelyYear": None, "likelyMonth": None, "likelyDay": 15}, - "expected": True, - "description": "day only returns True" - }, - { - "input": {"likelyYear": None, "likelyMonth": 5, "likelyDay": 15}, - "expected": True, - "description": "month and day returns True" - } - ] - - # Note: NaN handling test (line 78) cannot be tested via public API - # since Pydantic validators enforce Optional[int] types, not float. - # The math.isnan check appears to be defensive programming. - - @pytest.mark.parametrize("test_case", bool_test_cases, ids=lambda x: x['description']) - def test_bool_method(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "FlexibleDate.__bool__", - "FlexibleDate.valueOf", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - -class TestStrMethod: - """Test the __str__ method of FlexibleDate.""" - - str_test_cases = [ - { - "input": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "expected": "15 May 2020", - "description": "full date" - }, - { - "input": {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None}, - "expected": "2020", - "description": "year only" - }, - { - "input": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None}, - "expected": "May 2020", - "description": "year and month" - }, - { - "input": {"likelyYear": None, "likelyMonth": 5, "likelyDay": 15}, - "expected": "15 May", - "description": "month and day (no year)" - }, - { - "input": {"likelyYear": None, "likelyMonth": 5, "likelyDay": None}, - "expected": "May", - "description": "month only" - }, - { - "input": {"likelyYear": None, "likelyMonth": None, "likelyDay": 15}, - "expected": "15", - "description": "day only" - }, - { - "input": {"likelyYear": None, "likelyMonth": None, "likelyDay": None}, - "expected": "", - "description": "null date" - }, - { - "input": {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": None}, - "expected": "Jan 2020", - "description": "January abbreviation" - }, - { - "input": {"likelyYear": 2020, "likelyMonth": 2, "likelyDay": None}, - "expected": "Feb 2020", - "description": "February abbreviation" - }, - { - "input": {"likelyYear": 2020, "likelyMonth": 3, "likelyDay": None}, - "expected": "Mar 2020", - "description": "March abbreviation" - }, - { - "input": {"likelyYear": 2020, "likelyMonth": 4, "likelyDay": None}, - "expected": "Apr 2020", - "description": "April abbreviation" - }, - { - "input": {"likelyYear": 2020, "likelyMonth": 6, "likelyDay": None}, - "expected": "Jun 2020", - "description": "June abbreviation" - }, - { - "input": {"likelyYear": 2020, "likelyMonth": 7, "likelyDay": None}, - "expected": "Jul 2020", - "description": "July abbreviation" - }, - { - "input": {"likelyYear": 2020, "likelyMonth": 8, "likelyDay": None}, - "expected": "Aug 2020", - "description": "August abbreviation" - }, - { - "input": {"likelyYear": 2020, "likelyMonth": 9, "likelyDay": None}, - "expected": "Sep 2020", - "description": "September abbreviation" - }, - { - "input": {"likelyYear": 2020, "likelyMonth": 10, "likelyDay": None}, - "expected": "Oct 2020", - "description": "October abbreviation" - }, - { - "input": {"likelyYear": 2020, "likelyMonth": 11, "likelyDay": None}, - "expected": "Nov 2020", - "description": "November abbreviation" - }, - { - "input": {"likelyYear": 2020, "likelyMonth": 12, "likelyDay": None}, - "expected": "Dec 2020", - "description": "December abbreviation" - } - ] - - @pytest.mark.parametrize("test_case", str_test_cases, ids=lambda x: x['description']) - def test_str_method(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "FlexibleDate.__str__", - "FlexibleDate.toString", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - -class TestReprMethod: - """Test the __repr__ method of FlexibleDate.""" - - repr_test_cases = [ - { - "input": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "expected": "+2020-05-15", - "description": "full date" - }, - { - "input": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None}, - "expected": "+2020-05", - "description": "year and month" - }, - { - "input": {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None}, - "expected": "+2020", - "description": "year only" - }, - { - "input": {"likelyYear": 500, "likelyMonth": None, "likelyDay": None}, - "expected": "+0500", - "description": "year with padding (3 digits)" - }, - { - "input": {"likelyYear": 50, "likelyMonth": None, "likelyDay": None}, - "expected": "+0050", - "description": "year with padding (2 digits)" - }, - { - "input": {"likelyYear": 5, "likelyMonth": None, "likelyDay": None}, - "expected": "+0005", - "description": "year with padding (1 digit)" - }, - { - "input": {"likelyYear": -500, "likelyMonth": None, "likelyDay": None}, - "expected": "-0500", - "description": "BC date (negative year)" - }, - { - "input": {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 5}, - "expected": "+2020-01-05", - "description": "single digit month and day with padding" - }, - { - "input": {"likelyYear": 2020, "likelyMonth": 12, "likelyDay": 31}, - "expected": "+2020-12-31", - "description": "double digit month and day" - }, - { - "input": {"likelyYear": 0, "likelyMonth": None, "likelyDay": None}, - "expected": "0000", - "description": "year 0" - }, - { - "input": {"likelyYear": None, "likelyMonth": 5, "likelyDay": None}, - "expected": "XXXX-05", - "description": "month only (no year, no day)" - } - ] - - @pytest.mark.parametrize("test_case", repr_test_cases, ids=lambda x: x['description']) - def test_repr_method(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "FlexibleDate.__repr__", - "FlexibleDate.inspect", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - -class TestEqualsMethod: - """Test the __equals__ method of FlexibleDate.""" - - equals_test_cases = [ - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15} - ], - "expected": True, - "description": "two identical dates return True" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2021, "likelyMonth": 6, "likelyDay": 20} - ], - "expected": False, - "description": "two different dates return False" - }, - { - "input": [ - {"likelyYear": None, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": None, "likelyMonth": None, "likelyDay": None} - ], - "expected": True, - "description": "two null dates return True" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None} - ], - "expected": False, - "description": "dates with different None values return False" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 20} - ], - "expected": False, - "description": "dates with partial matches return False" - } - ] - - @pytest.mark.parametrize("test_case", equals_test_cases, ids=lambda x: x['description']) - def test_equals_method(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "FlexibleDate.__eq__", - "FlexibleDate.equals", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) diff --git a/tests/test_combine_flexible_dates.py b/tests/test_combine_flexible_dates.py deleted file mode 100644 index bc4621f..0000000 --- a/tests/test_combine_flexible_dates.py +++ /dev/null @@ -1,619 +0,0 @@ -from pathlib import Path -from FlexibleDate.FlexibleDate import ( - FlexibleDate, - combine_flexible_dates, -) -import pytest -from PyScriptTestRunner import PyScriptTestRunner - -runner = PyScriptTestRunner( - Path(__file__).resolve().parent.parent / "dist" / "test_bridge.js", - serializer = lambda d: {"likelyYear": d.likely_year, "likelyMonth": d.likely_month, "likelyDay": d.likely_day}, - deserializer = lambda d: FlexibleDate(likely_day=d["likelyDay"], likely_month=d["likelyMonth"], likely_year=d["likelyYear"]), -) - -runner.add_method(combine_flexible_dates, "combineFlexibleDates") - -class TestBasicCombining: - """Test fundamental combining operations.""" - - basic_cases = [ - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "single full date" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "two identical full dates" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "three identical full dates" - }, - { - "input": [ - {"likelyYear": 1995, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 1995, "likelyMonth": None, "likelyDay": None} - ], - "expected": {"likelyYear": 1995, "likelyMonth": None, "likelyDay": None}, - "description": "two identical year-only dates" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 7, "likelyDay": None} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 7, "likelyDay": None}, - "description": "single year-month date" - } - ] - - @pytest.mark.parametrize("test_case", basic_cases, ids=lambda x: x['description']) - def test_basic_combining(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "combine_flexible_dates", - "combineFlexibleDates", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - -class TestConsensus: - """Test when all or most dates agree.""" - - perfect_consensus_cases = [ - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "four dates all agree on all fields" - }, - { - "input": [ - {"likelyYear": 1995, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 1995, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 1995, "likelyMonth": None, "likelyDay": None} - ], - "expected": {"likelyYear": 1995, "likelyMonth": None, "likelyDay": None}, - "description": "three dates all agree on year only" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None}, - "description": "three dates all agree on year and month" - } - ] - - @pytest.mark.parametrize("test_case", perfect_consensus_cases, ids=lambda x: x['description']) - def test_perfect_consensus(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "combine_flexible_dates", - "combineFlexibleDates", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - majority_cases = [ - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2021, "likelyMonth": 5, "likelyDay": 15} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "3 of 4 agree on year (strong majority)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 6, "likelyDay": 15} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "2 of 3 agree on month" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 16} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "4 of 5 agree on day (overwhelming majority)" - }, - { - "input": [ - {"likelyYear": 1995, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 1995, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 1996, "likelyMonth": None, "likelyDay": None} - ], - "expected": {"likelyYear": 1995, "likelyMonth": None, "likelyDay": None}, - "description": "2 of 3 agree on year-only dates" - } - ] - - @pytest.mark.parametrize("test_case", majority_cases, ids=lambda x: x['description']) - def test_majority_agreement(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "combine_flexible_dates", - "combineFlexibleDates", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - -class TestProximityScoring: - """Test the confidence scoring with nearby values.""" - - proximity_cases = [ - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2021, "likelyMonth": 5, "likelyDay": 15} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "two years 1 apart (proximity effect, first wins tie)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 6, "likelyDay": 15} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "two months 1 apart (proximity effect, first wins tie)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 16} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "two days 1 apart (proximity effect, first wins tie)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 2021, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 2030, "likelyMonth": None, "likelyDay": None} - ], - "expected": {"likelyYear": 2021, "likelyMonth": None, "likelyDay": None}, - "description": "close years get proximity bonus (2020-2021 closer than 2030)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 6, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 12, "likelyDay": None} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 6, "likelyDay": None}, - "description": "close months get proximity bonus (5-6 closer than 12)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 16}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 30} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 16}, - "description": "close days get proximity bonus (15-16 closer than 30)" - } - ] - - @pytest.mark.parametrize("test_case", proximity_cases, ids=lambda x: x['description']) - def test_proximity_scoring(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "combine_flexible_dates", - "combineFlexibleDates", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - -class TestPartialDates: - """Test combining dates with null fields.""" - - partial_date_cases = [ - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "full date with year-only date (non-null wins)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "full date with year-month date (non-null day wins)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None}, - "description": "year-only with year-month (non-null month wins)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 16}, - {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "two full dates with one year-only (consensus on non-nulls)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 6, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None}, - "description": "two year-month with one year-only (month from non-nulls)" - }, - { - "input": [ - {"likelyYear": None, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "complementary nulls (each field from non-null)" - } - ] - - @pytest.mark.parametrize("test_case", partial_date_cases, ids=lambda x: x['description']) - def test_partial_dates(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "combine_flexible_dates", - "combineFlexibleDates", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - -class TestNullValues: - """Test null handling.""" - - null_cases = [ - { - "input": [ - {"likelyYear": None, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": None, "likelyMonth": None, "likelyDay": None} - ], - "expected": {"likelyYear": None, "likelyMonth": None, "likelyDay": None}, - "description": "all fields null across all dates" - }, - { - "input": [ - {"likelyYear": None, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "one fully null, one with values" - }, - { - "input": [ - {"likelyYear": None, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": None, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "two fully null, one with values" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 2021, "likelyMonth": None, "likelyDay": None} - ], - "expected": {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None}, - "description": "month and day null across all dates" - }, - { - "input": [ - {"likelyYear": None, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": None, "likelyMonth": 6, "likelyDay": 16} - ], - "expected": {"likelyYear": None, "likelyMonth": 5, "likelyDay": 15}, - "description": "year null across all dates" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 6, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 7, "likelyDay": None} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 6, "likelyDay": None}, - "description": "day null across all dates" - } - ] - - @pytest.mark.parametrize("test_case", null_cases, ids=lambda x: x['description']) - def test_null_values(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "combine_flexible_dates", - "combineFlexibleDates", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - -class TestTieBreaking: - """Test scenarios where confidence scores might be equal.""" - - tie_cases = [ - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2021, "likelyMonth": 5, "likelyDay": 15} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "even split 1 vs 1 on year (proximity/first wins)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2021, "likelyMonth": 6, "likelyDay": 16}, - {"likelyYear": 2021, "likelyMonth": 6, "likelyDay": 16} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "even split 2 vs 2 (proximity/first group wins)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 2021, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 2022, "likelyMonth": None, "likelyDay": None} - ], - "expected": {"likelyYear": 2021, "likelyMonth": None, "likelyDay": None}, - "description": "three-way tie (proximity cluster wins)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 6, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 12, "likelyDay": None} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 6, "likelyDay": None}, - "description": "three-way tie on months (proximity effect)" - } - ] - - @pytest.mark.parametrize("test_case", tie_cases, ids=lambda x: x['description']) - def test_tie_breaking(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "combine_flexible_dates", - "combineFlexibleDates", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - -class TestMixedPrecision: - """Test combining dates at different levels of precision.""" - - mixed_precision_cases = [ - { - "input": [ - {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "year-only + year-month + full date (each field resolved)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "two year-only with one full date" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 16}, - {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "two full dates with two year-only (fields independent)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 6, "likelyDay": 15} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "year-month dates with different month consensus" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 2021, "likelyMonth": 5, "likelyDay": None}, - {"likelyYear": 2022, "likelyMonth": 5, "likelyDay": 15} - ], - "expected": {"likelyYear": 2021, "likelyMonth": 5, "likelyDay": 15}, - "description": "different years, month appears twice, day once" - } - ] - - @pytest.mark.parametrize("test_case", mixed_precision_cases, ids=lambda x: x['description']) - def test_mixed_precision(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "combine_flexible_dates", - "combineFlexibleDates", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - -class TestEdgeCases: - """Test boundary conditions and edge cases.""" - - edge_cases = [ - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 12, "likelyDay": 31}, - {"likelyYear": 2020, "likelyMonth": 12, "likelyDay": 30} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 12, "likelyDay": 31}, - "description": "end of year dates" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 2, "likelyDay": 29}, - {"likelyYear": 2020, "likelyMonth": 2, "likelyDay": 28} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 2, "likelyDay": 29}, - "description": "leap year date" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 1}, - {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 2} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 1}, - "description": "start of year dates" - }, - { - "input": [ - {"likelyYear": 1850, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15} - ], - "expected": {"likelyYear": 1850, "likelyMonth": 5, "likelyDay": 15}, - "description": "ancient and modern dates" - }, - { - "input": [ - {"likelyYear": -500, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": -499, "likelyMonth": None, "likelyDay": None} - ], - "expected": {"likelyYear": -500, "likelyMonth": None, "likelyDay": None}, - "description": "BC dates" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "many identical dates (10 dates)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 16}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 17}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 18}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 19}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 20} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 17}, - "description": "six consecutive days (proximity clustering)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 2, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 3, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 4, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 6, "likelyDay": None} - ], - "expected": {"likelyYear": 2020, "likelyMonth": 3, "likelyDay": None}, - "description": "six consecutive months (proximity clustering)" - } - ] - - @pytest.mark.parametrize("test_case", edge_cases, ids=lambda x: x['description']) - def test_edge_cases(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "combine_flexible_dates", - "combineFlexibleDates", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - diff --git a/tests/test_compare_dates.py b/tests/test_compare_dates.py deleted file mode 100644 index 996d983..0000000 --- a/tests/test_compare_dates.py +++ /dev/null @@ -1,425 +0,0 @@ -from pathlib import Path -import pytest -from PyScriptTestRunner import PyScriptTestRunner -from FlexibleDate.FlexibleDate import ( - FlexibleDate, - compare_two_dates, -) - -from tests.test_validators import runner - -runner = PyScriptTestRunner( - Path(__file__).resolve().parent.parent / "dist" / "test_bridge.js", - deserializer = lambda d: FlexibleDate(likely_day=d["likelyDay"], likely_month=d["likelyMonth"], likely_year=d["likelyYear"]), -) - -runner.add_method(compare_two_dates, "compareDates", executor=lambda d: compare_two_dates(d[0], d[1])) - -class TestIdenticalDates: - """Test comparison of identical dates returns perfect score of 100.""" - - test_cases = [ - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 15} - ], - "expected": 100, - "description": "identical full dates" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None} - ], - "expected": 100, - "description": "identical year-month dates" - }, - { - "input": [ - {"likelyYear": 1995, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 1995, "likelyMonth": None, "likelyDay": None} - ], - "expected": 100, - "description": "identical year-only dates" - } - ] - - @pytest.mark.parametrize("test_case", test_cases, ids=lambda x: x['description']) - def test_identical_full_dates(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - py_result, ts_result = runner.run( - "compare_two_dates", - "compareDates", - test_data - ) - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - edge_cases = [ - { - "input": [ - {"likelyYear": None, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": None, "likelyMonth": None, "likelyDay": None} - ], - "expected": 100, - "description": "both dates completely null" - }, - { - "input": [ - {"likelyYear": None, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15} - ], - "expected": 100, - "description": "one null date, one valid date" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": None, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None} - ], - "expected": 100, - "description": "no overlapping non-null fields except year" - } - ] - - @pytest.mark.parametrize("test_case", edge_cases, ids=lambda x: x['description']) - def test_edge_cases(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "compare_two_dates", - "compareDates", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - -class TestSimilarDates: - """Test comparison of similar dates.""" - - one_day_different_cases = [ - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 16} - ], - "expected": 97.77778, - "description": "1 day difference, same month and year" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 3, "likelyDay": 10}, - {"likelyYear": 2020, "likelyMonth": 3, "likelyDay": 13} - ], - "expected": 93.33333, - "description": "3 day difference" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 6, "likelyDay": 1}, - {"likelyYear": 2020, "likelyMonth": 6, "likelyDay": 16} - ], - "expected": 66.66667, - "description": "15 day difference (at boundary)" - } - ] - - @pytest.mark.parametrize("test_case", one_day_different_cases, ids=lambda x: x['description']) - def test_one_day_different_cases(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "compare_two_dates", - "compareDates", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - one_month_different_cases = [ - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 6, "likelyDay": None} - ], - "expected": 91.66667, - "description": "1 month difference, year-month dates" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 4, "likelyDay": 15} - ], - "expected": 83.33333, - "description": "3 month difference, same day and year" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 7, "likelyDay": None} - ], - "expected": 50, - "description": "6 month difference (at boundary)" - } - ] - - @pytest.mark.parametrize("test_case", one_month_different_cases, ids=lambda x: x['description']) - def test_one_month_different_cases(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "compare_two_dates", - "compareDates", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - one_year_different_cases = [ - { - "input": [ - {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 2021, "likelyMonth": None, "likelyDay": None} - ], - "expected": 95, - "description": "1 year difference, year-only dates" - }, - { - "input": [ - {"likelyYear": 2015, "likelyMonth": 5, "likelyDay": 10}, - {"likelyYear": 2016, "likelyMonth": 5, "likelyDay": 10} - ], - "expected": 98.33333, - "description": "1 year difference, same month and day" - } - ] - - @pytest.mark.parametrize("test_case", one_year_different_cases, ids=lambda x: x['description']) - def test_one_year_different_cases(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "compare_two_dates", - "compareDates", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - year_only_five_years_different_cases = [ - { - "input": [ - {"likelyYear": 1990, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 1995, "likelyMonth": None, "likelyDay": None} - ], - "expected": 75, - "description": "5 year difference, year-only dates" - }, - { - "input": [ - {"likelyYear": 2000, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 2005, "likelyMonth": None, "likelyDay": None} - ], - "expected": 75, - "description": "5 year difference forward" - }, - { - "input": [ - {"likelyYear": 2010, "likelyMonth": 6, "likelyDay": None}, - {"likelyYear": 2015, "likelyMonth": 6, "likelyDay": None} - ], - "expected": 87.5, - "description": "5 year difference with same month" - } - ] - - @pytest.mark.parametrize("test_case", year_only_five_years_different_cases, ids=lambda x: x['description']) - def test_year_only_five_years_different_cases(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "compare_two_dates", - "compareDates", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - partial_date_comparisons_cases = [ - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": None} - ], - "expected": 100, - "description": "full date vs year-month (matching fields)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15} - ], - "expected": 100, - "description": "year-only vs full date (matching year)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 2025, "likelyMonth": 5, "likelyDay": 15} - ], - "expected": 75, - "description": "year-only vs full date (5 year diff)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 3, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 6, "likelyDay": 15} - ], - "expected": 75, - "description": "year-month vs full date (3 month diff)" - } - ] - - @pytest.mark.parametrize("test_case", partial_date_comparisons_cases, ids=lambda x: x['description']) - def test_partial_date_comparisons_cases(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "compare_two_dates", - "compareDates", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - scoring_boundaries_cases = [ - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 1}, - {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 15} - ], - "expected": 68.88889, - "description": "14 day difference (near boundary)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 1}, - {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 16} - ], - "expected": 66.66667, - "description": "15 day difference (at max_diff boundary)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 1}, - {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 20} - ], - "expected": 66.66667, - "description": "19 day difference (beyond max_diff)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": None}, - {"likelyYear": 2020, "likelyMonth": 6, "likelyDay": None} - ], - "expected": 58.33333, - "description": "5 month difference (near boundary)" - }, - { - "input": [ - {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 2039, "likelyMonth": None, "likelyDay": None} - ], - "expected": 5, - "description": "19 year difference (just before boundary)" - }, - { - "input": [ - {"likelyYear": 2000, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 2010, "likelyMonth": None, "likelyDay": None} - ], - "expected": 50, - "description": "10 year difference (halfway to boundary)" - } - ] - - @pytest.mark.parametrize("test_case", scoring_boundaries_cases, ids=lambda x: x['description']) - def test_scoring_boundaries_cases(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "compare_two_dates", - "compareDates", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}: got {py_result}, expected {test_case['expected']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}: got {ts_result}, expected {test_case['expected']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - -class TestBadDates: - """Test comparison of dates that are too different (score = 0).""" - - very_different_dates_cases = [ - { - "input": [ - {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 2040, "likelyMonth": None, "likelyDay": None} - ], - "expected": 0, - "description": "20 year difference (at boundary)" - }, - { - "input": [ - {"likelyYear": 1990, "likelyMonth": 5, "likelyDay": 15}, - {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15} - ], - "expected": 0, - "description": "30 year difference with same month and day" - }, - { - "input": [ - {"likelyYear": 1950, "likelyMonth": 1, "likelyDay": 1}, - {"likelyYear": 2024, "likelyMonth": 12, "likelyDay": 31} - ], - "expected": 0, - "description": "very different dates in all fields" - }, - { - "input": [ - {"likelyYear": 1800, "likelyMonth": None, "likelyDay": None}, - {"likelyYear": 1900, "likelyMonth": None, "likelyDay": None} - ], - "expected": 0, - "description": "100 year difference" - } - ] - - @pytest.mark.parametrize("test_case", very_different_dates_cases, ids=lambda x: x['description']) - def test_very_different_dates_cases(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "compare_two_dates", - "compareDates", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) \ No newline at end of file diff --git a/tests/test_create_flexible_date.py b/tests/test_create_flexible_date.py deleted file mode 100644 index ec5f59d..0000000 --- a/tests/test_create_flexible_date.py +++ /dev/null @@ -1,343 +0,0 @@ -from pathlib import Path -import pytest -from PyScriptTestRunner import PyScriptTestRunner -from FlexibleDate.FlexibleDate import ( - FlexibleDate, - create_flexible_date, - create_flexible_date_from_formal_date, -) - -runner = PyScriptTestRunner( - Path(__file__).resolve().parent.parent / "dist" / "test_bridge.js", - serializer = lambda d: {"likelyYear": d.likely_year, "likelyMonth": d.likely_month, "likelyDay": d.likely_day}, - deserializer = lambda d: FlexibleDate(likely_day=d.likelyDay, likely_month=d.likelyMonth, likely_year=d.likelyYear), -) - -runner.add_method(create_flexible_date, "createFlexibleDate") -runner.add_method(create_flexible_date_from_formal_date, "createFlexibleDateFromFormalDate") - -class TestCreateFlexibleDate: - """Test FlexibleDate creation from string inputs in both Python and TypeScript.""" - - class TestFullDates: - """Test parsing of complete dates with year, month, and day.""" - - full_date_cases = [ - { - "input": "2023-05-15", - "expected": {"likelyYear": 2023, "likelyMonth": 5, "likelyDay": 15}, - "description": "ISO format" - }, - { - "input": "January 15, 2020", - "expected": {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 15}, - "description": "American format with comma" - }, - { - "input": "15 January 2020", - "expected": {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 15}, - "description": "European format" - }, - { - "input": "2020-01-15", - "expected": {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 15}, - "description": "ISO format dash separated" - }, - { - "input": "01/15/2020", - "expected": {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 15}, - "description": "MM/DD/YYYY format" - }, - { - "input": "15/01/2020", - "expected": {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 15}, - "description": "DD/MM/YYYY format" - }, - { - "input": "Born on March 15, 1990 in New York", - "expected": {"likelyYear": 1990, "likelyMonth": 3, "likelyDay": 15}, - "description": "date embedded in text" - }, - { - "input": "1990, 1991, or 1992", - "expected": {"likelyYear": 1991, "likelyMonth": None, "likelyDay": None}, - "description": "multiple 4-digit years (tests glean_year_month_day scoring)" - } - ] - - @pytest.mark.parametrize("test_case", full_date_cases, ids=lambda x: x['description']) - def test_full_date_parsing(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "create_flexible_date", - "createFlexibleDate", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - class TestPartialDates: - """Test parsing of partial dates (missing day, month, or both).""" - - partial_date_cases = [ - { - "input": "May 2023", - "expected": {"likelyYear": 2023, "likelyMonth": 5, "likelyDay": None}, - "description": "month and year only" - }, - { - "input": "1995", - "expected": {"likelyYear": 1995, "likelyMonth": None, "likelyDay": None}, - "description": "year only" - }, - { - "input": "December", - "expected": {"likelyYear": None, "likelyMonth": 12, "likelyDay": None}, - "description": "month only" - }, - { - "input": "December 12", - "expected": {"likelyYear": None, "likelyMonth": 12, "likelyDay": 12}, - "description": "month and day only" - }, - { - "input": "The event happened sometime in July 2021", - "expected": {"likelyYear": 2021, "likelyMonth": 7, "likelyDay": None}, - "description": "month and year in sentence" - }, - { - "input": "circa 1850s", - "expected": {"likelyYear": 1850, "likelyMonth": None, "likelyDay": None}, - "description": "approximate year with text" - } - ] - - @pytest.mark.parametrize("test_case", partial_date_cases, ids=lambda x: x['description']) - def test_partial_date_parsing(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "create_flexible_date", - "createFlexibleDate", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - class TestNullDates: - """Test handling of null and empty inputs.""" - - null_date_cases = [ - { - "input": None, - "expected": {"likelyYear": None, "likelyMonth": None, "likelyDay": None}, - "description": "null input" - }, - { - "input": "", - "expected": {"likelyYear": None, "likelyMonth": None, "likelyDay": None}, - "description": "empty string" - }, - { - "input": " ", - "expected": {"likelyYear": None, "likelyMonth": None, "likelyDay": None}, - "description": "whitespace only" - } - ] - - @pytest.mark.parametrize("test_case", null_date_cases, ids=lambda x: x['description']) - def test_null_and_empty_inputs(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "create_flexible_date", - "createFlexibleDate", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - class TestInvalidDatesExceptionHandling: - """Test handling of invalid dates that trigger exception handling.""" - - invalid_date_cases = [ - { - "input": "February 30, 2020", - "expected": {"likelyYear": 2020, "likelyMonth": 2, "likelyDay": None}, - "description": "February 31st (invalid) falls back to year-month" - }, - { - "input": "April 31, 2020", - "expected": {"likelyYear": 2020, "likelyMonth": 4, "likelyDay": None}, - "description": "April 31st (invalid) falls back to year-month" - }, - { - "input": "June 31, 2020", - "expected": {"likelyYear": 2020, "likelyMonth": 6, "likelyDay": None}, - "description": "June 31st (invalid) falls back to year-month" - } - ] - - @pytest.mark.parametrize("test_case", invalid_date_cases, ids=lambda x: x['description']) - def test_invalid_date_exception_handling(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "create_flexible_date", - "createFlexibleDate", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - -class TestCreateFlexibleDateFromFormalDate: - """Test FlexibleDate creation from EDTF format strings in both Python and TypeScript.""" - - class TestFullDates: - """Test EDTF parsing of complete dates with year, month, and day.""" - - full_edtf_cases = [ - { - "input": "2020-01-15", - "expected": {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 15}, - "description": "simple EDTF date" - }, - { - "input": "+2020-01-15", - "expected": {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 15}, - "description": "EDTF with plus prefix" - }, - { - "input": "2020-01-01/2020-12-31", - "expected": {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None}, - "description": "date range within year" - }, - { - "input": "+1526-01-01/+2020-12-31", - "expected": {"likelyYear": 1526, "likelyMonth": 1, "likelyDay": 1}, - "description": "long date range with plus" - }, - { - "input": "2020-01-15T10:30:00Z", - "expected": {"likelyYear": 2020, "likelyMonth": 1, "likelyDay": 15}, - "description": "EDTF with time and timezone" - }, - { - "input": "+1910-01-01T00:00:00Z/+1910-12-31T23:59:59Z", - "expected": {"likelyYear": 1910, "likelyMonth": None, "likelyDay": None}, - "description": "datetime range" - } - ] - - @pytest.mark.parametrize("test_case", full_edtf_cases, ids=lambda x: x['description']) - def test_full_edtf_parsing(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "create_flexible_date_from_formal_date", - "createFlexibleDateFromFormalDate", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - class TestPartialDates: - """Test EDTF parsing of partial dates (missing day or month).""" - - partial_edtf_cases = [ - { - "input": "1945", - "expected": {"likelyYear": 1945, "likelyMonth": None, "likelyDay": None}, - "description": "year only EDTF" - }, - { - "input": "1945-05", - "expected": {"likelyYear": 1945, "likelyMonth": 5, "likelyDay": None}, - "description": "year-month EDTF" - }, - { - "input": "1910/1920", - "expected": {"likelyYear": 1910, "likelyMonth": None, "likelyDay": None}, - "description": "year range" - }, - { - "input": "+1945", - "expected": {"likelyYear": 1945, "likelyMonth": None, "likelyDay": None}, - "description": "year only with plus prefix" - }, - { - "input": "A+1850", - "expected": {"likelyYear": 1850, "likelyMonth": None, "likelyDay": None}, - "description": "year only with plus prefix and text" - }, - { - "input": '+1953-01/+1953-12', - "expected": {"likelyYear": 1953, "likelyMonth": None, "likelyDay": None}, - "description": "full year as month range" - } - ] - - @pytest.mark.parametrize("test_case", partial_edtf_cases, ids=lambda x: x['description']) - def test_partial_edtf_parsing(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "create_flexible_date_from_formal_date", - "createFlexibleDateFromFormalDate", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - class TestNullDates: - """Test edge cases and error handling for EDTF parsing.""" - error_cases = [ - { - "input": "invalid-date-format", - "expected": {"error": True}, - "expected_error": True, - "description": "invalid date format" - }, - { - "input": "2023-13-45", - "expected": {"error": True}, - "expected_error": True, - "description": "invalid month/day" - } - ] - - @pytest.mark.parametrize("test_case", error_cases, ids=lambda x: x['description']) - def test_error_handling(self, test_case): - """Test that both implementations handle errors consistently.""" - test_data = { - "input": test_case["input"], - "expected": test_case["expected"], - "expected_error": test_case["expected_error"], - "mocks": {} - } - py_result, ts_result = runner.run( - "create_flexible_date_from_formal_date", - "createFlexibleDateFromFormalDate", - test_data - ) - - # Both should return error dicts with error=True - assert isinstance(py_result, dict) and py_result.get("error") is True, \ - f"Python should raise error for {test_case['description']}, got: {py_result}" - assert isinstance(ts_result, dict) and ts_result.get("error") is True, \ - f"TypeScript should raise error for {test_case['description']}, got: {ts_result}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) diff --git a/tests/test_helper_edge_cases.py b/tests/test_helper_edge_cases.py deleted file mode 100644 index 1343ea5..0000000 --- a/tests/test_helper_edge_cases.py +++ /dev/null @@ -1,329 +0,0 @@ -from pathlib import Path -from FlexibleDate.FlexibleDate import ( - FlexibleDate, - create_flexible_date, -) -import pytest -from PyScriptTestRunner import PyScriptTestRunner - -runner = PyScriptTestRunner( - Path(__file__).resolve().parent.parent / "dist" / "test_bridge.js", - serializer = lambda d: {"likelyYear": d.likely_year, "likelyMonth": d.likely_month, "likelyDay": d.likely_day}, - deserializer = lambda d: FlexibleDate(likely_day=d.likelyDay, likely_month=d.likelyMonth, likely_year=d.likelyYear), -) -runner.add_method(create_flexible_date, "createFlexibleDate") - - -class TestEdgeCases: - """Test edge cases in date parsing and helper functions.""" - - class TestAncientDates: - """Test handling of BC dates and ancient years.""" - - ancient_date_cases = [ - { - "input": "-500", - "expected": {"likelyYear": -500, "likelyMonth": None, "likelyDay": None}, - "description": "BC year with minus sign" - }, - { - "input": "500 BC", - "expected": {"likelyYear": -500, "likelyMonth": None, "likelyDay": None}, - "description": "BC year with BC suffix" - }, - { - "input": "0050", - "expected": {"likelyYear": 50, "likelyMonth": None, "likelyDay": None}, - "description": "year 50 AD with leading zeros" - }, - { - "input": "0005", - "expected": {"likelyYear": 5, "likelyMonth": None, "likelyDay": None}, - "description": "year 5 AD with leading zeros" - }, - { - "input": "99", - "expected": {"likelyYear": 99, "likelyMonth": None, "likelyDay": None}, - "description": "year 99 AD" - }, - { - "input": "5", - "expected": {"likelyYear": 5, "likelyMonth": None, "likelyDay": None}, - "description": "single digit year" - }, - { - "input": "-1000", - "expected": {"likelyYear": -1000, "likelyMonth": None, "likelyDay": None}, - "description": "year 1000 BC" - }, - { - "input": "50", - "expected": {"likelyYear": 50, "likelyMonth": None, "likelyDay": None}, - "description": "year 50 AD (AncientDateTime path)" - }, - { - "input": "-50", - "expected": {"likelyYear": -50, "likelyMonth": None, "likelyDay": None}, - "description": "year 50 BC (AncientDateTime path)" - } - ] - - @pytest.mark.parametrize("test_case", ancient_date_cases, ids=lambda x: x['description']) - def test_ancient_dates(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "create_flexible_date", - "createFlexibleDate", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - class TestTextCleaning: - """Test complex text cleaning scenarios.""" - - text_cleaning_cases = [ - { - "input": "2020/05/15", - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "date with slashes" - }, - { - "input": "2020.05.15", - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "date with dots" - }, - { - "input": "2020_05_15", - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "date with underscores" - }, - { - "input": "2020-05-15 14:30:00", - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "date with time HH:MM:SS" - }, - { - "input": "2020-05-15 14:30", - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "date with time HH:MM" - }, - { - "input": "May 15th, 2020", - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "date with ordinal suffix" - }, - { - "input": "15May2020", - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "date without spaces" - }, - { - "input": " 2020-05-15 ", - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "date with leading/trailing whitespace" - }, - { - "input": "9", - "expected": {"likelyYear": 9, "likelyMonth": None, "likelyDay": None}, - "description": "single digit year (zero-padding)" - }, - { - "input": "85", - "expected": {"likelyYear": 85, "likelyMonth": None, "likelyDay": None}, - "description": "two digit year (zero-padding)" - }, - { - "input": "099", - "expected": {"likelyYear": 99, "likelyMonth": None, "likelyDay": None}, - "description": "three digit year with leading zero (zero-padding)" - }, - { - "input": "1000 bc", - "expected": {"likelyYear": -1000, "likelyMonth": None, "likelyDay": None}, - "description": "year with BC suffix conversion" - } - ] - - @pytest.mark.parametrize("test_case", text_cleaning_cases, ids=lambda x: x['description']) - def test_text_cleaning(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "create_flexible_date", - "createFlexibleDate", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - class TestDecadeAndRanges: - """Test parsing of decades and special formats.""" - - decade_cases = [ - { - "input": "1850s", - "expected": {"likelyYear": 1850, "likelyMonth": None, "likelyDay": None}, - "description": "decade format (1850s)" - }, - { - "input": "circa 1920s", - "expected": {"likelyYear": 1920, "likelyMonth": None, "likelyDay": None}, - "description": "circa with decade" - }, - { - "input": "early 1990s", - "expected": {"likelyYear": 1990, "likelyMonth": None, "likelyDay": None}, - "description": "early with decade" - }, - { - "input": "late 2000s", - "expected": {"likelyYear": 2000, "likelyMonth": None, "likelyDay": None}, - "description": "late with decade" - } - ] - - @pytest.mark.parametrize("test_case", decade_cases, ids=lambda x: x['description']) - def test_decades(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "create_flexible_date", - "createFlexibleDate", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - class TestAMPMHandling: - """Test handling of AM/PM markers in dates.""" - - ampm_cases = [ - { - "input": "May 15, 2020 3:00 PM", - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "date with PM time" - }, - { - "input": "May 15, 2020 9:00 AM", - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "date with AM time" - }, - { - "input": "2020-05-15 11:30 pm", - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "date with lowercase pm" - } - ] - - @pytest.mark.parametrize("test_case", ampm_cases, ids=lambda x: x['description']) - def test_ampm_handling(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "create_flexible_date", - "createFlexibleDate", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - class TestSpecialCharacters: - """Test handling of special characters and quotes.""" - - special_char_cases = [ - { - "input": '"May 15, 2020"', - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "date in double quotes" - }, - { - "input": "'May 15, 2020'", - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "date in single quotes" - }, - { - "input": "May, 15, 2020", - "expected": {"likelyYear": 2020, "likelyMonth": 5, "likelyDay": 15}, - "description": "date with extra commas" - } - ] - - @pytest.mark.parametrize("test_case", special_char_cases, ids=lambda x: x['description']) - def test_special_characters(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "create_flexible_date", - "createFlexibleDate", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - class TestParserEdgeCases: - """Test edge cases in _parse_with_date_util that trigger fallback to gleaning.""" - - parser_edge_cases = [ - { - "input": "0050", - "expected": {"likelyYear": 50, "likelyMonth": None, "likelyDay": None}, - "description": "year 0050 triggers parser exception, falls back to gleaning" - }, - { - "input": "50 bc", - "expected": {"likelyYear": -50, "likelyMonth": None, "likelyDay": None}, - "description": "BC in input triggers parser exception, falls back to gleaning" - } - ] - - @pytest.mark.parametrize("test_case", parser_edge_cases, ids=lambda x: x['description']) - def test_parser_edge_cases(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "create_flexible_date", - "createFlexibleDate", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - class TestComplexDateGleaning: - """Test complex date gleaning scenarios with multiple possibilities.""" - - complex_gleaning_cases = [ - { - "input": "12 13 2020", - "expected": {"likelyYear": 2020, "likelyMonth": 12, "likelyDay": 13}, - "description": "ambiguous day/month numbers (exercises substitution logic)" - } - ] - - @pytest.mark.parametrize("test_case", complex_gleaning_cases, ids=lambda x: x['description']) - def test_complex_gleaning(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "create_flexible_date", - "createFlexibleDate", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - diff --git a/tests/test_validators.py b/tests/test_validators.py deleted file mode 100644 index 8a11db6..0000000 --- a/tests/test_validators.py +++ /dev/null @@ -1,251 +0,0 @@ -from pathlib import Path -import pytest -from PyScriptTestRunner import PyScriptTestRunner -from FlexibleDate.FlexibleDate import FlexibleDate - -runner = PyScriptTestRunner( - Path(__file__).resolve().parent.parent / "dist" / "test_bridge.js", - serializer = lambda d: {"likelyYear": d.likely_year, "likelyMonth": d.likely_month, "likelyDay": d.likely_day}, - deserializer = lambda d: FlexibleDate(likely_day=d["likelyDay"], likely_month=d["likelyMonth"], likely_year=d["likelyYear"]), -) - -def executor(d): - fd = runner.deserializer(d) - if not isinstance(fd, FlexibleDate): - return "ValueError" - return fd - -runner.add_method(FlexibleDate.__init__, "testValidator", executor=executor) - - -class TestYearValidator: - """Test the validate_likely_year validator.""" - - valid_year_cases = [ - { - "input": {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None}, - "expected": {"likelyYear": 2020, "likelyMonth": None, "likelyDay": None}, - "description": "valid positive year" - }, - { - "input": {"likelyYear": -500, "likelyMonth": None, "likelyDay": None}, - "expected": {"likelyYear": -500, "likelyMonth": None, "likelyDay": None}, - "description": "valid negative year (BC)" - }, - { - "input": {"likelyYear": None, "likelyMonth": None, "likelyDay": None}, - "expected": {"likelyYear": None, "likelyMonth": None, "likelyDay": None}, - "description": "None value accepted" - }, - { - "input": {"likelyYear": -100000, "likelyMonth": None, "likelyDay": None}, - "expected": {"likelyYear": -100000, "likelyMonth": None, "likelyDay": None}, - "description": "boundary value -100,000" - }, - { - "input": {"likelyYear": 100000, "likelyMonth": None, "likelyDay": None}, - "expected": {"likelyYear": 100000, "likelyMonth": None, "likelyDay": None}, - "description": "boundary value 100,000" - }, - { - "input": {"likelyYear": 0, "likelyMonth": None, "likelyDay": None}, - "expected": {"likelyYear": 0, "likelyMonth": None, "likelyDay": None}, - "description": "year zero" - } - ] - - @pytest.mark.parametrize("test_case", valid_year_cases, ids=lambda x: x['description']) - def test_valid_years(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "BaseModel.__init__", - "testValidator", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - invalid_year_cases = [ - { - "input": {"likelyYear": -100001, "likelyMonth": None, "likelyDay": None}, - "description": "year below -100,000" - }, - { - "input": {"likelyYear": 100001, "likelyMonth": None, "likelyDay": None}, - "description": "year above 100,000" - }, - { - "input": {"likelyYear": -500000, "likelyMonth": None, "likelyDay": None}, - "description": "very large negative year" - }, - { - "input": {"likelyYear": 500000, "likelyMonth": None, "likelyDay": None}, - "description": "very large positive year" - } - ] - - @pytest.mark.parametrize("test_case", invalid_year_cases, ids=lambda x: x['description']) - def test_invalid_years(self, test_case): - test_data = {"input": test_case["input"], "expected": "ValueError", "mocks": {}} - - py_result, ts_result = runner.run( - "BaseModel.__init__", - "testValidator", - test_data - ) - - assert py_result == "ValueError", f"Python should raise ValueError for {test_case['description']}" - assert ts_result == "ValueError", f"TypeScript should raise ValueError for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - -class TestMonthValidator: - """Test the validate_likely_month validator.""" - - valid_month_cases = [ - { - "input": {"likelyYear": None, "likelyMonth": 1, "likelyDay": None}, - "expected": {"likelyYear": None, "likelyMonth": 1, "likelyDay": None}, - "description": "valid month 1" - }, - { - "input": {"likelyYear": None, "likelyMonth": 6, "likelyDay": None}, - "expected": {"likelyYear": None, "likelyMonth": 6, "likelyDay": None}, - "description": "valid month 6" - }, - { - "input": {"likelyYear": None, "likelyMonth": 12, "likelyDay": None}, - "expected": {"likelyYear": None, "likelyMonth": 12, "likelyDay": None}, - "description": "valid month 12" - }, - { - "input": {"likelyYear": None, "likelyMonth": None, "likelyDay": None}, - "expected": {"likelyYear": None, "likelyMonth": None, "likelyDay": None}, - "description": "None value accepted" - } - ] - - @pytest.mark.parametrize("test_case", valid_month_cases, ids=lambda x: x['description']) - def test_valid_months(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "BaseModel.__init__", - "testValidator", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - invalid_month_cases = [ - { - "input": {"likelyYear": None, "likelyMonth": 0, "likelyDay": None}, - "description": "month = 0" - }, - { - "input": {"likelyYear": None, "likelyMonth": 13, "likelyDay": None}, - "description": "month = 13" - }, - { - "input": {"likelyYear": None, "likelyMonth": -1, "likelyDay": None}, - "description": "month = -1" - }, - { - "input": {"likelyYear": None, "likelyMonth": 100, "likelyDay": None}, - "description": "month = 100" - } - ] - - @pytest.mark.parametrize("test_case", invalid_month_cases, ids=lambda x: x['description']) - def test_invalid_months(self, test_case): - test_data = {"input": test_case["input"], "expected": "ValueError", "mocks": {}} - - py_result, ts_result = runner.run( - "BaseModel.__init__", - "testValidator", - test_data - ) - - assert py_result == "ValueError", f"Python should raise ValueError for {test_case['description']}" - assert ts_result == "ValueError", f"TypeScript should raise ValueError for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - -class TestDayValidator: - """Test the validate_likely_day validator.""" - - valid_day_cases = [ - { - "input": {"likelyYear": None, "likelyMonth": None, "likelyDay": 1}, - "expected": {"likelyYear": None, "likelyMonth": None, "likelyDay": 1}, - "description": "valid day 1" - }, - { - "input": {"likelyYear": None, "likelyMonth": None, "likelyDay": 15}, - "expected": {"likelyYear": None, "likelyMonth": None, "likelyDay": 15}, - "description": "valid day 15" - }, - { - "input": {"likelyYear": None, "likelyMonth": None, "likelyDay": 31}, - "expected": {"likelyYear": None, "likelyMonth": None, "likelyDay": 31}, - "description": "valid day 31" - }, - { - "input": {"likelyYear": None, "likelyMonth": None, "likelyDay": None}, - "expected": {"likelyYear": None, "likelyMonth": None, "likelyDay": None}, - "description": "None value accepted" - } - ] - - @pytest.mark.parametrize("test_case", valid_day_cases, ids=lambda x: x['description']) - def test_valid_days(self, test_case): - test_data = {"input": test_case["input"], "expected": test_case["expected"], "mocks": {}} - - py_result, ts_result = runner.run( - "BaseModel.__init__", - "testValidator", - test_data - ) - - assert py_result == test_case["expected"], f"Python failed for {test_case['description']}" - assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - - invalid_day_cases = [ - { - "input": {"likelyYear": None, "likelyMonth": None, "likelyDay": 0}, - "description": "day = 0" - }, - { - "input": {"likelyYear": None, "likelyMonth": None, "likelyDay": 32}, - "description": "day = 32" - }, - { - "input": {"likelyYear": None, "likelyMonth": None, "likelyDay": -1}, - "description": "day = -1" - }, - { - "input": {"likelyYear": None, "likelyMonth": None, "likelyDay": 100}, - "description": "day = 100" - } - ] - - @pytest.mark.parametrize("test_case", invalid_day_cases, ids=lambda x: x['description']) - def test_invalid_days(self, test_case): - test_data = {"input": test_case["input"], "expected": "ValueError", "mocks": {}} - - py_result, ts_result = runner.run( - "BaseModel.__init__", - "testValidator", - test_data - ) - - assert py_result == "ValueError", f"Python should raise ValueError for {test_case['description']}" - assert ts_result == "ValueError", f"TypeScript should raise ValueError for {test_case['description']}" - runner.assert_strict_parity(py_result, ts_result, test_case['description']) - From 46977a0ffb30b7132cc67ceedf022fee17841bb3 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Wed, 1 Apr 2026 16:31:50 -0600 Subject: [PATCH 17/41] yaml --- .github/.workflows/ci.yaml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/.workflows/ci.yaml diff --git a/.github/.workflows/ci.yaml b/.github/.workflows/ci.yaml new file mode 100644 index 0000000..7632370 --- /dev/null +++ b/.github/.workflows/ci.yaml @@ -0,0 +1,24 @@ +name: CI + +on: + pull_request: + branches: [prd, stg, dev] + paths-ignore: + - "**/README.md" + - "**/.gitignore" + - "**/docs/*" + +jobs: + checkMeds: + name: Check Meds (merge every day) + runs-on: ubuntu-latest + steps: + - name: Check Meds + uses: byuawsfhtl/MedsAction@v1.0.0 + + checkStandard: + name: Python Standard Check + runs-on: ubuntu-latest + steps: + - name: Check Standard + uses: byuawsfhtl/PythonStandardAction@v1.2.0 \ No newline at end of file From fa34ddf5ec7557b4a28223370bf31c1231fdf82e Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Wed, 1 Apr 2026 16:34:32 -0600 Subject: [PATCH 18/41] no dependancies --- package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/package.json b/package.json index 5cd81b3..7090561 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,6 @@ "build": "tsc", "test": "echo \"Error: no test specified\" && exit 1" }, - "dependencies": { - "flexibledatets": "file:../FlexibleDate/FlexibleDateTS" - }, "devDependencies": { "@types/node": "^22.10.1", "typescript": "^5.6.3" From 75164d5adb82fbc631b3bc2a18f940239fd9a75f Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Wed, 1 Apr 2026 16:46:11 -0600 Subject: [PATCH 19/41] delete things --- test_bridge.ts | 64 -------------------------------------------------- tsconfig.json | 16 ------------- 2 files changed, 80 deletions(-) delete mode 100644 test_bridge.ts delete mode 100644 tsconfig.json diff --git a/test_bridge.ts b/test_bridge.ts deleted file mode 100644 index 6785aed..0000000 --- a/test_bridge.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Consumer entry: register domain handlers, then serve JSON-RPC lines from argv. - * FlexibleDate comes from the local package (file:../FlexibleDate/FlexibleDateTS) so - * imports work from dist/ (relative ../ paths would break once compiled into dist/). - */ -import FlexibleDate from "flexibledatets"; -import { PyScriptTestBridge } from "./PyScriptTestBridge"; - -function serializeFlexibleDate(fd: FlexibleDate): any { - if (fd.constructor.name === "FlexibleDate") { - return { - likelyYear: fd.likelyYear, - likelyMonth: fd.likelyMonth, - likelyDay: fd.likelyDay, - }; - } - return fd; -} - -function deserializeFlexibleDate(data: any): FlexibleDate { - if ("likelyDay" in data && - "likelyMonth" in data && - "likelyYear" in data) { - return new FlexibleDate(data.likelyDay, data.likelyMonth, data.likelyYear); - } - return data; -} - -const bridge = new PyScriptTestBridge(serializeFlexibleDate, deserializeFlexibleDate); - -bridge.addMethod("createFlexibleDate", (args) => new FlexibleDate(args[0])); - -bridge.addMethod("createFlexibleDateFromFormalDate", (args) => { - const fd = new FlexibleDate(null, null, null); - return fd.createFlexibleDateFromFormalDate(args[0]); -}); - -bridge.addMethod("compareDates", (args) => args[0].compareDates(args[1])); - -bridge.addMethod("combineFlexibleDates", (args) => { - const dates = args as FlexibleDate[]; - const fdTemp = new FlexibleDate(null, null, null); - return fdTemp.combineFlexibleDates(dates); -}); - -bridge.addMethod("FlexibleDate.toString", (args) => args[0].toString()); - -bridge.addMethod("FlexibleDate.valueOf", (args) => args[0].valueOf()); - -bridge.addMethod("FlexibleDate.inspect", (args) => args[0].inspect()); - -bridge.addMethod("FlexibleDate.equals", (args) => args[0].equals(args[1])); - -bridge.addMethod("testValidator", (args) => { - const [x] = args; - if (x instanceof FlexibleDate) { - return x; - } - return "ValueError"; -}); - -if (require.main === module) { - bridge.runCli(process.argv.slice(2)); -} diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index d3a9e59..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2019", - "module": "commonjs", - "moduleResolution": "node", - "outDir": "dist", - "rootDir": ".", - "strict": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "declaration": true, - "skipLibCheck": true, - "types": ["node"] - }, - "include": ["PyScriptTestBridge.ts", "test_bridge.ts"] -} From f07fc26262cdbde961389abb682e6830b73f81dd Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Wed, 1 Apr 2026 16:46:40 -0600 Subject: [PATCH 20/41] more --- __pycache__/PyScriptTestRunner.cpython-312.pyc | Bin 16052 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 __pycache__/PyScriptTestRunner.cpython-312.pyc diff --git a/__pycache__/PyScriptTestRunner.cpython-312.pyc b/__pycache__/PyScriptTestRunner.cpython-312.pyc deleted file mode 100644 index d00387e653ea58383a84f84ff544e4ee66244b79..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16052 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 Date: Fri, 3 Apr 2026 10:55:31 -0600 Subject: [PATCH 21/41] moved src files and created deploy script --- .github/.workflows/deploy.yaml | 131 ++++++++++++++++++ .../PyScriptTestBridge.ts | 0 .../PyScriptTestRunner.py | 0 3 files changed, 131 insertions(+) create mode 100644 .github/.workflows/deploy.yaml rename PyScriptTestBridge.ts => src/PyScriptTestBridge.ts (100%) rename PyScriptTestRunner.py => src/PyScriptTestRunner.py (100%) diff --git a/.github/.workflows/deploy.yaml b/.github/.workflows/deploy.yaml new file mode 100644 index 0000000..1199ed4 --- /dev/null +++ b/.github/.workflows/deploy.yaml @@ -0,0 +1,131 @@ +name: Deploy + +on: + push: + branches: [prd, dev] + paths-ignore: # Pushes that include only these changed files won't trigger actions + - "**/README.md" + - "**/.gitignore" + - "**/docs/*" + - "**/.github/*" + - "**/tests/*" + - "**/_version.py" + +jobs: + update-version: + name: Update Version + runs-on: ubuntu-latest + permissions: write-all + steps: + - name: Update Version + uses: byuawsfhtl/UpdateVersion@v1.0.9 + with: + token: ${{ secrets.RLL_BOT_PA_TOKEN }} + versionPath: ${{ github.workspace }}/_version.py + + build-python: + name: Build Python distribution 📦 + if: github.ref == 'refs/heads/prd' # Only publish when pushing to prd + needs: update-version + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} # Explicitly checkout the branch that triggered the workflow + # This ensures we're using the latest commit including version updates + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install pypa/build + run: python3 -m pip install build + - name: Build a binary wheel and a source tarball + run: python3 -m build + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + publish-to-pypi: + name: Publish Python 🐍 distribution 📦 to PyPI + if: github.ref == 'refs/heads/prd' # Only publish when pushing to prd + needs: build-python + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/PyScriptTestUtils + permissions: + id-token: write + + steps: + - uses: actions/checkout@v4 # Needed to ensure repo context + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Verify dist exists + run: ls -l dist/ + - name: Publish distribution 📦 to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_ACCESS_TOKEN }} + + build-typescript: + name: Build TypeScript distribution 📦 + if: github.ref == 'refs/heads/prd' # Only publish when pushing to prd + needs: update-version + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18.x' + registry-url: 'https://registry.npmjs.org' + - name: Sync version from _version.py to package.json + run: | + VERSION=$(python3 -c "exec(open('_version.py').read()); print(__version__)") + cd FlexibleDateTS + npm version $VERSION --no-git-tag-version --allow-same-version + - name: Install dependencies + working-directory: ./FlexibleDateTS + run: npm ci + - name: Build TypeScript + working-directory: ./FlexibleDateTS + run: npm run build + - name: Store the TypeScript distribution + uses: actions/upload-artifact@v4 + with: + name: typescript-package-distributions + path: FlexibleDateTS/ + + publish-to-npm: + name: Publish TypeScript 📘 distribution 📦 to npm + if: github.ref == 'refs/heads/prd' # Only publish when pushing to prd + needs: build-typescript + runs-on: ubuntu-latest + + steps: + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18.x' + registry-url: 'https://registry.npmjs.org' + - name: Download TypeScript distribution + uses: actions/download-artifact@v4 + with: + name: typescript-package-distributions + path: FlexibleDateTS/ + - name: Verify dist exists + run: ls -l FlexibleDateTS/dist/ + - name: Publish to npm + working-directory: ./FlexibleDateTS + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # Token will expire every 90 days. Will have to update this token periodically. diff --git a/PyScriptTestBridge.ts b/src/PyScriptTestBridge.ts similarity index 100% rename from PyScriptTestBridge.ts rename to src/PyScriptTestBridge.ts diff --git a/PyScriptTestRunner.py b/src/PyScriptTestRunner.py similarity index 100% rename from PyScriptTestRunner.py rename to src/PyScriptTestRunner.py From 59e78de86486248794522f0fa1e8638816a94caa Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Fri, 3 Apr 2026 11:18:41 -0600 Subject: [PATCH 22/41] deploy scripts --- .github/.workflows/deploy.yaml | 24 ++++++++-------- .gitignore | 6 +++- package-lock.json | 23 --------------- package.json | 4 +++ pyproject.toml | 28 +++++++++++++++++++ setup.cfg | 7 +++++ .../PyScriptTestRunner.py | 17 ++++++++--- src/pyscripttestutils/__init__.py | 5 ++++ tsconfig.json | 19 +++++++++++++ 9 files changed, 94 insertions(+), 39 deletions(-) create mode 100644 pyproject.toml create mode 100644 setup.cfg rename src/{ => pyscripttestutils}/PyScriptTestRunner.py (97%) create mode 100644 src/pyscripttestutils/__init__.py create mode 100644 tsconfig.json diff --git a/.github/.workflows/deploy.yaml b/.github/.workflows/deploy.yaml index 1199ed4..11314b1 100644 --- a/.github/.workflows/deploy.yaml +++ b/.github/.workflows/deploy.yaml @@ -88,22 +88,23 @@ jobs: with: node-version: '18.x' registry-url: 'https://registry.npmjs.org' + - name: Install dependencies + run: npm ci - name: Sync version from _version.py to package.json run: | VERSION=$(python3 -c "exec(open('_version.py').read()); print(__version__)") - cd FlexibleDateTS - npm version $VERSION --no-git-tag-version --allow-same-version - - name: Install dependencies - working-directory: ./FlexibleDateTS - run: npm ci + npm version "$VERSION" --no-git-tag-version --allow-same-version - name: Build TypeScript - working-directory: ./FlexibleDateTS run: npm run build - name: Store the TypeScript distribution uses: actions/upload-artifact@v4 with: name: typescript-package-distributions - path: FlexibleDateTS/ + path: | + dist + package.json + package-lock.json + README.md publish-to-npm: name: Publish TypeScript 📘 distribution 📦 to npm @@ -121,11 +122,12 @@ jobs: uses: actions/download-artifact@v4 with: name: typescript-package-distributions - path: FlexibleDateTS/ - - name: Verify dist exists - run: ls -l FlexibleDateTS/dist/ + path: npm-publish + - name: Verify package layout + working-directory: npm-publish + run: ls -la && ls -l dist/ - name: Publish to npm - working-directory: ./FlexibleDateTS + working-directory: npm-publish run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # Token will expire every 90 days. Will have to update this token periodically. diff --git a/.gitignore b/.gitignore index 2525745..ec8e8ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ node_modules/ .vscode/ -dist/ \ No newline at end of file +dist/ +build/ +*.egg-info/ +__pycache__/ +*.py[cod] \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index bf65ed8..6513fed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,30 +8,11 @@ "name": "pyscripttestutils", "version": "1.0.0", "license": "ISC", - "dependencies": { - "flexibledatets": "file:../FlexibleDate/FlexibleDateTS" - }, "devDependencies": { "@types/node": "^22.10.1", "typescript": "^5.6.3" } }, - "../FlexibleDate/FlexibleDateTS": { - "name": "flexibledatets", - "version": "1.0.5", - "license": "ISC", - "dependencies": { - "any-date-parser": "^1.5.4", - "date-fns": "^2.30.0", - "edtf": "^4.9.0" - }, - "devDependencies": { - "@types/node": "^24.7.2", - "nyc": "^17.1.0", - "ts-node": "^10.9.2", - "typescript": "^5.9.3" - } - }, "node_modules/@types/node": { "version": "22.19.15", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", @@ -42,10 +23,6 @@ "undici-types": "~6.21.0" } }, - "node_modules/flexibledatets": { - "resolved": "../FlexibleDate/FlexibleDateTS", - "link": true - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", diff --git a/package.json b/package.json index 7090561..6d4930f 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,10 @@ "author": "", "type": "commonjs", "main": "dist/PyScriptTestBridge.js", + "types": "dist/PyScriptTestBridge.d.ts", + "files": [ + "dist" + ], "scripts": { "build": "tsc", "test": "echo \"Error: no test specified\" && exit 1" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..63aaf23 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[build-system] +requires = ["setuptools>=61", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "pyscripttestutils" +version = "1.0.0" +description = "Run pytest-style checks concurrently in Python and TypeScript for dual-language libraries" +readme = "README.md" +license = "ISC" +requires-python = ">=3.9" +keywords = ["pytest", "typescript", "testing", "dual-language"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [] + +[project.urls] +Homepage = "https://github.com/byuawsfhtl/PyScriptTestUtils" +Repository = "https://github.com/byuawsfhtl/PyScriptTestUtils.git" +Issues = "https://github.com/byuawsfhtl/PyScriptTestUtils/issues" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..52abda5 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,7 @@ +[options] +package_dir = + = src +packages = find: + +[options.packages.find] +where = src diff --git a/src/PyScriptTestRunner.py b/src/pyscripttestutils/PyScriptTestRunner.py similarity index 97% rename from src/PyScriptTestRunner.py rename to src/pyscripttestutils/PyScriptTestRunner.py index ffd1403..0f82ccd 100644 --- a/src/PyScriptTestRunner.py +++ b/src/pyscripttestutils/PyScriptTestRunner.py @@ -7,6 +7,15 @@ from unittest.mock import patch +def _default_package_root() -> Path: + """Return the directory containing ``package.json``, or this file's directory.""" + here = Path(__file__).resolve().parent + for ancestor in (here, *here.parents): + if (ancestor / "package.json").is_file(): + return ancestor + return here + + @dataclass(frozen=True) class RegisteredMethod: py_fn: Callable[..., Any] @@ -32,12 +41,12 @@ def __init__( assert package_root is None or isinstance(package_root, Path) self.package_root = ( - package_root if package_root is not None else Path(__file__).resolve().parent + package_root if package_root is not None else _default_package_root() ) self.ts_bridge_path = ts_bridge_path - + self.serializer = serializer - + def list_deserializer(d): if isinstance(d, list): try: d = [deserializer(d) for d in d] @@ -45,7 +54,7 @@ def list_deserializer(d): try: return deserializer(d) except: return d self.deserializer = list_deserializer - + self._by_py: Dict[str, RegisteredMethod] = {} self._by_ts: Dict[str, RegisteredMethod] = {} diff --git a/src/pyscripttestutils/__init__.py b/src/pyscripttestutils/__init__.py new file mode 100644 index 0000000..244c157 --- /dev/null +++ b/src/pyscripttestutils/__init__.py @@ -0,0 +1,5 @@ +"""Dual-language test utilities: Python runner plus TypeScript bridge contract.""" + +from .PyScriptTestRunner import PyScriptTestRunner, RegisteredMethod + +__all__ = ["PyScriptTestRunner", "RegisteredMethod"] diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..904726c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "moduleResolution": "node", + "rootDir": "src", + "outDir": "dist", + "strict": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} From 332fc1b7dd7eb0eafa1df78606ad1a7f3410a90d Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Fri, 3 Apr 2026 11:23:44 -0600 Subject: [PATCH 23/41] changed node version --- .github/.workflows/deploy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/.workflows/deploy.yaml b/.github/.workflows/deploy.yaml index 11314b1..d477c2a 100644 --- a/.github/.workflows/deploy.yaml +++ b/.github/.workflows/deploy.yaml @@ -86,7 +86,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: '18.x' + node-version: '22.x' registry-url: 'https://registry.npmjs.org' - name: Install dependencies run: npm ci From 60a3010ac77e52ccd2bda8b07f88c43b1672a175 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Fri, 3 Apr 2026 11:46:39 -0600 Subject: [PATCH 24/41] docs and simplify --- src/PyScriptTestBridge.ts | 5 ++ src/pyscripttestutils/PyScriptTestRunner.py | 70 +++++++++++---------- 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/PyScriptTestBridge.ts b/src/PyScriptTestBridge.ts index 849fc4b..6ed8483 100644 --- a/src/PyScriptTestBridge.ts +++ b/src/PyScriptTestBridge.ts @@ -11,6 +11,11 @@ export interface TestResponse { export type TestMethodHandler = (args: any[]) => any; +/** + * A bridge that allows Python to call TypeScript functions. Constaints a map of method names to executables. + * @param serializer - A function to serialize objects into consistent Json-like structures. + * @param plainDeserializer - A function to deserialize objects into an expected custom class. + */ export class PyScriptTestBridge { constructor( diff --git a/src/pyscripttestutils/PyScriptTestRunner.py b/src/pyscripttestutils/PyScriptTestRunner.py index 0f82ccd..f94be2f 100644 --- a/src/pyscripttestutils/PyScriptTestRunner.py +++ b/src/pyscripttestutils/PyScriptTestRunner.py @@ -307,40 +307,42 @@ def compare_results(self, py_result: Any, ts_result: Any) -> bool: def assert_strict_parity(self, py_result: Any, ts_result: Any, context: str = "") -> None: if not self.compare_results(py_result, ts_result): - error_details = [] + return - if py_result != ts_result: - error_details.append(f"Value mismatch: Python={py_result}, TypeScript={ts_result}") + error_details = [] - if type(py_result) != type(ts_result): - error_details.append( - f"Type mismatch: Python={type(py_result).__name__}, " - f"TypeScript={type(ts_result).__name__}" - ) - error_details.append(f"Python value: {py_result}, TypeScript value: {ts_result}") - - if isinstance(py_result, dict) and isinstance(ts_result, dict): - py_keys = set(py_result.keys()) - ts_keys = set(ts_result.keys()) - - if py_keys != ts_keys: - missing_in_ts = py_keys - ts_keys - missing_in_py = ts_keys - py_keys - if missing_in_ts: - error_details.append(f"Fields missing in TypeScript: {missing_in_ts}") - if missing_in_py: - error_details.append(f"Fields missing in Python: {missing_in_py}") - - for key in py_keys & ts_keys: - if type(py_result[key]) != type(ts_result[key]): - error_details.append( - f"Field '{key}' type mismatch: " - f"Python={type(py_result[key]).__name__}, " - f"TypeScript={type(ts_result[key]).__name__}" - ) - - context_str = f" ({context})" if context else "" - raise AssertionError( - f"Implementation parity check failed{context_str}:\n" - + "\n".join(f" - {detail}" for detail in error_details) + if py_result != ts_result: + error_details.append(f"Value mismatch: Python={py_result}, TypeScript={ts_result}") + + if type(py_result) != type(ts_result): + error_details.append( + f"Type mismatch: Python={type(py_result).__name__}, " + f"TypeScript={type(ts_result).__name__}" ) + error_details.append(f"Python value: {py_result}, TypeScript value: {ts_result}") + + if isinstance(py_result, dict) and isinstance(ts_result, dict): + py_keys = set(py_result.keys()) + ts_keys = set(ts_result.keys()) + + if py_keys != ts_keys: + missing_in_ts = py_keys - ts_keys + missing_in_py = ts_keys - py_keys + if missing_in_ts: + error_details.append(f"Fields missing in TypeScript: {missing_in_ts}") + if missing_in_py: + error_details.append(f"Fields missing in Python: {missing_in_py}") + + for key in py_keys & ts_keys: + if type(py_result[key]) != type(ts_result[key]): + error_details.append( + f"Field '{key}' type mismatch: " + f"Python={type(py_result[key]).__name__}, " + f"TypeScript={type(ts_result[key]).__name__}" + ) + + context_str = f" ({context})" if context else "" + raise AssertionError( + f"Implementation parity check failed{context_str}:\n" + + "\n".join(f" - {detail}" for detail in error_details) + ) From d5de07fddbd0c678c28f2192a064267b21d6e351 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Fri, 3 Apr 2026 11:58:51 -0600 Subject: [PATCH 25/41] outlined test files and made simple classes/functions to test --- tests/utils/bridge.ts | 0 tests/utils/python/CustomClass.py | 28 +++++++++++++++++++++ tests/utils/python/random_funcs.py | 16 ++++++++++++ tests/utils/test_class_functions.py | 0 tests/utils/test_mocking.py | 0 tests/utils/test_standalone_functions.py | 0 tests/utils/ts/CustomClassTS.ts | 32 ++++++++++++++++++++++++ tests/utils/ts/randomFuncs.ts | 17 +++++++++++++ 8 files changed, 93 insertions(+) create mode 100644 tests/utils/bridge.ts create mode 100644 tests/utils/python/CustomClass.py create mode 100644 tests/utils/python/random_funcs.py create mode 100644 tests/utils/test_class_functions.py create mode 100644 tests/utils/test_mocking.py create mode 100644 tests/utils/test_standalone_functions.py create mode 100644 tests/utils/ts/CustomClassTS.ts create mode 100644 tests/utils/ts/randomFuncs.ts diff --git a/tests/utils/bridge.ts b/tests/utils/bridge.ts new file mode 100644 index 0000000..e69de29 diff --git a/tests/utils/python/CustomClass.py b/tests/utils/python/CustomClass.py new file mode 100644 index 0000000..55dacaf --- /dev/null +++ b/tests/utils/python/CustomClass.py @@ -0,0 +1,28 @@ +class CustomClass: + def __init__(self, value: int, my_name: str): + self.value = value + self.my_name = my_name + + def __eq__(self, other: object) -> bool: + if not isinstance(other, CustomClass): + return False + return self.value == other.value and self.my_name == other.my_name + + def __str__(self) -> str: + return f"{self.my_name}: {self.value}" + + def __repr__(self) -> str: + return f"CustomClass(value={self.value}, my_name={self.my_name})" + + def set_value(self, value: int): + self.value = value + + def set_my_name(self, my_name: str): + self.my_name = my_name + + def add_to_val(self, increment: int) -> int: + self.value += increment + return self.value + + def print_name_and_value(self): + print(f"My name is {self.my_name} and my value is {self.value}") \ No newline at end of file diff --git a/tests/utils/python/random_funcs.py b/tests/utils/python/random_funcs.py new file mode 100644 index 0000000..c91903f --- /dev/null +++ b/tests/utils/python/random_funcs.py @@ -0,0 +1,16 @@ +from tests.utils.python.CustomClass import CustomClass + + +def add_two_ints(a: int, b: int) -> int: + return a + b + +def create_custom_class(value: int, my_name: str) -> CustomClass: + return CustomClass(value, my_name) + +def print_custom_class(custom_class: CustomClass): + custom_class.print_name_and_value() + +def add_three_ints(a: int, b: int, c: int) -> int: + step1 = add_two_ints(a, b) + step2 = add_two_ints(step1, c) + return step2 diff --git a/tests/utils/test_class_functions.py b/tests/utils/test_class_functions.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/utils/test_mocking.py b/tests/utils/test_mocking.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/utils/test_standalone_functions.py b/tests/utils/test_standalone_functions.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/utils/ts/CustomClassTS.ts b/tests/utils/ts/CustomClassTS.ts new file mode 100644 index 0000000..0d6b540 --- /dev/null +++ b/tests/utils/ts/CustomClassTS.ts @@ -0,0 +1,32 @@ +class CustomClassTS { + constructor(public value: number, public myName: string) {} + + public equals(other: CustomClassTS): boolean { + return this.value === other.value && this.myName === other.myName; + } + + public toString(): string { + return `${this.myName}: ${this.value}`; + } + + public inspect(): string { + return `CustomClassTS(value=${this.value}, myName=${this.myName})`; + } + + public setValue(value: number): void { + this.value = value; + } + + public setMyName(myName: string): void { + this.myName = myName; + } + + public addToVal(increment: number): number { + this.value += increment; + return this.value; + } + + public printNameAndValue(): void { + console.log(`My name is ${this.myName} and my value is ${this.value}`); + } +} \ No newline at end of file diff --git a/tests/utils/ts/randomFuncs.ts b/tests/utils/ts/randomFuncs.ts new file mode 100644 index 0000000..12a57ea --- /dev/null +++ b/tests/utils/ts/randomFuncs.ts @@ -0,0 +1,17 @@ +function addTwoInts(a: number, b: number): number { + return a + b +} + +function createCustomClass(value: number, my_name: string): CustomClassTS { + return new CustomClassTS(value, my_name) +} + +function printCustomClass(customClass: CustomClassTS): void { + customClass.printNameAndValue() +} + +function addThreeInts(a: number, b: number, c: number): number { + const step1 = addTwoInts(a, b) + const step2 = addTwoInts(step1, c) + return step2 +} \ No newline at end of file From acc53a77984e9a9a434b6dfb412587446f43abfb Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Fri, 3 Apr 2026 12:17:34 -0600 Subject: [PATCH 26/41] added tests --- tests/utils/bridge.ts | 15 ++++++++ tests/utils/package-lock.json | 48 ++++++++++++++++++++++++ tests/utils/package.json | 17 +++++++++ tests/utils/python/random_funcs.py | 2 + tests/utils/test_standalone_functions.py | 36 ++++++++++++++++++ tests/utils/ts/CustomClassTS.ts | 2 +- tests/utils/ts/randomFuncs.ts | 10 ++++- 7 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 tests/utils/package-lock.json create mode 100644 tests/utils/package.json diff --git a/tests/utils/bridge.ts b/tests/utils/bridge.ts index e69de29..cf2890a 100644 --- a/tests/utils/bridge.ts +++ b/tests/utils/bridge.ts @@ -0,0 +1,15 @@ +import { PyScriptTestBridge } from "../../src/PyScriptTestBridge"; + +import { addTwoInts, multiplyByTen } from "./ts/randomFuncs"; + +const bridge = new PyScriptTestBridge(); + +bridge.addMethod("multiplyByTen", (args) => { + return multiplyByTen(args[0]); +}); + +bridge.addMethod("addTwoInts", (args) => { + return addTwoInts(args[0], args[1]); +}); + +export default bridge; \ No newline at end of file diff --git a/tests/utils/package-lock.json b/tests/utils/package-lock.json new file mode 100644 index 0000000..71492e2 --- /dev/null +++ b/tests/utils/package-lock.json @@ -0,0 +1,48 @@ +{ + "name": "utils", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "utils", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@types/node": "^22.10.1", + "typescript": "^5.6.3" + } + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/tests/utils/package.json b/tests/utils/package.json new file mode 100644 index 0000000..1f6598f --- /dev/null +++ b/tests/utils/package.json @@ -0,0 +1,17 @@ +{ + "name": "utils", + "version": "1.0.0", + "description": "", + "license": "ISC", + "author": "", + "type": "commonjs", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "tsc" + }, + "devDependencies": { + "@types/node": "^22.10.1", + "typescript": "^5.6.3" + } +} diff --git a/tests/utils/python/random_funcs.py b/tests/utils/python/random_funcs.py index c91903f..98ae3be 100644 --- a/tests/utils/python/random_funcs.py +++ b/tests/utils/python/random_funcs.py @@ -1,5 +1,7 @@ from tests.utils.python.CustomClass import CustomClass +def multiply_by_ten(a: int) -> int: + return a * 10 def add_two_ints(a: int, b: int) -> int: return a + b diff --git a/tests/utils/test_standalone_functions.py b/tests/utils/test_standalone_functions.py index e69de29..bc22a3a 100644 --- a/tests/utils/test_standalone_functions.py +++ b/tests/utils/test_standalone_functions.py @@ -0,0 +1,36 @@ +from tests.utils.python.random_funcs import * +from src.pyscripttestutils.PyScriptTestRunner import PyScriptTestRunner +from pathlib import Path +import pytest + +runner = PyScriptTestRunner(Path("tests/utils/ts/bridge.ts")) +runner.add_method(multiply_by_ten, "multiplyByTen") +runner.add_method(add_two_ints, "addTwoInts", executor=lambda args: add_two_ints(args[0], args[1])) + +@pytest.mark.parametrize("a, expected", [ + (1, 10), + (2, 20), + (3, 30), +], ids=["1*10=10", "2*10=20", "3*10=30"]) +def test_multiply_by_ten(a, expected): + test_data = { + "input": [a] + } + py_result, ts_result = runner.run("multiply_by_ten", "multiplyByTen", test_data) + assert py_result == expected + assert ts_result == expected + runner.assert_strict_parity(py_result, ts_result) + +@pytest.mark.parametrize("a, b, expected", [ + (1, 2, 3), + (2, 3, 5), + (3, 4, 7), +], ids=["1+2=3", "2+3=5", "3+4=7"]) +def test_add_two_ints(a, b, expected): + test_data = { + "input": [a, b] + } + py_result, ts_result = runner.run("add_two_ints", "addTwoInts", test_data) + assert py_result == expected + assert ts_result == expected + runner.assert_strict_parity(py_result, ts_result) \ No newline at end of file diff --git a/tests/utils/ts/CustomClassTS.ts b/tests/utils/ts/CustomClassTS.ts index 0d6b540..1e3a847 100644 --- a/tests/utils/ts/CustomClassTS.ts +++ b/tests/utils/ts/CustomClassTS.ts @@ -1,4 +1,4 @@ -class CustomClassTS { +export default class CustomClassTS { constructor(public value: number, public myName: string) {} public equals(other: CustomClassTS): boolean { diff --git a/tests/utils/ts/randomFuncs.ts b/tests/utils/ts/randomFuncs.ts index 12a57ea..727dc7d 100644 --- a/tests/utils/ts/randomFuncs.ts +++ b/tests/utils/ts/randomFuncs.ts @@ -1,3 +1,9 @@ +import CustomClassTS from "./CustomClassTS" + +function multiplyByTen(a: number): number { + return a * 10 +} + function addTwoInts(a: number, b: number): number { return a + b } @@ -14,4 +20,6 @@ function addThreeInts(a: number, b: number, c: number): number { const step1 = addTwoInts(a, b) const step2 = addTwoInts(step1, c) return step2 -} \ No newline at end of file +} + +export { multiplyByTen, addTwoInts, createCustomClass, printCustomClass, addThreeInts } \ No newline at end of file From 0ee4fe36b907b82c48b6f532196ef86bca4f4ca8 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Fri, 3 Apr 2026 12:39:21 -0600 Subject: [PATCH 27/41] moved to one node system --- README.md | 3 ++- package.json | 6 +++--- tests/utils/bridge.ts | 4 +++- tests/utils/package.json | 17 ----------------- tests/utils/test_standalone_functions.py | 6 +++++- tsconfig.json | 4 ++-- 6 files changed, 15 insertions(+), 25 deletions(-) delete mode 100644 tests/utils/package.json diff --git a/README.md b/README.md index 15d1a12..740cb61 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ A package that runs pytest-style checks concurrently in Python and TypeScript so ```python runner = PyScriptTestRunner( - "Path/to/test/bridge.ts", + "Path/to/built/test/bridge.js", (Optional) serializer = function to serialize objects into consistent Json-like structures, (Optional) deserializer = function to deserialize objects into an expected custom class. ) @@ -34,6 +34,7 @@ py_result, ts_result = runner.run( Rules: - **`add_method(py_callable, ts_method_name, *, ts_pack_input=False)`** + - `path/to/brige` must be the path to the **built** dist of the ts bridge, usually within a dist/ dir. Ex: `Path(__file__).resolve().parent() / "dist" / bridge.js` - `py_callable` must be a **named** function (not a lambda). The registry key is `py_callable.__name__` (what you pass as the first argument to `run`). - `ts_method_name` must match `addMethod` on the TS side and the JSON `method` field. - `executor` is some exeutable that processes the test data if neccesary. diff --git a/package.json b/package.json index 6d4930f..432753c 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,10 @@ "license": "ISC", "author": "", "type": "commonjs", - "main": "dist/PyScriptTestBridge.js", - "types": "dist/PyScriptTestBridge.d.ts", + "main": "dist/src/PyScriptTestBridge.js", + "types": "dist/src/PyScriptTestBridge.d.ts", "files": [ - "dist" + "dist/src" ], "scripts": { "build": "tsc", diff --git a/tests/utils/bridge.ts b/tests/utils/bridge.ts index cf2890a..814d8da 100644 --- a/tests/utils/bridge.ts +++ b/tests/utils/bridge.ts @@ -12,4 +12,6 @@ bridge.addMethod("addTwoInts", (args) => { return addTwoInts(args[0], args[1]); }); -export default bridge; \ No newline at end of file +if (require.main === module) { + bridge.runCli(); +} \ No newline at end of file diff --git a/tests/utils/package.json b/tests/utils/package.json deleted file mode 100644 index 1f6598f..0000000 --- a/tests/utils/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "utils", - "version": "1.0.0", - "description": "", - "license": "ISC", - "author": "", - "type": "commonjs", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "build": "tsc" - }, - "devDependencies": { - "@types/node": "^22.10.1", - "typescript": "^5.6.3" - } -} diff --git a/tests/utils/test_standalone_functions.py b/tests/utils/test_standalone_functions.py index bc22a3a..403e59a 100644 --- a/tests/utils/test_standalone_functions.py +++ b/tests/utils/test_standalone_functions.py @@ -3,7 +3,11 @@ from pathlib import Path import pytest -runner = PyScriptTestRunner(Path("tests/utils/ts/bridge.ts")) +_REPO_ROOT = Path(__file__).resolve().parent.parent.parent +runner = PyScriptTestRunner( + _REPO_ROOT / "dist" / "tests" / "utils" / "bridge.js", + package_root=_REPO_ROOT, +) runner.add_method(multiply_by_ten, "multiplyByTen") runner.add_method(add_two_ints, "addTwoInts", executor=lambda args: add_two_ints(args[0], args[1])) diff --git a/tsconfig.json b/tsconfig.json index 904726c..486103d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "module": "commonjs", "lib": ["ES2020"], "moduleResolution": "node", - "rootDir": "src", + "rootDir": ".", "outDir": "dist", "strict": true, "skipLibCheck": true, @@ -14,6 +14,6 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true }, - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "tests/utils/bridge.ts", "tests/utils/ts/**/*.ts"], "exclude": ["node_modules", "dist"] } From 84968e030eafac1486c31bec210a43ccb45ab167 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Fri, 3 Apr 2026 12:47:26 -0600 Subject: [PATCH 28/41] first tests pass --- src/pyscripttestutils/PyScriptTestRunner.py | 3 +++ tests/utils/test_standalone_functions.py | 8 ++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pyscripttestutils/PyScriptTestRunner.py b/src/pyscripttestutils/PyScriptTestRunner.py index f94be2f..e5da9b3 100644 --- a/src/pyscripttestutils/PyScriptTestRunner.py +++ b/src/pyscripttestutils/PyScriptTestRunner.py @@ -341,6 +341,9 @@ def assert_strict_parity(self, py_result: Any, ts_result: Any, context: str = "" f"TypeScript={type(ts_result[key]).__name__}" ) + if len(error_details) == 0: + return + context_str = f" ({context})" if context else "" raise AssertionError( f"Implementation parity check failed{context_str}:\n" diff --git a/tests/utils/test_standalone_functions.py b/tests/utils/test_standalone_functions.py index 403e59a..b89ce99 100644 --- a/tests/utils/test_standalone_functions.py +++ b/tests/utils/test_standalone_functions.py @@ -3,11 +3,7 @@ from pathlib import Path import pytest -_REPO_ROOT = Path(__file__).resolve().parent.parent.parent -runner = PyScriptTestRunner( - _REPO_ROOT / "dist" / "tests" / "utils" / "bridge.js", - package_root=_REPO_ROOT, -) +runner = PyScriptTestRunner(Path(__file__).resolve().parent.parent.parent / "dist" / "tests" / "utils" / "bridge.js") runner.add_method(multiply_by_ten, "multiplyByTen") runner.add_method(add_two_ints, "addTwoInts", executor=lambda args: add_two_ints(args[0], args[1])) @@ -18,7 +14,7 @@ ], ids=["1*10=10", "2*10=20", "3*10=30"]) def test_multiply_by_ten(a, expected): test_data = { - "input": [a] + "input": a } py_result, ts_result = runner.run("multiply_by_ten", "multiplyByTen", test_data) assert py_result == expected From 0d4138a84b035dbf7b75dec6e3a0d453e1588084 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Fri, 3 Apr 2026 13:17:30 -0600 Subject: [PATCH 29/41] changed some tests for better coverage --- README.md | 18 +++++---- src/pyscripttestutils/PyScriptTestRunner.py | 2 +- tests/utils/bridge.ts | 29 ++++++++++++++- tests/utils/python/CustomClass.py | 4 +- tests/utils/python/random_funcs.py | 3 -- tests/utils/test_class_functions.py | 41 +++++++++++++++++++++ tests/utils/ts/CustomClassTS.ts | 4 +- tests/utils/ts/randomFuncs.ts | 6 +-- 8 files changed, 85 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 740cb61..f200920 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A package that runs pytest-style checks concurrently in Python and TypeScript so ## Architecture - `**PyScriptTestRunner**` (Python): register one **named** Python function per operation with the TypeScript RPC name your Node bridge expects, then call `run` with the same `test_data` shape as before. -- `**PyScriptTestBridge`** (TypeScript): register handlers with `addMethod(tsName, (args) => response)`. The compiled `**test_bridge_entry.js**` is invoked by the runner via `node`; it parses one JSON request from argv and prints one JSON response. +- `**PyScriptTestBridge`** (TypeScript): register handlers with `addMethod(tsName, (args) => response)`. The compiled `**test_bridge_entry.js`** is invoked by the runner via `node`; it parses one JSON request from argv and prints one JSON response. ## Python: registration and `run` @@ -33,13 +33,13 @@ py_result, ts_result = runner.run( Rules: -- **`add_method(py_callable, ts_method_name, *, ts_pack_input=False)`** +- `**add_method(py_callable, ts_method_name, *, ts_pack_input=False)**` - `path/to/brige` must be the path to the **built** dist of the ts bridge, usually within a dist/ dir. Ex: `Path(__file__).resolve().parent() / "dist" / bridge.js` - `py_callable` must be a **named** function (not a lambda). The registry key is `py_callable.__name__` (what you pass as the first argument to `run`). - `ts_method_name` must match `addMethod` on the TS side and the JSON `method` field. - `executor` is some exeutable that processes the test data if neccesary. - `ts_pack_input=True`: for this operation the runner sends `args: [input_data]` to Node (single array argument). Use this for TS handlers that expect one aggregate argument (for example `combineFlexibleDates` with `const [datesData] = args`). -- **`run(python_name, ts_name, test_data)`** checks that `ts_name` matches the name registered for `python_name`. +- `**run(python_name, ts_name, test_data)`** checks that `ts_name` matches the name registered for `python_name`. - By default, registered Python callables receive a **single** argument: `test_data["input"]`, and returns the (optionally) serialized result of running the callable on the argument. If more arguments are needed, or if other post-processing on the output is needed, provide a test executor. - Serializers and deserializers are optional. If a function is meant to return or take in a custom class, the runner must be provided with (de)serialization function(s), otherwise the data will be treated as raw types (bool, int, float, str, etc...). By default, the deserializer is capable of deserializing lists, so the provided deserialization function only needs to support deserialiation into the given class. The serializer does not support this. @@ -48,6 +48,7 @@ Rules: The test bridge contains all that information neccessary for the python runner to call the TS functions under test. It is instantiated with optional serializer and deserializer parameters like the runner. **Example:** + ```ts function serializeFlexibleDate(fd: FlexibleDate): any { if (fd.constructor.name === "FlexibleDate") { @@ -71,7 +72,7 @@ bridge.addMethod("createFlexibleDate", (args) => new FlexibleDate(args[0])); Rules: -**`addMethod("TSFunctionName, exector)`** +`**addMethod("TSFunctionName, exector)**` - `TSFunctionName` must exactly match an `add_method()` entry on the cooresponding test runner. This is how the runner knows which function to run within the bridge. - `executor` must be the test executor function which runs the function under test. Unlike the runner, the bridge has no default executor, and so an executor must be provided with each `addMethod()` call. @@ -83,10 +84,10 @@ Rules: The subprocess request is `{ "method": string, "args": any[], "mocks": object }`. -- For most operations the runner sets **`args`** from `test_data["input"]` as: +- For most operations the runner sets `**args`** from `test_data["input"]` as: - `[input_data]` when `input_data` is not a `list`, - - or **`input_data` as-is** when it is already a `list`. -- When **`ts_pack_input=True`**, the runner always sends **`args: [input_data]`** (one element), so the TS handler uses `const [x] = args` (for example a list of serialized dates). + - or `**input_data` as-is** when it is already a `list`. +- When `**ts_pack_input=True`**, the runner always sends `**args: [input_data]`** (one element), so the TS handler uses `const [x] = args` (for example a list of serialized dates). Align Python `input_data` in tests with this contract so both sides see the same logical inputs. @@ -135,6 +136,9 @@ class TestIdenticalDates: assert ts_result == test_case["expected"], f"TypeScript failed for {test_case['description']}" runner.assert_strict_parity(py_result, ts_result, test_case['description']) ``` + +**It should be noted** that when testing construction of custom classes, the expected test results should be the **serialized** version of the class, as the runner has no way of converting + ## Developing ```bash diff --git a/src/pyscripttestutils/PyScriptTestRunner.py b/src/pyscripttestutils/PyScriptTestRunner.py index e5da9b3..9f64df9 100644 --- a/src/pyscripttestutils/PyScriptTestRunner.py +++ b/src/pyscripttestutils/PyScriptTestRunner.py @@ -258,7 +258,7 @@ def _call_typescript_function_with_mocks( f"TypeScript function failed: {response.get('error', 'Unknown error')}" ) - return response["result"] + return self.serializer(self.deserializer(response["result"])) except json.JSONDecodeError as e: if expected_error: diff --git a/tests/utils/bridge.ts b/tests/utils/bridge.ts index 814d8da..8498dc4 100644 --- a/tests/utils/bridge.ts +++ b/tests/utils/bridge.ts @@ -1,8 +1,25 @@ import { PyScriptTestBridge } from "../../src/PyScriptTestBridge"; -import { addTwoInts, multiplyByTen } from "./ts/randomFuncs"; +import { addTwoInts, createCustomClass, multiplyByTen } from "./ts/randomFuncs"; +import CustomClassTS from "./ts/CustomClassTS"; -const bridge = new PyScriptTestBridge(); +function serializeCustomClass(customClass: CustomClassTS): any { + if (customClass.constructor.name !== "CustomClassTS") { + return customClass; + } + return { + value: customClass.value, + myName: customClass.myName + }; +} +function deserializeCustomClass(data: any): CustomClassTS { + if (!("value" in data && "myName" in data)) { + return data; + } + return new CustomClassTS(data.value, data.myName); +} + +const bridge = new PyScriptTestBridge(serializeCustomClass, deserializeCustomClass); bridge.addMethod("multiplyByTen", (args) => { return multiplyByTen(args[0]); @@ -12,6 +29,14 @@ bridge.addMethod("addTwoInts", (args) => { return addTwoInts(args[0], args[1]); }); +bridge.addMethod("createCustomClass", (args) => { + return createCustomClass(args[0], args[1]); +}); + +bridge.addMethod("CustomClassTS.getNameAndValue", (args) => { + return args[0].getNameAndValue(); +}); + if (require.main === module) { bridge.runCli(); } \ No newline at end of file diff --git a/tests/utils/python/CustomClass.py b/tests/utils/python/CustomClass.py index 55dacaf..daffca7 100644 --- a/tests/utils/python/CustomClass.py +++ b/tests/utils/python/CustomClass.py @@ -24,5 +24,5 @@ def add_to_val(self, increment: int) -> int: self.value += increment return self.value - def print_name_and_value(self): - print(f"My name is {self.my_name} and my value is {self.value}") \ No newline at end of file + def get_name_and_value(self) -> str: + return f"My name is {self.my_name} and my value is {self.value}" \ No newline at end of file diff --git a/tests/utils/python/random_funcs.py b/tests/utils/python/random_funcs.py index 98ae3be..b95dcc1 100644 --- a/tests/utils/python/random_funcs.py +++ b/tests/utils/python/random_funcs.py @@ -9,9 +9,6 @@ def add_two_ints(a: int, b: int) -> int: def create_custom_class(value: int, my_name: str) -> CustomClass: return CustomClass(value, my_name) -def print_custom_class(custom_class: CustomClass): - custom_class.print_name_and_value() - def add_three_ints(a: int, b: int, c: int) -> int: step1 = add_two_ints(a, b) step2 = add_two_ints(step1, c) diff --git a/tests/utils/test_class_functions.py b/tests/utils/test_class_functions.py index e69de29..57ec643 100644 --- a/tests/utils/test_class_functions.py +++ b/tests/utils/test_class_functions.py @@ -0,0 +1,41 @@ +import pytest +from pathlib import Path + +from src.pyscripttestutils.PyScriptTestRunner import PyScriptTestRunner +from tests.utils.python.CustomClass import CustomClass +from tests.utils.python.random_funcs import * + +runner = PyScriptTestRunner( + Path(__file__).resolve().parent.parent.parent / "dist" / "tests" / "utils" / "bridge.js", + deserializer=lambda x: CustomClass(x["value"], x["myName"]) +) +runner.add_method(create_custom_class, "createCustomClass", executor=lambda args: create_custom_class(args[0], args[1])) +runner.add_method(CustomClass.get_name_and_value, "CustomClassTS.getNameAndValue") + +@pytest.mark.parametrize("value, name, expected", [ + (1, "test", CustomClass(1, "test")), + (2, "test2", CustomClass(2, "test2")), + (3, "test3", CustomClass(3, "test3")), +], ids=["1, test", "2, test2", "3, test3"]) +def test_create_custom_class(value, name, expected): + test_data = { + "input": [value, name] + } + py_result, ts_result = runner.run("create_custom_class", "createCustomClass", test_data) + assert py_result == expected + assert ts_result == expected + runner.assert_strict_parity(py_result, ts_result) + +@pytest.mark.parametrize("custom_class, expected", [ + (CustomClass(1, "test"), "test: 1"), + (CustomClass(2, "test2"), "test2: 2"), + (CustomClass(3, "test3"), "test3: 3"), +], ids=["1, test", "2, test2", "3, test3"]) +def test_print_custom_class(custom_class, expected): + test_data = { + "input": custom_class + } + py_result, ts_result = runner.run("print_custom_class", "printCustomClass", test_data) + assert py_result == expected + assert ts_result == expected + runner.assert_strict_parity(py_result, ts_result) \ No newline at end of file diff --git a/tests/utils/ts/CustomClassTS.ts b/tests/utils/ts/CustomClassTS.ts index 1e3a847..00b0fd4 100644 --- a/tests/utils/ts/CustomClassTS.ts +++ b/tests/utils/ts/CustomClassTS.ts @@ -26,7 +26,7 @@ export default class CustomClassTS { return this.value; } - public printNameAndValue(): void { - console.log(`My name is ${this.myName} and my value is ${this.value}`); + public getNameAndValue(): string { + return `My name is ${this.myName} and my value is ${this.value}`; } } \ No newline at end of file diff --git a/tests/utils/ts/randomFuncs.ts b/tests/utils/ts/randomFuncs.ts index 727dc7d..982b805 100644 --- a/tests/utils/ts/randomFuncs.ts +++ b/tests/utils/ts/randomFuncs.ts @@ -12,14 +12,10 @@ function createCustomClass(value: number, my_name: string): CustomClassTS { return new CustomClassTS(value, my_name) } -function printCustomClass(customClass: CustomClassTS): void { - customClass.printNameAndValue() -} - function addThreeInts(a: number, b: number, c: number): number { const step1 = addTwoInts(a, b) const step2 = addTwoInts(step1, c) return step2 } -export { multiplyByTen, addTwoInts, createCustomClass, printCustomClass, addThreeInts } \ No newline at end of file +export { multiplyByTen, addTwoInts, createCustomClass, addThreeInts } \ No newline at end of file From a3f81f1a9a369c963895f813f2ed4f1deb22e476 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Fri, 3 Apr 2026 13:30:49 -0600 Subject: [PATCH 30/41] more tests --- README.md | 6 +++++- tests/utils/test_class_functions.py | 8 ++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f200920..6727cf7 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,11 @@ class TestIdenticalDates: runner.assert_strict_parity(py_result, ts_result, test_case['description']) ``` -**It should be noted** that when testing construction of custom classes, the expected test results should be the **serialized** version of the class, as the runner has no way of converting +## Notes + +- When testing construction of custom classes, the runner will attempt to deserialize the TS result via the provided deserializer function. This means that expected test results **can** be custom classes. +- When testing custom class methods, the runner cannot deserialize those custom classes, meaning that the input test-data must be provided pre-serialized. + ## Developing diff --git a/tests/utils/test_class_functions.py b/tests/utils/test_class_functions.py index 57ec643..d273e2a 100644 --- a/tests/utils/test_class_functions.py +++ b/tests/utils/test_class_functions.py @@ -27,15 +27,15 @@ def test_create_custom_class(value, name, expected): runner.assert_strict_parity(py_result, ts_result) @pytest.mark.parametrize("custom_class, expected", [ - (CustomClass(1, "test"), "test: 1"), - (CustomClass(2, "test2"), "test2: 2"), - (CustomClass(3, "test3"), "test3: 3"), + ({"value": 1, "myName": "test"}, "My name is test and my value is 1"), + ({"value": 2, "myName": "test2"}, "My name is test2 and my value is 2"), + ({"value": 3, "myName": "test3"}, "My name is test3 and my value is 3"), ], ids=["1, test", "2, test2", "3, test3"]) def test_print_custom_class(custom_class, expected): test_data = { "input": custom_class } - py_result, ts_result = runner.run("print_custom_class", "printCustomClass", test_data) + py_result, ts_result = runner.run("CustomClass.get_name_and_value", "CustomClassTS.getNameAndValue", test_data) assert py_result == expected assert ts_result == expected runner.assert_strict_parity(py_result, ts_result) \ No newline at end of file From 0b59764a08503688a01be85f6035a057c88db9d0 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Mon, 6 Apr 2026 11:08:12 -0600 Subject: [PATCH 31/41] deleted mocking --- package-lock.json | 4014 ++++++++++++++++++++++++++++++++- package.json | 3 + src/PyScriptTestBridge.ts | 65 +- tests/utils/bridge.ts | 11 +- tests/utils/test_mocking.py | 0 tests/utils/ts/randomFuncs.ts | 4 +- 6 files changed, 4086 insertions(+), 11 deletions(-) delete mode 100644 tests/utils/test_mocking.py diff --git a/package-lock.json b/package-lock.json index 6513fed..69c8e5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,19 +8,3716 @@ "name": "pyscripttestutils", "version": "1.0.0", "license": "ISC", + "dependencies": { + "jest": "^30.3.0" + }, "devDependencies": { "@types/node": "^22.10.1", "typescript": "^5.6.3" } }, - "node_modules/@types/node": { - "version": "22.19.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", - "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", - "dev": true, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "license": "MIT" + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.3.0.tgz", + "integrity": "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==", + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz", + "integrity": "sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==", + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.3.0", + "jest-config": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-resolve-dependencies": "30.3.0", + "jest-runner": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "jest-watcher": "30.3.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", + "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==", + "license": "MIT", + "dependencies": { + "expect": "30.3.0", + "jest-snapshot": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", + "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", + "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@sinonjs/fake-timers": "^15.0.0", + "@types/node": "*", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.3.0.tgz", + "integrity": "sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==", + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/types": "30.3.0", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.3.0.tgz", + "integrity": "sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==", + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz", + "integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==", + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.3.0.tgz", + "integrity": "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==", + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/types": "30.3.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.3.0.tgz", + "integrity": "sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==", + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz", + "integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.0.tgz", + "integrity": "sha512-m2xozxSfCIxjDdvbhIWazlP2i2aha/iUmbl94alpsIbd3iLTfeXgfBVbwyWogB6l++istyGZqamgA/EcqYf+Bg==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz", + "integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==", + "license": "MIT", + "dependencies": { + "@jest/transform": "30.3.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz", + "integrity": "sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==", + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz", + "integrity": "sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==", + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.14.tgz", + "integrity": "sha512-fOVLPAsFTsQfuCkvahZkzq6nf8KvGWanlYoTh0SVA0A/PIUxQGU2AOZAoD95n2gFLVDW/jP6sbGLny95nmEuHA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001784", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001784.tgz", + "integrity": "sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.331", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", + "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==", + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.3.0.tgz", + "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==", + "license": "MIT", + "dependencies": { + "@jest/core": "30.3.0", + "@jest/types": "30.3.0", + "import-local": "^3.2.0", + "jest-cli": "30.3.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.3.0.tgz", + "integrity": "sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==", + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.3.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.3.0.tgz", + "integrity": "sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==", + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "p-limit": "^3.1.0", + "pretty-format": "30.3.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.3.0.tgz", + "integrity": "sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==", + "license": "MIT", + "dependencies": { + "@jest/core": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.3.0.tgz", + "integrity": "sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.3.0", + "@jest/types": "30.3.0", + "babel-jest": "30.3.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-circus": "30.3.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-runner": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "parse-json": "^5.2.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", + "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.3.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.3.0.tgz", + "integrity": "sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==", + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "jest-util": "30.3.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.3.0.tgz", + "integrity": "sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==", + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz", + "integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==", + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "picomatch": "^4.0.3", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.3.0.tgz", + "integrity": "sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==", + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", + "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.3.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.3.0.tgz", + "integrity": "sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.3.0.tgz", + "integrity": "sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==", + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.3.0.tgz", + "integrity": "sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==", + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/environment": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-leak-detector": "30.3.0", + "jest-message-util": "30.3.0", + "jest-resolve": "30.3.0", + "jest-runtime": "30.3.0", + "jest-util": "30.3.0", + "jest-watcher": "30.3.0", + "jest-worker": "30.3.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.3.0.tgz", + "integrity": "sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==", + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/globals": "30.3.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.3.0.tgz", + "integrity": "sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "pretty-format": "30.3.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.3.0.tgz", + "integrity": "sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==", + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.3.0.tgz", + "integrity": "sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==", + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.3.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", + "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.3.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "license": "BSD-3-Clause" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/typescript": { @@ -41,8 +3738,311 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 432753c..69e6995 100644 --- a/package.json +++ b/package.json @@ -25,5 +25,8 @@ "devDependencies": { "@types/node": "^22.10.1", "typescript": "^5.6.3" + }, + "dependencies": { + "jest": "^30.3.0" } } diff --git a/src/PyScriptTestBridge.ts b/src/PyScriptTestBridge.ts index 6ed8483..0109a72 100644 --- a/src/PyScriptTestBridge.ts +++ b/src/PyScriptTestBridge.ts @@ -1,6 +1,17 @@ +// import { jest } from "@jest/globals"; + +// NOTE for future developers: Ideally, this package should somehow support mocking. +// However, I can't currently figure out how to sucessfully identify and mock over helper functions/objects within the FUTs. +// I left the skeletal outline for the mock "structure" in commented-out code in case anyone would like to impliment it in the future. + +// export interface MockSpec { +// return_value?: any; +// } + export interface TestRequest { method: string; args: any[]; + // mocks?: Record; } export interface TestResponse { @@ -9,6 +20,31 @@ export interface TestResponse { error?: string; } +// export class MockContext { +// private readonly specs: Record; + +// constructor(rawMocks: Record = {}) { +// if (typeof rawMocks !== "object" || Array.isArray(rawMocks)) { +// throw new TypeError("rawMocks must be a plain object"); +// } +// this.specs = rawMocks; +// } + +// get any>(name: string, fallback: T): T { +// if (typeof name !== "string" || !name) { +// throw new TypeError("name must be a non-empty string"); +// } +// if (typeof fallback !== "function") { +// throw new TypeError("fallback must be a function"); +// } +// const spec = this.specs[name]; +// if (spec === undefined) { +// return fallback; +// } +// return ((..._args: any[]) => spec.return_value) as T; +// } +// } + export type TestMethodHandler = (args: any[]) => any; /** @@ -30,6 +66,8 @@ export class PyScriptTestBridge { } try { return this.plainDeserializer(data); } catch { return data; } }; + private readonly mockReturns = new Map(); + private readonly mockSideEffects = new Map any>(); addMethod(tsMethodName: string, handler: TestMethodHandler): void { if (!tsMethodName || !String(tsMethodName).trim()) { @@ -41,13 +79,38 @@ export class PyScriptTestBridge { this.handlers.set(tsMethodName, handler); } + // addMock(tsMethodName: string, returnValue: any): void { + // if (!tsMethodName || !String(tsMethodName).trim()) { + // throw new Error("tsMethodName must be non-empty"); + // } + // if (!this.handlers.has(tsMethodName)) { + // throw new Error(`Unknown method: ${tsMethodName}`); + // } + // if (!this.mockReturns.has(tsMethodName)) { + // this.mockReturns.set(tsMethodName, [returnValue]); + // } else { + // this.mockReturns.get(tsMethodName)!.push(returnValue); + // } + // } + + // addMockSideEffect(tsMethodName: string, sideEffect: (...args: any[]) => any): void { + // if (!tsMethodName || !String(tsMethodName).trim()) { + // throw new Error("tsMethodName must be non-empty"); + // } + // if (!this.handlers.has(tsMethodName)) { + // throw new Error(`Unknown method: ${tsMethodName}`); + // } + // this.mockSideEffects.set(tsMethodName, sideEffect); + // } + processRequest(request: TestRequest): TestResponse { const handler = this.handlers.get(request.method); if (!handler) { return { success: false, error: `Unknown method: ${request.method}` }; } try { - const result = handler(this.deserializer(request.args)) + // const mockCtx = new MockContext(request.mocks ?? {}); + const result = handler(this.deserializer(request.args)); try { return {success: true, result: this.serializer(result)}; } catch { diff --git a/tests/utils/bridge.ts b/tests/utils/bridge.ts index 8498dc4..28e497b 100644 --- a/tests/utils/bridge.ts +++ b/tests/utils/bridge.ts @@ -1,6 +1,6 @@ import { PyScriptTestBridge } from "../../src/PyScriptTestBridge"; -import { addTwoInts, createCustomClass, multiplyByTen } from "./ts/randomFuncs"; +import { multiplyByTen, addTwoInts, createCustomClass } from "./ts/randomFuncs"; import CustomClassTS from "./ts/CustomClassTS"; function serializeCustomClass(customClass: CustomClassTS): any { @@ -29,6 +29,15 @@ bridge.addMethod("addTwoInts", (args) => { return addTwoInts(args[0], args[1]); }); +// bridge.addMethod("addThreeInts", (args) => { +// const spy = jest.spyOn(functions, "addTwoInts").mockImplementation(() => 42); +// try { +// return functions.addThreeInts(args[0], args[1], args[2]); +// } finally { +// spy.mockRestore(); +// } +// }); + bridge.addMethod("createCustomClass", (args) => { return createCustomClass(args[0], args[1]); }); diff --git a/tests/utils/test_mocking.py b/tests/utils/test_mocking.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/utils/ts/randomFuncs.ts b/tests/utils/ts/randomFuncs.ts index 982b805..a8aed1c 100644 --- a/tests/utils/ts/randomFuncs.ts +++ b/tests/utils/ts/randomFuncs.ts @@ -14,8 +14,8 @@ function createCustomClass(value: number, my_name: string): CustomClassTS { function addThreeInts(a: number, b: number, c: number): number { const step1 = addTwoInts(a, b) - const step2 = addTwoInts(step1, c) + const step2 = step1 + c return step2 } -export { multiplyByTen, addTwoInts, createCustomClass, addThreeInts } \ No newline at end of file +export { multiplyByTen, addTwoInts, createCustomClass, addThreeInts }; \ No newline at end of file From 2386bf6a88b82416426cbdecc5ef3a907004a798 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Mon, 6 Apr 2026 11:13:35 -0600 Subject: [PATCH 32/41] coverage ignore --- .coveragerc | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..7a88240 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[run] +source = source +omit = tests/* \ No newline at end of file From 8d7bb074275a6306fe442eb7f23f9ea2cd75ca33 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Mon, 6 Apr 2026 11:24:25 -0600 Subject: [PATCH 33/41] yaml changes --- .github/.workflows/ci.yaml | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/.github/.workflows/ci.yaml b/.github/.workflows/ci.yaml index 7632370..e6eae7a 100644 --- a/.github/.workflows/ci.yaml +++ b/.github/.workflows/ci.yaml @@ -16,9 +16,40 @@ jobs: - name: Check Meds uses: byuawsfhtl/MedsAction@v1.0.0 - checkStandard: + standardCheck: name: Python Standard Check runs-on: ubuntu-latest steps: - - name: Check Standard - uses: byuawsfhtl/PythonStandardAction@v1.2.0 \ No newline at end of file + - name: Follow Python Standard + uses: byuawsfhtl/PythonStandardAction@v1.2.0 + + checkTests: + name: Test Coverage Check + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + token: ${{ secrets.RLL_BOT_PA_TOKEN }} + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: npm + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e . + npm ci + + - name: Check Test Coverage + uses: byuawsfhtl/TestCoverageAction@v1.0.0 + with: + exclude_paths: 'tests/*' \ No newline at end of file From 51344f12886cda66775b90a68e5b8e085ecf2a71 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Mon, 6 Apr 2026 11:35:57 -0600 Subject: [PATCH 34/41] renamed to they actually run. --- .github/{.workflows => workflows}/ci.yaml | 0 .github/{.workflows => workflows}/deploy.yaml | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename .github/{.workflows => workflows}/ci.yaml (100%) rename .github/{.workflows => workflows}/deploy.yaml (100%) diff --git a/.github/.workflows/ci.yaml b/.github/workflows/ci.yaml similarity index 100% rename from .github/.workflows/ci.yaml rename to .github/workflows/ci.yaml diff --git a/.github/.workflows/deploy.yaml b/.github/workflows/deploy.yaml similarity index 100% rename from .github/.workflows/deploy.yaml rename to .github/workflows/deploy.yaml From 35068c1398d3d883c825df90bd43b5b0a19761e5 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Mon, 6 Apr 2026 11:43:25 -0600 Subject: [PATCH 35/41] standard stuff --- .standardignore | 1 + src/pyscripttestutils/PyScriptTestRunner.py | 119 +++++++++++++++++--- 2 files changed, 103 insertions(+), 17 deletions(-) create mode 100644 .standardignore diff --git a/.standardignore b/.standardignore new file mode 100644 index 0000000..20b097e --- /dev/null +++ b/.standardignore @@ -0,0 +1 @@ +tests/* \ No newline at end of file diff --git a/src/pyscripttestutils/PyScriptTestRunner.py b/src/pyscripttestutils/PyScriptTestRunner.py index 9f64df9..0c9e681 100644 --- a/src/pyscripttestutils/PyScriptTestRunner.py +++ b/src/pyscripttestutils/PyScriptTestRunner.py @@ -31,12 +31,22 @@ class PyScriptTestRunner: _setup_lock = threading.Lock() def __init__( + self, ts_bridge_path: Path, package_root: Optional[Path] = None, serializer: Callable[[Any], Any] = lambda d: d, deserializer: Callable[[Any], Any] = lambda d: d ) -> None: + """ + Initialize the test runner. + + Args: + ts_bridge_path: The path to the TypeScript bridge. + package_root: The root of the package. + serializer: A function to serialize objects into consistent Json-like structures. + deserializer: A function to deserialize objects into an expected custom class. + """ assert isinstance(ts_bridge_path, Path) assert package_root is None or isinstance(package_root, Path) @@ -71,6 +81,15 @@ def add_method( *, ts_pack_input: bool = False, ) -> None: + """ + Add a method to the test runner. + + Args: + py_callable: The Python callable to add. + ts_method_name: The name of the TypeScript method to add. + executor: An optional executor function to process the test data. + ts_pack_input: Whether to pack the input data into a single array. + """ assert callable(py_callable) if not ts_method_name or not str(ts_method_name).strip(): raise ValueError("ts_method_name must be non-empty") @@ -98,6 +117,17 @@ def run( ts_function: str, test_data: Dict[str, Any], ) -> tuple[Any, Any]: + """ + Run the test. + + Args: + python_function: The name of the Python function to run. + ts_function: The name of the TypeScript function to run. + test_data: The test data to run. + + Returns: + A tuple containing the Python and TypeScript results. + """ rec = self._by_py.get(python_function) if rec is None: raise ValueError(f"Unknown Python function: {python_function!r}") @@ -114,11 +144,11 @@ def run( py_result_holder: Dict[str, Any] = {} ts_result_holder: Dict[str, Any] = {} - py_result_holder["result"] = self._call_python_function_with_mocks( + py_result_holder["result"] = self._call_python_function( python_function, input_data, mocks.get("python", {}), expected_error ) - ts_result_holder["result"] = self._call_typescript_function_with_mocks( + ts_result_holder["result"] = self._call_typescript_function( ts_function, input_data, mocks.get("typescript", {}), expected_error ) @@ -128,6 +158,9 @@ def run( return py_result, ts_result def _setup_environment(self) -> None: + """ + Setup the dual-language testing environment. + """ print("Setting up dual-language testing environment...") if not self.ts_bridge_path.exists(): @@ -144,6 +177,9 @@ def _setup_environment(self) -> None: print("Environment setup complete.") def _check_nodejs(self) -> bool: + """ + Check if Node.js is installed. + """ try: result = subprocess.run( ["node", "--version"], capture_output=True, text=True @@ -153,6 +189,9 @@ def _check_nodejs(self) -> bool: return False def _compile_typescript(self) -> None: + """ + Compile the TypeScript code. + """ ts_dir = self.package_root try: print("Installing TypeScript dependencies...") @@ -176,13 +215,29 @@ def _compile_typescript(self) -> None: "After installation, restart your terminal/IDE and try again." ) from exc - def _call_python_function_with_mocks( + def _call_python_function( self, function_name: str, input_data: Any, mocks: Dict[str, Any], expected_error: bool = False, ) -> Any: + """ + Call the Python function. + + Args: + function_name: The name of the Python function to call. + input_data: The input data to pass to the Python function. + mocks: The mocks to use for the Python function. + expected_error: Whether to expect an error from the Python function. + + Returns: + The result of the Python function. + + Raises: + ValueError: If the Python function is not found. + RuntimeError: If the Python function fails. + """ rec = self._by_py.get(function_name) if rec is None: raise ValueError(f"Unknown Python function: {function_name!r}") @@ -211,13 +266,31 @@ def _call_python_function_with_mocks( return {"error": True, "error_type": type(e).__name__, "error_message": str(e)} raise RuntimeError(f"Python function {function_name} failed: {str(e)}") from e - def _call_typescript_function_with_mocks( + def _call_typescript_function( self, function_name: str, input_data: Any, mocks: Dict[str, Any], expected_error: bool = False, ) -> Any: + """ + Call the TypeScript function. + + Args: + function_name: The name of the TypeScript function to call. + input_data: The input data to pass to the TypeScript function. + mocks: The mocks to use for the TypeScript function. + expected_error: Whether to expect an error from the TypeScript function. + + Returns: + The result of the TypeScript function. + + Raises: + ValueError: If the TypeScript function is not found. + RuntimeError: If the TypeScript function fails. + json.JSONDecodeError: If the TypeScript response is not valid JSON. + Exception: If the TypeScript function fails for any other reason. + """ rec = self._by_ts.get(function_name) if rec is None: raise ValueError(f"Unknown TypeScript function: {function_name!r}") @@ -249,11 +322,7 @@ def _call_typescript_function_with_mocks( if not response.get("success", False): if expected_error: - return { - "error": True, - "error_type": "Error", - "error_message": response.get("error", "Unknown error"), - } + return {"error": True, "error_type": "Error", "error_message": response.get("error", "Unknown error")} raise RuntimeError( f"TypeScript function failed: {response.get('error', 'Unknown error')}" ) @@ -270,6 +339,16 @@ def _call_typescript_function_with_mocks( raise RuntimeError(f"TypeScript function {function_name} failed: {str(e)}") from e def compare_results(self, py_result: Any, ts_result: Any) -> bool: + """ + Compare the Python and TypeScript results. + + Args: + py_result: The Python result to compare. + ts_result: The TypeScript result to compare. + + Returns: + True if the results are equal, False otherwise. + """ if isinstance(py_result, dict) and isinstance(ts_result, dict): if py_result.get("error") is True and ts_result.get("error") is True: return True @@ -291,9 +370,8 @@ def compare_results(self, py_result: Any, ts_result: Any) -> bool: if type(py_value) != type(ts_value): return False - if isinstance(py_value, dict) and isinstance(ts_value, dict): - if not self.compare_results(py_value, ts_value): - return False + if isinstance(py_value, dict) and isinstance(ts_value, dict) and not self.compare_results(py_value, ts_value): + return False elif isinstance(py_result, list) and isinstance(ts_result, list): if len(py_result) != len(ts_result): @@ -306,6 +384,17 @@ def compare_results(self, py_result: Any, ts_result: Any) -> bool: return True def assert_strict_parity(self, py_result: Any, ts_result: Any, context: str = "") -> None: + """ + Assert that the Python and TypeScript results are strictly equal. + + Args: + py_result: The Python result to compare. + ts_result: The TypeScript result to compare. + context: The context of the comparison. + + Raises: + AssertionError: If the Python and TypeScript results are not strictly equal. + """ if not self.compare_results(py_result, ts_result): return @@ -335,11 +424,7 @@ def assert_strict_parity(self, py_result: Any, ts_result: Any, context: str = "" for key in py_keys & ts_keys: if type(py_result[key]) != type(ts_result[key]): - error_details.append( - f"Field '{key}' type mismatch: " - f"Python={type(py_result[key]).__name__}, " - f"TypeScript={type(ts_result[key]).__name__}" - ) + error_details.append(f"Field '{key}' type mismatch: Python={type(py_result[key]).__name__}, TypeScript={type(ts_result[key]).__name__}") if len(error_details) == 0: return From f1b26c501eab76cc4a0823e62faddd4b06b58f77 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Mon, 6 Apr 2026 12:43:10 -0600 Subject: [PATCH 36/41] mor tests --- .coverage | Bin 0 -> 53248 bytes src/pyscripttestutils/PyScriptTestRunner.py | 68 +-- .../test_py_script_test_runner_coverage.py | 513 ++++++++++++++++++ 3 files changed, 514 insertions(+), 67 deletions(-) create mode 100644 .coverage create mode 100644 tests/utils/test_py_script_test_runner_coverage.py diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..92cf650c26dcbc03cb81ce41f31e3e50178a543e GIT binary patch literal 53248 zcmeI)%WoS+90%~(wQJW-%obIZ6-8C%0%>TH5~x)5B?xT}6_L_M%B5Df>-9L<*4}k@ z$7ymv)Gbma5Pt!P9^lFu36MbQl?z7{IB-FQ3leI}qX_YveR%CQu6n3c)qGce>|+clv<()gV8zL-|<=b={=_?KGNQ-i!P6^qbUsJiS`j%_2 zieQ>s-4fa!<#Sy|rS-k$KI!p>^uuzc0gxEBYcoIcgorZP);;MIm#?Qd&giwOjE5E=`T zVD+|ZtF98LGT7dl@U0aw8y}5^g{R$+sOiiVa^;1IK~1OI1kAbaYOynx%a_N;wQGg2 z8O5~~Kjr(HP*Rgy45P`Rrm%vxpA00A8YGh#=_r}>k@yG|XC|L3&yNo(P8`mhi&wUH z^4WZO*DkFS^$0aCsr$CRLa04y7kFSIc>L_QEV;@b&b!)88K2yiD<9r9DB~&%=A3%W zT|cLXS-vqA>Njc7SNBX`merm#%WsN}nKi3Hqe;theCo0^21hdhR{?7l1-7bQTZZjd zZHCQL%{^&;qXg3J#GXP;g~6$NpY(#jp=ni3Tfw1ZFP~hs0yPQ+lf3GA4Pm*_+(0o- z1awX(;9(vfo|Rrp(+DeqTGZWDrSznDWmPP;)j zcD$+gHIfGortM{k23&fz)7wszT@Q!ajLx72x2OiIkwOwGeyvlSQWyA|ai7r*3j`nl z0SG_<0uX=z1Rwwb2tWV=!zYl@bSm zE1gDA34FWK+6cnElJ>U4y}jX1w_R8G^p^(bG&;@UCwUsvbX-T8CM6zy{(qDiM~zR@ zl3@)35P$##AOHafKmY;|fB*y_0D-PRJ=FKxoqcf;YpsR=lUQ2|JSWe ztUv$)5P$##AOHafKmY;|fB*!BQ6QsbSV?{UuNnU^x?zC;1Rwwb2tWV=5P$##AOHaf zKwt<3GMQ2#`u+bcX8dK`8p0x?NDzPk1Rwwb2tWV=5P$##AOL}35!kP1wZcXH-#dSP z|GU<>`?<@${^PU!H#axG{WZgW*!=PQ1iSj>-(TJR#boY>n?Dz?-uvR_W@ej~Efk)6 zDf<2Y9cJ7&?hFeE$^iigKmY;|fB*y_009U<00Izz0Ao3Nzd+OVEWH5`e*VvPKm`I2 zfB*y_009U<00Izz00bZafngR0uX=z1Rwwb2tWV=5P$##ATUe<8J+(6Km7my{{TCn;K%>~ literal 0 HcmV?d00001 diff --git a/src/pyscripttestutils/PyScriptTestRunner.py b/src/pyscripttestutils/PyScriptTestRunner.py index 0c9e681..c4297f9 100644 --- a/src/pyscripttestutils/PyScriptTestRunner.py +++ b/src/pyscripttestutils/PyScriptTestRunner.py @@ -68,11 +68,6 @@ def list_deserializer(d): self._by_py: Dict[str, RegisteredMethod] = {} self._by_ts: Dict[str, RegisteredMethod] = {} - with PyScriptTestRunner._setup_lock: - if not PyScriptTestRunner._environment_initialized: - self._setup_environment() - PyScriptTestRunner._environment_initialized = True - def add_method( self, py_callable: Callable[..., Any], @@ -157,64 +152,6 @@ def run( return py_result, ts_result - def _setup_environment(self) -> None: - """ - Setup the dual-language testing environment. - """ - print("Setting up dual-language testing environment...") - - if not self.ts_bridge_path.exists(): - if not self._check_nodejs(): - raise EnvironmentError( - "Node.js not found and TypeScript bridge not compiled. " - "Install Node.js to run dual-language tests.\n" - "Download from: https://nodejs.org/" - ) - self._compile_typescript() - else: - print("TypeScript bridge found, skipping compilation.") - - print("Environment setup complete.") - - def _check_nodejs(self) -> bool: - """ - Check if Node.js is installed. - """ - try: - result = subprocess.run( - ["node", "--version"], capture_output=True, text=True - ) - return result.returncode == 0 - except FileNotFoundError: - return False - - def _compile_typescript(self) -> None: - """ - Compile the TypeScript code. - """ - ts_dir = self.package_root - try: - print("Installing TypeScript dependencies...") - result = subprocess.run( - ["npm", "ci"], cwd=str(ts_dir), capture_output=True, text=True - ) - if result.returncode != 0: - raise RuntimeError(f"Failed to install npm dependencies: {result.stderr}") - - print("Compiling TypeScript...") - result = subprocess.run( - ["npm", "run", "build"], cwd=str(ts_dir), capture_output=True, text=True - ) - if result.returncode != 0: - raise RuntimeError(f"Failed to compile TypeScript: {result.stderr}") - - except FileNotFoundError as exc: - raise EnvironmentError( - "npm command not found. Ensure Node.js and npm are installed and on PATH.\n" - "Download from: https://nodejs.org/\n" - "After installation, restart your terminal/IDE and try again." - ) from exc - def _call_python_function( self, function_name: str, @@ -395,12 +332,9 @@ def assert_strict_parity(self, py_result: Any, ts_result: Any, context: str = "" Raises: AssertionError: If the Python and TypeScript results are not strictly equal. """ - if not self.compare_results(py_result, ts_result): - return - error_details = [] - if py_result != ts_result: + if py_result != ts_result or not self.compare_results(py_result, ts_result): error_details.append(f"Value mismatch: Python={py_result}, TypeScript={ts_result}") if type(py_result) != type(ts_result): diff --git a/tests/utils/test_py_script_test_runner_coverage.py b/tests/utils/test_py_script_test_runner_coverage.py new file mode 100644 index 0000000..095f9fd --- /dev/null +++ b/tests/utils/test_py_script_test_runner_coverage.py @@ -0,0 +1,513 @@ +import json +from pathlib import Path +from unittest.mock import Mock, patch + +import pytest + +from src.pyscripttestutils.PyScriptTestRunner import PyScriptTestRunner +from tests.utils.python.CustomClass import CustomClass +from tests.utils.python.random_funcs import ( + add_three_ints, + add_two_ints, + create_custom_class, + multiply_by_ten, +) + +BRIDGE = ( + Path(__file__).resolve().parent.parent.parent + / "dist" / "tests" / "utils" / "bridge.js" +) +SUBPROCESS_PATH = "src.pyscripttestutils.PyScriptTestRunner.subprocess.run" + +# Module-level lambda: __qualname__ is exactly "", which the runner rejects. +_MODULE_LAMBDA = lambda x: x # noqa: E731 + + +def _make_runner(**kwargs) -> PyScriptTestRunner: + return PyScriptTestRunner(BRIDGE, **kwargs) + + +class TestAddMethod: + @pytest.mark.parametrize("bad_name", ["", " ", "\t"]) + def test_add_method_rejects_empty_ts_name(self, bad_name): + runner = _make_runner() + with pytest.raises(ValueError, match="ts_method_name must be non-empty"): + runner.add_method(multiply_by_ten, bad_name) + + + def test_add_method_rejects_lambda(self): + runner = _make_runner() + with pytest.raises(ValueError, match="named function"): + runner.add_method(_MODULE_LAMBDA, "someMethod") + + + def test_add_method_rejects_duplicate_python_key(self): + runner = _make_runner() + runner.add_method(multiply_by_ten, "multiplyByTen") + with pytest.raises(ValueError, match="Duplicate Python"): + runner.add_method(multiply_by_ten, "multiplyByTen2") + + + def test_add_method_rejects_duplicate_ts_name(self): + runner = _make_runner() + runner.add_method(multiply_by_ten, "multiplyByTen") + with pytest.raises(ValueError, match="Duplicate TypeScript"): + runner.add_method(add_two_ints, "multiplyByTen") + + + def test_add_method_stores_executor_and_ts_pack_input(self): + runner = _make_runner() + executor = lambda args: add_two_ints(args[0], args[1]) + runner.add_method(add_two_ints, "addTwoInts", executor=executor, ts_pack_input=True) + rec = runner._by_ts["addTwoInts"] + assert rec.executor is executor + assert rec.ts_pack_input is True + + +class TestRunValidation: + def test_run_rejects_unknown_python_function(self): + runner = _make_runner() + runner.add_method(multiply_by_ten, "multiplyByTen") + with pytest.raises(ValueError, match="Unknown Python function"): + runner.run("no_such_fn", "multiplyByTen", {"input": 5}) + + + def test_run_rejects_ts_name_mismatch(self): + runner = _make_runner() + runner.add_method(multiply_by_ten, "multiplyByTen") + with pytest.raises(ValueError, match="TypeScript name mismatch"): + runner.run("multiply_by_ten", "wrongName", {"input": 5}) + + +class TestCallPythonFunction: + def test_call_python_unknown_fn_raises_value_error(self): + runner = _make_runner() + with pytest.raises(ValueError, match="Unknown Python function"): + runner._call_python_function("no_such_fn", 1, {}) + + + def test_call_python_expected_error_returns_error_dict(self): + def failing_fn(x): + raise ValueError("intentional failure") + + runner = _make_runner() + runner.add_method(failing_fn, "someTs") + result = runner._call_python_function( + failing_fn.__qualname__, 1, {}, expected_error=True + ) + assert result["error"] is True + assert result["error_type"] == "ValueError" + assert "intentional failure" in result["error_message"] + + + def test_call_python_raises_runtime_when_error_not_expected(self): + def raising_fn(x): + raise ValueError("boom") + + runner = _make_runner() + runner.add_method(raising_fn, "someTs2") + with pytest.raises(RuntimeError, match="failed"): + runner._call_python_function( + raising_fn.__qualname__, 1, {}, expected_error=False + ) + + + def test_call_python_serializer_failure_returns_raw_result(self): + def bad_serializer(x): + raise RuntimeError("serializer exploded") + + runner = _make_runner(serializer=bad_serializer) + runner.add_method(multiply_by_ten, "multiplyByTen") + result = runner._call_python_function("multiply_by_ten", 3, {}) + assert result == 30 + + + def test_call_python_executor_invoked_instead_of_py_fn(self): + side_effect = [] + + def recording_executor(args): + side_effect.append(args) + return add_two_ints(args[0], args[1]) + + runner = _make_runner() + runner.add_method(add_two_ints, "addTwoInts", executor=recording_executor) + result = runner._call_python_function("add_two_ints", [3, 4], {}) + assert result == 7 + assert side_effect, "executor was never called" + + + def test_call_python_mock_patches_named_target(self): + runner = _make_runner() + runner.add_method( + add_three_ints, + "addThreeInts", + executor=lambda args: add_three_ints(args[0], args[1], args[2]), + ) + mocks = {"tests.utils.python.random_funcs.add_two_ints": 42} + result = runner._call_python_function("add_three_ints", [1, 2, 3], mocks) + assert result == 42 + + +class TestInitListDeserializerWrapping: + def test_init_list_deserializer_applies_per_element(self): + received = [] + + def capture(arg): + received.append(arg) + return arg + + runner = PyScriptTestRunner( + BRIDGE, + deserializer=lambda x: CustomClass(x["value"], x["myName"]), + ) + runner.add_method(capture, "captureTs") + + runner._call_python_function( + capture.__qualname__, + [{"value": 1, "myName": "a"}, {"value": 2, "myName": "b"}], + {}, + ) + assert received[0] == [CustomClass(1, "a"), CustomClass(2, "b")] + + + def test_init_list_deserializer_falls_back_to_identity_on_non_deserializable_list(self): + received = [] + + def capture(arg): + received.append(arg) + return arg + + runner = PyScriptTestRunner( + BRIDGE, + deserializer=lambda x: CustomClass(x["value"], x["myName"]), + ) + runner.add_method(capture, "captureTs2") + + runner._call_python_function(capture.__qualname__, [1, 2, 3], {}) + assert received[0] == [1, 2, 3] + + +class TestCallTypescriptFunction: + def test_call_typescript_unknown_fn_raises_value_error(self): + runner = _make_runner() + with pytest.raises(ValueError, match="Unknown TypeScript function"): + runner._call_typescript_function("no_such_ts_fn", 1, {}) + + + def test_call_typescript_unknown_bridge_method_with_expected_error(self): + runner = _make_runner() + runner.add_method( + add_three_ints, + "addThreeInts", + executor=lambda args: add_three_ints(args[0], args[1], args[2]), + ) + result = runner._call_typescript_function( + "addThreeInts", [1, 2, 3], {}, expected_error=True + ) + assert result["error"] is True + assert "addThreeInts" in result["error_message"] + + + def test_call_typescript_unknown_bridge_method_raises_without_expected_error(self): + runner = _make_runner() + runner.add_method( + add_three_ints, + "addThreeInts", + executor=lambda args: add_three_ints(args[0], args[1], args[2]), + ) + with pytest.raises(RuntimeError, match="TypeScript function failed"): + runner._call_typescript_function("addThreeInts", [1, 2, 3], {}) + + + def test_call_typescript_nonzero_returncode_raises(self): + runner = _make_runner() + runner.add_method(multiply_by_ten, "multiplyByTen") + mock_proc = Mock(returncode=1, stderr="node crashed", stdout="") + with patch(SUBPROCESS_PATH, return_value=mock_proc): + with pytest.raises(RuntimeError, match="TypeScript bridge failed"): + runner._call_typescript_function("multiplyByTen", 5, {}) + + + def test_call_typescript_invalid_json_swallowed_when_expected_error(self): + runner = _make_runner() + runner.add_method(multiply_by_ten, "multiplyByTen") + mock_proc = Mock(returncode=0, stderr="", stdout="not valid json {{") + with patch(SUBPROCESS_PATH, return_value=mock_proc): + result = runner._call_typescript_function( + "multiplyByTen", 5, {}, expected_error=True + ) + assert result["error"] is True + + + def test_call_typescript_invalid_json_raises_when_not_expected(self): + runner = _make_runner() + runner.add_method(multiply_by_ten, "multiplyByTen") + mock_proc = Mock(returncode=0, stderr="", stdout="not valid json {{") + with patch(SUBPROCESS_PATH, return_value=mock_proc): + with pytest.raises(RuntimeError, match="Failed to parse TypeScript response"): + runner._call_typescript_function("multiplyByTen", 5, {}) + + + def test_call_typescript_subprocess_raises_exception_with_expected_error(self): + runner = _make_runner() + runner.add_method(multiply_by_ten, "multiplyByTen") + with patch(SUBPROCESS_PATH, side_effect=OSError("node not found")): + result = runner._call_typescript_function( + "multiplyByTen", 5, {}, expected_error=True + ) + assert result["error"] is True + assert "node not found" in result["error_message"] + + + def test_call_typescript_subprocess_raises_exception_without_expected_error(self): + runner = _make_runner() + runner.add_method(multiply_by_ten, "multiplyByTen") + with patch(SUBPROCESS_PATH, side_effect=OSError("node not found")): + with pytest.raises(RuntimeError, match="multiplyByTen failed"): + runner._call_typescript_function("multiplyByTen", 5, {}) + + + def test_call_typescript_stderr_triggers_debug_banner(self, capsys): + runner = _make_runner() + runner.add_method(multiply_by_ten, "multiplyByTen") + mock_proc = Mock( + returncode=0, + stderr="console.log debug info", + stdout=json.dumps({"success": True, "result": 50}), + ) + with patch(SUBPROCESS_PATH, return_value=mock_proc): + result = runner._call_typescript_function("multiplyByTen", 5, {}) + assert result == 50 + out = capsys.readouterr().out + assert "TypeScript Debug Output" in out + assert "console.log debug info" in out + + + def test_call_typescript_success_applies_serializer_and_deserializer(self): + runner = PyScriptTestRunner( + BRIDGE, + deserializer=lambda x: CustomClass(x["value"], x["myName"]), + serializer=lambda c: {"value": c.value, "myName": c.my_name}, + ) + runner.add_method( + create_custom_class, + "createCustomClass", + executor=lambda args: create_custom_class(args[0], args[1]), + ) + mock_proc = Mock( + returncode=0, + stderr="", + stdout=json.dumps({"success": True, "result": {"value": 1, "myName": "a"}}), + ) + with patch(SUBPROCESS_PATH, return_value=mock_proc): + result = runner._call_typescript_function("createCustomClass", [1, "a"], {}) + assert result == {"value": 1, "myName": "a"} + + + def test_call_typescript_scalar_input_wrapped_in_list(self): + runner = _make_runner() + runner.add_method(multiply_by_ten, "multiplyByTen") + captured = {} + + def fake_run(cmd, **_kwargs): + captured["args"] = json.loads(cmd[2])["args"] + return Mock(returncode=0, stderr="", stdout=json.dumps({"success": True, "result": 50})) + + with patch(SUBPROCESS_PATH, side_effect=fake_run): + runner._call_typescript_function("multiplyByTen", 5, {}) + + assert captured["args"] == [5] + + + def test_call_typescript_list_input_spread_as_args(self): + runner = _make_runner() + runner.add_method( + add_two_ints, + "addTwoInts", + executor=lambda args: add_two_ints(args[0], args[1]), + ) + captured = {} + + def fake_run(cmd, **_kwargs): + captured["args"] = json.loads(cmd[2])["args"] + return Mock(returncode=0, stderr="", stdout=json.dumps({"success": True, "result": 3})) + + with patch(SUBPROCESS_PATH, side_effect=fake_run): + runner._call_typescript_function("addTwoInts", [1, 2], {}) + + assert captured["args"] == [1, 2] + + + def test_call_typescript_ts_pack_input_wraps_list_into_single_arg(self): + runner = _make_runner() + runner.add_method(multiply_by_ten, "multiplyByTen", ts_pack_input=True) + captured = {} + + def fake_run(cmd, **_kwargs): + captured["args"] = json.loads(cmd[2])["args"] + return Mock(returncode=0, stderr="", stdout=json.dumps({"success": True, "result": 30})) + + with patch(SUBPROCESS_PATH, side_effect=fake_run): + runner._call_typescript_function("multiplyByTen", [3], {}) + + assert captured["args"] == [[3]] + + +class TestCompareResults: + @pytest.fixture() + def runner(self): + return _make_runner() + + + def test_compare_results_both_error_dicts_short_circuits_to_true(self): + runner = _make_runner() + assert runner.compare_results( + {"error": True, "message": "x"}, + {"error": True, "message": "y"}, + ) is True + + + def test_compare_results_unequal_primitives(self, runner): + runner = _make_runner() + assert runner.compare_results(1, 2) is False + + + def test_compare_results_same_value_different_type(self, runner): + runner = _make_runner() + assert runner.compare_results(1, True) is False + + + def test_compare_results_dicts_key_set_mismatch(self, runner): + runner = _make_runner() + assert runner.compare_results({"a": 1}, {"b": 1}) is False + + + def test_compare_results_dicts_nested_type_mismatch(self, runner): + runner = _make_runner() + assert runner.compare_results({"a": 1}, {"a": "1"}) is False + + + def test_compare_results_nested_dicts_value_mismatch(self, runner): + runner = _make_runner() + assert runner.compare_results({"a": {"b": 1}}, {"a": {"b": 2}}) is False + + + def test_compare_results_lists_length_mismatch(self, runner): + runner = _make_runner() + assert runner.compare_results([1], [1, 2]) is False + + + def test_compare_results_lists_element_mismatch(self, runner): + runner = _make_runner() + assert runner.compare_results([1], [2]) is False + + + def test_compare_results_equal_primitives(self, runner): + runner = _make_runner() + assert runner.compare_results(42, 42) is True + + + def test_compare_results_equal_dicts(self, runner): + runner = _make_runner() + assert runner.compare_results({"a": 1, "b": 2}, {"a": 1, "b": 2}) is True + + + def test_compare_results_equal_lists(self, runner): + runner = _make_runner() + assert runner.compare_results([1, 2, 3], [1, 2, 3]) is True + + + def test_compare_results_equal_nested_dicts(self, runner): + runner = _make_runner() + assert runner.compare_results({"a": {"b": 1}}, {"a": {"b": 1}}) is True + + + def test_compare_results_dict_values_python_equal_but_different_type(self, runner): + # {"a": 1} == {"a": True} in Python, so the top-level != check passes, + # but the per-key type check (line 308) must catch the int/bool mismatch. + runner = _make_runner() + assert runner.compare_results({"a": 1}, {"a": True}) is False + + + def test_compare_results_nested_dict_recursive_type_mismatch(self, runner): + # Outer dicts compare equal in Python (1 == True), but recursive check + # on the nested dict surfaces the type mismatch (line 311). + runner = _make_runner() + assert runner.compare_results({"a": {"b": 1}}, {"a": {"b": True}}) is False + + + def test_compare_results_list_elements_recursive_type_mismatch(self, runner): + # [{"a": 1}] == [{"a": True}] in Python, but recursive per-element + # compare_results catches the nested type mismatch (line 319). + runner = _make_runner() + assert runner.compare_results([{"a": 1}], [{"a": True}]) is False + + +class TestAssertStrictParity: + @pytest.fixture() + def runner(self): + return _make_runner() + + + def test_assert_strict_parity_raises_on_value_mismatch(self, runner): + runner = _make_runner() + with pytest.raises(AssertionError, match="Value mismatch"): + runner.assert_strict_parity(1, 2) + + + def test_assert_strict_parity_type_mismatch_populates_details(self, runner): + # 1 == True in Python, so value check passes, but type detail is added. + runner = _make_runner() + with pytest.raises(AssertionError, match="Type mismatch"): + runner.assert_strict_parity(1, True) + + + def test_assert_strict_parity_dict_missing_keys_populates_details(self, runner): + runner = _make_runner() + with pytest.raises(AssertionError, match="Fields missing"): + runner.assert_strict_parity({"a": 1, "b": 2}, {"a": 1}) + + + def test_assert_strict_parity_dict_field_type_mismatch_populates_details(self, runner): + # {"a": 1} == {"a": True} in Python so value mismatch uses compare_results, + # and the dict-field type detail (line 361) is also added. + runner = _make_runner() + with pytest.raises(AssertionError, match="type mismatch"): + runner.assert_strict_parity({"a": 1}, {"a": True}) + + + def test_assert_strict_parity_equal_values_do_not_raise(self, runner): + runner = _make_runner() + runner.assert_strict_parity(42, 42) + + + def test_assert_strict_parity_raises_on_mismatched_error_dicts_with_context(self, runner): + """ + Both-error dicts short-circuit compare_results to True, so assert_strict_parity + proceeds into its own checks, finds a value mismatch, and raises AssertionError. + """ + runner = _make_runner() + with pytest.raises(AssertionError, match="my_ctx"): + runner.assert_strict_parity( + {"error": True, "msg": "x"}, + {"error": True, "msg": "y"}, + context="my_ctx", + ) + + +class TestRunWithExpectedError: + def test_run_with_expected_error_py_succeeds_ts_fails(self): + runner = _make_runner() + runner.add_method( + add_three_ints, + "addThreeInts", + executor=lambda args: add_three_ints(args[0], args[1], args[2]), + ) + py_result, ts_result = runner.run( + "add_three_ints", + "addThreeInts", + {"input": [1, 2, 3], "expected_error": True}, + ) + assert py_result == 6 + assert ts_result["error"] is True + assert "addThreeInts" in ts_result["error_message"] From 0dd90aef5a592870cfd0833d040a46d30f219e3a Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Mon, 6 Apr 2026 12:51:38 -0600 Subject: [PATCH 37/41] standard --- src/pyscripttestutils/PyScriptTestRunner.py | 83 ++++++++++++++++----- 1 file changed, 63 insertions(+), 20 deletions(-) diff --git a/src/pyscripttestutils/PyScriptTestRunner.py b/src/pyscripttestutils/PyScriptTestRunner.py index c4297f9..45bea43 100644 --- a/src/pyscripttestutils/PyScriptTestRunner.py +++ b/src/pyscripttestutils/PyScriptTestRunner.py @@ -8,7 +8,14 @@ def _default_package_root() -> Path: - """Return the directory containing ``package.json``, or this file's directory.""" + """Return the directory containing ``package.json``, or this file's directory. + + Args: + None + + Returns: + The directory containing ``package.json``, or this file's directory. + """ here = Path(__file__).resolve().parent for ancestor in (here, *here.parents): if (ancestor / "package.json").is_file(): @@ -18,6 +25,7 @@ def _default_package_root() -> Path: @dataclass(frozen=True) class RegisteredMethod: + """A registered method to be run by the test runner.""" py_fn: Callable[..., Any] ts_name: str executor: Optional[Callable[..., Any]] = None @@ -47,8 +55,6 @@ def __init__( serializer: A function to serialize objects into consistent Json-like structures. deserializer: A function to deserialize objects into an expected custom class. """ - assert isinstance(ts_bridge_path, Path) - assert package_root is None or isinstance(package_root, Path) self.package_root = ( package_root if package_root is not None else _default_package_root() @@ -57,7 +63,16 @@ def __init__( self.serializer = serializer - def list_deserializer(d): + def list_deserializer(d: Any) -> Any: + """ + Deserialize a list or a single item. + + Args: + d: The list or single item to deserialize. + + Returns: + The deserialized list or single item. + """ if isinstance(d, list): try: d = [deserializer(d) for d in d] except: pass @@ -73,7 +88,6 @@ def add_method( py_callable: Callable[..., Any], ts_method_name: str, executor: Optional[Callable[..., Any]] = None, - *, ts_pack_input: bool = False, ) -> None: """ @@ -85,7 +99,6 @@ def add_method( executor: An optional executor function to process the test data. ts_pack_input: Whether to pack the input data into a single array. """ - assert callable(py_callable) if not ts_method_name or not str(ts_method_name).strip(): raise ValueError("ts_method_name must be non-empty") py_key = getattr(py_callable, "__qualname__", None) or getattr(py_callable, "__name__", None) @@ -297,26 +310,56 @@ def compare_results(self, py_result: Any, ts_result: Any) -> bool: return False if isinstance(py_result, dict) and isinstance(ts_result, dict): - if set(py_result.keys()) != set(ts_result.keys()): - return False + return self._compare_dicts(py_result, ts_result) - for key in py_result.keys(): - py_value = py_result[key] - ts_value = ts_result[key] + elif isinstance(py_result, list) and isinstance(ts_result, list): + return self._compare_lists(py_result, ts_result) - if type(py_value) != type(ts_value): - return False + return True - if isinstance(py_value, dict) and isinstance(ts_value, dict) and not self.compare_results(py_value, ts_value): - return False + def _compare_dicts(self, py_result: Dict[str, Any], ts_result: Dict[str, Any]) -> bool: + """ + Compare two dictionaries. - elif isinstance(py_result, list) and isinstance(ts_result, list): - if len(py_result) != len(ts_result): + Args: + py_result: The Python dictionary to compare. + ts_result: The TypeScript dictionary to compare. + + Returns: + True if the dictionaries are equal, False otherwise. + """ + if set(py_result.keys()) != set(ts_result.keys()): + return False + + for key in py_result.keys(): + py_value = py_result[key] + ts_value = ts_result[key] + + if type(py_value) != type(ts_value): return False - for py_item, ts_item in zip(py_result, ts_result): - if not self.compare_results(py_item, ts_item): - return False + if isinstance(py_value, dict) and isinstance(ts_value, dict) and not self.compare_results(py_value, ts_value): + return False + + return True + + def _compare_lists(self, py_result: list[Any], ts_result: list[Any]) -> bool: + """ + Compare two lists. + + Args: + py_result: The Python list to compare. + ts_result: The TypeScript list to compare. + + Returns: + True if the lists are equal, False otherwise. + """ + if len(py_result) != len(ts_result): + return False + + for py_item, ts_item in zip(py_result, ts_result): + if not self.compare_results(py_item, ts_item): + return False return True From b88849f45d7a74efd6f10ad328bdfcb3651232b1 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Mon, 6 Apr 2026 12:53:22 -0600 Subject: [PATCH 38/41] small test fix --- tests/utils/test_py_script_test_runner_coverage.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/utils/test_py_script_test_runner_coverage.py b/tests/utils/test_py_script_test_runner_coverage.py index 095f9fd..ce960fb 100644 --- a/tests/utils/test_py_script_test_runner_coverage.py +++ b/tests/utils/test_py_script_test_runner_coverage.py @@ -14,8 +14,7 @@ ) BRIDGE = ( - Path(__file__).resolve().parent.parent.parent - / "dist" / "tests" / "utils" / "bridge.js" + Path(__file__).resolve().parent.parent.parent / "dist" / "tests" / "utils" / "bridge.js" ) SUBPROCESS_PATH = "src.pyscripttestutils.PyScriptTestRunner.subprocess.run" From 2dfe5fbd001eda6f2cbfa6bbad69c6a176372ebf Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Mon, 6 Apr 2026 12:57:54 -0600 Subject: [PATCH 39/41] run build --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e6eae7a..819d034 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -48,6 +48,7 @@ jobs: python -m pip install --upgrade pip pip install -e . npm ci + npm run build - name: Check Test Coverage uses: byuawsfhtl/TestCoverageAction@v1.0.0 From a28c4f66f24f20da9d0666aa35d3e73157aec827 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Mon, 6 Apr 2026 16:38:48 -0600 Subject: [PATCH 40/41] Update README.md for improved formatting and clarity in function descriptions --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6727cf7..9c5e2d3 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ A package that runs pytest-style checks concurrently in Python and TypeScript so ## Architecture -- `**PyScriptTestRunner**` (Python): register one **named** Python function per operation with the TypeScript RPC name your Node bridge expects, then call `run` with the same `test_data` shape as before. -- `**PyScriptTestBridge`** (TypeScript): register handlers with `addMethod(tsName, (args) => response)`. The compiled `**test_bridge_entry.js`** is invoked by the runner via `node`; it parses one JSON request from argv and prints one JSON response. +- **`PyScriptTestRunner`** (Python): register one **named** Python function per operation with the TypeScript RPC name your Node bridge expects, then call `run` with the same `test_data` shape as before. +- **`PyScriptTestBridge`** (TypeScript): register handlers with `addMethod(tsName, (args) => response)`. The compiled **`test_bridge_entry.js`** is invoked by the runner via `node`; it parses one JSON request from argv and prints one JSON response. ## Python: registration and `run` @@ -33,13 +33,13 @@ py_result, ts_result = runner.run( Rules: -- `**add_method(py_callable, ts_method_name, *, ts_pack_input=False)**` +- **`add_method(py_callable, ts_method_name, *, ts_pack_input=False)`** - `path/to/brige` must be the path to the **built** dist of the ts bridge, usually within a dist/ dir. Ex: `Path(__file__).resolve().parent() / "dist" / bridge.js` - `py_callable` must be a **named** function (not a lambda). The registry key is `py_callable.__name__` (what you pass as the first argument to `run`). - `ts_method_name` must match `addMethod` on the TS side and the JSON `method` field. - `executor` is some exeutable that processes the test data if neccesary. - `ts_pack_input=True`: for this operation the runner sends `args: [input_data]` to Node (single array argument). Use this for TS handlers that expect one aggregate argument (for example `combineFlexibleDates` with `const [datesData] = args`). -- `**run(python_name, ts_name, test_data)`** checks that `ts_name` matches the name registered for `python_name`. +- **`run(python_name, ts_name, test_data)`** checks that `ts_name` matches the name registered for `python_name`. - By default, registered Python callables receive a **single** argument: `test_data["input"]`, and returns the (optionally) serialized result of running the callable on the argument. If more arguments are needed, or if other post-processing on the output is needed, provide a test executor. - Serializers and deserializers are optional. If a function is meant to return or take in a custom class, the runner must be provided with (de)serialization function(s), otherwise the data will be treated as raw types (bool, int, float, str, etc...). By default, the deserializer is capable of deserializing lists, so the provided deserialization function only needs to support deserialiation into the given class. The serializer does not support this. @@ -72,7 +72,7 @@ bridge.addMethod("createFlexibleDate", (args) => new FlexibleDate(args[0])); Rules: -`**addMethod("TSFunctionName, exector)**` +**`addMethod("TSFunctionName, exector)`** - `TSFunctionName` must exactly match an `add_method()` entry on the cooresponding test runner. This is how the runner knows which function to run within the bridge. - `executor` must be the test executor function which runs the function under test. Unlike the runner, the bridge has no default executor, and so an executor must be provided with each `addMethod()` call. @@ -84,10 +84,10 @@ Rules: The subprocess request is `{ "method": string, "args": any[], "mocks": object }`. -- For most operations the runner sets `**args`** from `test_data["input"]` as: +- For most operations the runner sets **`args`** from `test_data["input"]` as: - `[input_data]` when `input_data` is not a `list`, - - or `**input_data` as-is** when it is already a `list`. -- When `**ts_pack_input=True`**, the runner always sends `**args: [input_data]`** (one element), so the TS handler uses `const [x] = args` (for example a list of serialized dates). + - or **`input_data` as-is** when it is already a `list`. +- When **`ts_pack_input=True`**, the runner always sends **`args: [input_data]`** (one element), so the TS handler uses `const [x] = args` (for example a list of serialized dates). Align Python `input_data` in tests with this contract so both sides see the same logical inputs. From 8d39485b415b785301a7b39a10dafbe3c6aaf078 Mon Sep 17 00:00:00 2001 From: Thomas Bean <47thomasj@gmail.com> Date: Wed, 8 Apr 2026 11:29:40 -0600 Subject: [PATCH 41/41] version --- _version.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 _version.py diff --git a/_version.py b/_version.py new file mode 100644 index 0000000..e69de29