From b4bc31745c7838e457a59cc2e66164eee0ba7b26 Mon Sep 17 00:00:00 2001 From: Maximilian Haye Date: Fri, 27 Sep 2024 16:12:41 +0200 Subject: [PATCH 1/2] feat(run): allow a custom state dir --- questionpy_sdk/commands/run.py | 13 +++++++++---- questionpy_sdk/webserver/app.py | 5 ++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/questionpy_sdk/commands/run.py b/questionpy_sdk/commands/run.py index 84d7a3b..d31c0e1 100644 --- a/questionpy_sdk/commands/run.py +++ b/questionpy_sdk/commands/run.py @@ -1,18 +1,23 @@ # This file is part of the QuestionPy SDK. (https://questionpy.org) # The QuestionPy SDK is free software released under terms of the MIT license. See LICENSE.md. # (c) Technische Universität Berlin, innoCampus - from pathlib import Path import click from questionpy_sdk.commands._helper import get_package_location -from questionpy_sdk.webserver.app import WebServer +from questionpy_sdk.webserver.app import DEFAULT_STATE_STORAGE_PATH, WebServer @click.command() @click.argument("package") -def run(package: str) -> None: +@click.option( + "--state-storage-path", + type=click.Path(path_type=Path, exists=False, file_okay=False, dir_okay=True, resolve_path=True), + default=DEFAULT_STATE_STORAGE_PATH, + envvar="QPY_STATE_STORAGE_PATH", +) +def run(package: str, state_storage_path: Path) -> None: """Run a package. \b @@ -22,5 +27,5 @@ def run(package: str) -> None: - a source directory (built on-the-fly). """ # noqa: D301 pkg_path = Path(package).resolve() - web_server = WebServer(get_package_location(package, pkg_path)) + web_server = WebServer(get_package_location(package, pkg_path), state_storage_path) web_server.start_server() diff --git a/questionpy_sdk/webserver/app.py b/questionpy_sdk/webserver/app.py index 7ea9717..92c4964 100644 --- a/questionpy_sdk/webserver/app.py +++ b/questionpy_sdk/webserver/app.py @@ -51,11 +51,14 @@ class StateFilename(StrEnum): LAST_ATTEMPT_DATA = "last_attempt_data.json" +DEFAULT_STATE_STORAGE_PATH = Path(__file__).parent / "question_state_storage" + + class WebServer: def __init__( self, package_location: PackageLocation, - state_storage_path: Path = Path(__file__).parent / "question_state_storage", + state_storage_path: Path, ) -> None: # We import here, so we don't have to work around circular imports. from questionpy_sdk.webserver.routes.attempt import routes as attempt_routes # noqa: PLC0415 From 343c83d4a7a33bba3e7489f12a0c9ce99316b516 Mon Sep 17 00:00:00 2001 From: Maximilian Haye Date: Fri, 27 Sep 2024 16:14:34 +0200 Subject: [PATCH 2/2] test: isolate command tests from default state dir An invalid question state for the minimal example would cause test_run.py to fail since it used the default state storage dir. This commit builds upon b4bc317 to use an isolated tempdir for each long_running_cmd. --- tests/questionpy_sdk/commands/conftest.py | 32 +++++++++++++---------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/tests/questionpy_sdk/commands/conftest.py b/tests/questionpy_sdk/commands/conftest.py index d9d78d0..751fb28 100644 --- a/tests/questionpy_sdk/commands/conftest.py +++ b/tests/questionpy_sdk/commands/conftest.py @@ -7,6 +7,7 @@ import os import signal import sys +import tempfile from asyncio.subprocess import PIPE, Process from collections.abc import AsyncIterator, Iterable, Iterator from pathlib import Path @@ -71,23 +72,26 @@ async def client_session() -> AsyncIterator[aiohttp.ClientSession]: # can't test long-running processes with `CliRunner` (https://github.com/pallets/click/issues/2171) @contextlib.asynccontextmanager async def long_running_cmd(args: Iterable[str], timeout: float = 5) -> AsyncIterator[Process]: - try: - popen_args = [sys.executable, "-m", "questionpy_sdk", "--", *args] - proc = await asyncio.create_subprocess_exec(*popen_args, stdin=PIPE, stdout=PIPE, stderr=PIPE) + with tempfile.TemporaryDirectory("qpy-state-storage") as state_dir: + try: + popen_args = [sys.executable, "-m", "questionpy_sdk", "--", *args] + proc = await asyncio.create_subprocess_exec( + *popen_args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env={"QPY_STATE_STORAGE_PATH": state_dir} + ) - # ensure tests don't hang indefinitely - async def kill_after_timeout() -> None: - await asyncio.sleep(timeout) - proc.send_signal(signal.SIGINT) + # ensure tests don't hang indefinitely + async def kill_after_timeout() -> None: + await asyncio.sleep(timeout) + proc.send_signal(signal.SIGINT) - kill_task = asyncio.create_task(kill_after_timeout()) - yield proc + kill_task = asyncio.create_task(kill_after_timeout()) + yield proc - finally: - if kill_task: - kill_task.cancel() - proc.send_signal(signal.SIGINT) - await proc.wait() + finally: + if kill_task: + kill_task.cancel() + proc.send_signal(signal.SIGINT) + await proc.wait() async def assert_webserver_is_up(session: aiohttp.ClientSession, url: str = "http://localhost:8080/") -> None: