Skip to content

tests/core/test_cli.py

Source from this local checkout, regenerated when the reader rebuilds. Line links use #L<number>; a GitHub line range opens its first line.

1 # Copyright 2025 Softwell S.r.l.2 #3 # Licensed under the Apache License, Version 2.0 (the "License");4 # you may not use this file except in compliance with the License.5 # You may obtain a copy of the License at6 #7 #     https://www.apache.org/licenses/LICENSE-2.08 #9 # Unless required by applicable law or agreed to in writing, software10 # distributed under the License is distributed on an "AS IS" BASIS,11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12 # See the License for the specific language governing permissions and13 # limitations under the License.14 15 """CLI tests: the registry, target resolution, and the built (never booted) server.16 17 No server is ever started here: ``ServerLauncher.build_server`` is exercised on18 its own, so a recipe error surfaces as a boot error exactly as it does in19 ``test_config.py``. The registry always gets ``tmp_path`` as its base_dir.20 """21 22 from __future__ import annotations23 24 import json25 import os26 import signal27 import sys28 from pathlib import Path29 30 import pytest31 32 from genro_asgi.__main__ import (33     LAUNCHER_ENV,34     AppsRegistry,35     Cli,36     CliError,37     ServerLauncher,38     TargetResolver,39     factory,40 )41 42 CONFIG_RECIPE = (43     "from genro_asgi.config import AsgiConfigBuilder\n"44     "\n"45     "\n"46     "class ServerConfiguration(AsgiConfigBuilder):\n"47     "    def main(self, root):\n"48     "        cfg = root.configuration()\n"49     "        cfg.server(host='10.0.0.1', port=8321)\n"50 )51 52 APP_MODULE = (53     "from genro_asgi.application import BaseApplication\n"54     "\n"55     "\n"56     "class Hello(BaseApplication):\n"57     "    pass\n"58 )59 60 61 def parse(cli: Cli, argv: list[str]):62     """The parsed options of one invocation (no handler is called)."""63     return cli.parser().parse_args(argv)64 65 66 class TestAppsRegistry:67     def test_save_load_and_names(self, tmp_path: Path) -> None:68         registry = AppsRegistry(base_dir=tmp_path)69         registry.save("beta", {"source": "./b.py", "host": None, "port": None, "reload": False})70         registry.save("alpha", {"source": "./a.py", "host": "0.0.0.0", "port": 9000, "reload": True})71         assert registry.names() == ["alpha", "beta"]72         stored = registry.load("alpha")73         assert stored is not None and stored["port"] == 900074         assert registry.load("missing") is None75 76     def test_names_with_no_store_yet(self, tmp_path: Path) -> None:77         assert AppsRegistry(base_dir=tmp_path / "nothing").names() == []78 79     def test_remove_drops_entry_and_pidfile(self, tmp_path: Path) -> None:80         registry = AppsRegistry(base_dir=tmp_path)81         registry.save("demo", {"source": "./a.py"})82         registry.write_pid("demo", os.getpid())83         assert registry.remove("demo") is True84         assert registry.names() == []85         assert not registry.pid_path("demo").is_file()86         assert registry.remove("demo") is False87 88     def test_a_live_pid_reads_back(self, tmp_path: Path) -> None:89         registry = AppsRegistry(base_dir=tmp_path)90         registry.write_pid("demo", os.getpid())91         assert registry.read_pid("demo") == os.getpid()92 93     def test_a_stale_pidfile_reads_as_not_running(self, tmp_path: Path) -> None:94         registry = AppsRegistry(base_dir=tmp_path)95         registry.run_dir.mkdir(parents=True)96         registry.pid_path("demo").write_text("999999999", encoding="utf-8")97         assert registry.read_pid("demo") is None98 99     def test_an_unreadable_pidfile_reads_as_not_running(self, tmp_path: Path) -> None:100         registry = AppsRegistry(base_dir=tmp_path)101         registry.run_dir.mkdir(parents=True)102         registry.pid_path("demo").write_text("not-a-pid", encoding="utf-8")103         assert registry.read_pid("demo") is None104         assert registry.read_pid("never-written") is None105 106 107 class TestTargetResolver:108     def test_dotted_target(self) -> None:109         resolved = TargetResolver("genro_asgi.application:BaseApplication").resolve()110         assert resolved.__name__ == "BaseApplication"111 112     def test_file_target(self, tmp_path: Path) -> None:113         module = tmp_path / "hello.py"114         module.write_text(APP_MODULE)115         resolved = TargetResolver(f"{module}:Hello").resolve()116         assert resolved.__name__ == "Hello"117 118     def test_a_target_without_colon_is_an_error(self) -> None:119         with pytest.raises(CliError, match="package.module:ClassName"):120             TargetResolver("genro_asgi.application").resolve()121 122     def test_a_missing_file_is_an_error(self, tmp_path: Path) -> None:123         with pytest.raises(CliError, match="application file not found"):124             TargetResolver(f"{tmp_path / 'absent.py'}:Hello").resolve()125 126     def test_an_undefined_class_is_an_error(self) -> None:127         with pytest.raises(CliError, match="does not define 'Nope'"):128             TargetResolver("genro_asgi.application:Nope").resolve()129 130 131 class TestServeSourceResolution:132     def test_a_config_py_path_builds_a_configured_server(self, tmp_path: Path) -> None:133         module = tmp_path / "config.py"134         module.write_text(CONFIG_RECIPE)135         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))136         launcher = ServerLauncher(parse(cli, ["serve", str(module)]), cli.registry)137         server = launcher.build_server()138         assert server.config_host == "10.0.0.1"139         assert server.config_port == 8321140 141     def test_a_config_importing_a_sibling_module_resolves(142         self, tmp_path: Path, monkeypatch143     ) -> None:144         # The console script — unlike ``python -m`` — does not put the working145         # directory on sys.path: the launcher must insert the config's own one.146         (tmp_path / "sibling_app.py").write_text(APP_MODULE)147         module = tmp_path / "config.py"148         module.write_text(149             "from genro_asgi.config import AsgiConfigBuilder\n"150             "from sibling_app import Hello\n"151             "\n"152             "\n"153             "class ServerConfiguration(AsgiConfigBuilder):\n"154             "    def main(self, root):\n"155             "        cfg = root.configuration()\n"156             "        cfg.applications().application(code='hello', mount='', app_class=Hello)\n"157         )158         monkeypatch.setattr(sys, "path", [p for p in sys.path if p != str(tmp_path)])159         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))160         launcher = ServerLauncher(parse(cli, ["serve", str(module)]), cli.registry)161         server = launcher.build_server()162         assert "hello" in server.applications163 164     def test_explicit_host_and_port_win_over_the_recipe(self, tmp_path: Path) -> None:165         module = tmp_path / "config.py"166         module.write_text(CONFIG_RECIPE)167         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))168         options = parse(cli, ["serve", str(module), "--host", "0.0.0.0", "--port", "7000"])169         server = ServerLauncher(options, cli.registry).build_server()170         assert (server.config_host, server.config_port) == ("0.0.0.0", 7000)171 172     def test_a_quickstart_target_builds_the_named_application(self, tmp_path: Path) -> None:173         module = tmp_path / "hello.py"174         module.write_text(APP_MODULE)175         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))176         options = parse(cli, ["serve", f"application={module}:Hello"])177         server = ServerLauncher(options, cli.registry).build_server()178         assert "Hello" in {type(app).__name__ for app in server.applications.values()}179 180     def test_an_unknown_name_lists_the_registered_ones(self, tmp_path: Path) -> None:181         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))182         cli.registry.save("demo", {"source": "./a.py"})183         with pytest.raises(CliError, match="unknown app 'ghost' \\(registered: demo\\)"):184             ServerLauncher(parse(cli, ["serve", "ghost"]), cli.registry)185 186     def test_an_unknown_name_with_an_empty_registry(self, tmp_path: Path) -> None:187         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))188         with pytest.raises(CliError, match="none registered"):189             ServerLauncher(parse(cli, ["serve", "ghost"]), cli.registry)190 191     def test_a_registered_name_restores_source_and_options(self, tmp_path: Path) -> None:192         module = tmp_path / "config.py"193         module.write_text(CONFIG_RECIPE)194         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))195         cli.registry.save(196             "demo", {"source": str(module), "host": "127.0.0.5", "port": 9111, "reload": True}197         )198         launcher = ServerLauncher(parse(cli, ["serve", "demo"]), cli.registry)199         assert (launcher.source, launcher.host, launcher.port) == (str(module), "127.0.0.5", 9111)200         assert launcher.reload is True201         assert launcher.name == "demo"202         assert launcher.build_server().config_port == 9111203 204     def test_a_command_line_option_wins_over_the_stored_one(self, tmp_path: Path) -> None:205         module = tmp_path / "config.py"206         module.write_text(CONFIG_RECIPE)207         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))208         cli.registry.save("demo", {"source": str(module), "host": "127.0.0.5", "port": 9111})209         launcher = ServerLauncher(parse(cli, ["serve", "demo", "--port", "9500"]), cli.registry)210         assert (launcher.host, launcher.port) == ("127.0.0.5", 9500)211 212     def test_a_stored_source_that_no_longer_exists_is_an_error(self, tmp_path: Path) -> None:213         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))214         cli.registry.save("demo", {"source": str(tmp_path / "gone.py")})215         launcher = ServerLauncher(parse(cli, ["serve", "demo"]), cli.registry)216         with pytest.raises(CliError, match="cannot serve"):217             launcher.build_server()218 219 220 class TestArgumentParsing:221     def test_serve_options_become_the_registry_entry(self, tmp_path: Path) -> None:222         module = tmp_path / "config.py"223         module.write_text(CONFIG_RECIPE)224         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))225         options = parse(226             cli, ["serve", str(module), "--host", "0.0.0.0", "--port", "8080", "--reload", "--name", "demo"]227         )228         launcher = ServerLauncher(options, cli.registry)229         assert launcher.entry == {230             "source": str(module),231             "host": "0.0.0.0",232             "port": 8080,233             "reload": True,234             "debug": False,235         }236         assert launcher.server_kwargs == {"host": "0.0.0.0", "port": 8080}237 238     def test_no_option_given_forwards_no_kwarg(self, tmp_path: Path) -> None:239         module = tmp_path / "config.py"240         module.write_text(CONFIG_RECIPE)241         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))242         launcher = ServerLauncher(parse(cli, ["serve", str(module)]), cli.registry)243         assert launcher.server_kwargs == {}244 245 246 class TestSaveSessionWiring:247     """Naming an instance IS the switch: the snapshot file takes the name."""248 249     def test_a_named_serve_arms_the_snapshot(self, tmp_path: Path) -> None:250         module = tmp_path / "config.py"251         module.write_text(CONFIG_RECIPE)252         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))253         launcher = ServerLauncher(parse(cli, ["serve", str(module), "--name", "demo"]), cli.registry)254         expected = str(tmp_path / "sessions" / "demo.pickle")255         assert launcher.save_session_path == expected256         assert launcher.constructor_kwargs == {"save_session": expected}257         assert launcher.server_kwargs == {}  # serve() never sees the snapshot kwarg258         assert launcher.build_server().save_session == Path(expected)259 260     def test_a_nameless_serve_stays_volatile(self, tmp_path: Path) -> None:261         module = tmp_path / "config.py"262         module.write_text(CONFIG_RECIPE)263         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))264         launcher = ServerLauncher(parse(cli, ["serve", str(module)]), cli.registry)265         assert launcher.save_session_path is None266         assert "save_session" not in launcher.constructor_kwargs267         assert launcher.build_server().save_session is None268 269     def test_relaunching_a_registered_name_arms_the_same_file(self, tmp_path: Path) -> None:270         module = tmp_path / "config.py"271         module.write_text(CONFIG_RECIPE)272         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))273         cli.registry.save("demo", {"source": str(module)})274         launcher = ServerLauncher(parse(cli, ["serve", "demo"]), cli.registry)275         assert launcher.save_session_path == str(tmp_path / "sessions" / "demo.pickle")276 277     def test_a_missing_subcommand_is_a_usage_error(self) -> None:278         with pytest.raises(SystemExit) as exit_info:279             Cli().run([])280         assert exit_info.value.code == 2281 282 283 class TestRegistryCommands:284     def test_apps_reports_nothing_registered(self, tmp_path: Path, capsys) -> None:285         assert Cli(registry=AppsRegistry(base_dir=tmp_path)).run(["apps"]) == 0286         assert "No registered servers" in capsys.readouterr().out287 288     def test_apps_reports_status_source_and_address(self, tmp_path: Path, capsys) -> None:289         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))290         cli.registry.save("demo", {"source": "./a.py", "host": "0.0.0.0", "port": 8080})291         cli.registry.save("idle", {"source": "./b.py", "host": None, "port": None})292         cli.registry.write_pid("demo", os.getpid())293         assert cli.run(["apps"]) == 0294         out = capsys.readouterr().out295         assert f"running (pid {os.getpid()})" in out296         assert "0.0.0.0:8080" in out297         assert "./a.py" in out298         assert "stopped" in out299         assert "-:-" in out300 301     def test_stop_signals_the_recorded_pid(self, tmp_path: Path, capsys, monkeypatch) -> None:302         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))303         cli.registry.write_pid("demo", os.getpid())304         signalled: list[tuple[int, int]] = []305         original = os.kill306 307         def spy(pid: int, sig: int) -> None:308             if sig == 0:  # the liveness probe stays real309                 original(pid, sig)310             else:311                 signalled.append((pid, sig))312 313         monkeypatch.setattr(os, "kill", spy)314         assert cli.run(["stop", "demo"]) == 0315         assert signalled == [(os.getpid(), signal.SIGTERM)]316         assert "stopped" in capsys.readouterr().out317 318     def test_stop_a_process_dying_between_probe_and_signal(319         self, tmp_path: Path, capsys, monkeypatch320     ) -> None:321         # TOCTOU: the probe (sig 0) sees the process alive, the SIGTERM finds322         # it gone — same outcome as finding it already stopped, one line out.323         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))324         cli.registry.write_pid("demo", os.getpid())325         original = os.kill326 327         def dies_after_probe(pid: int, sig: int) -> None:328             if sig == 0:329                 original(pid, sig)330             else:331                 raise ProcessLookupError(pid)332 333         monkeypatch.setattr(os, "kill", dies_after_probe)334         assert cli.run(["stop", "demo"]) == 0335         assert not cli.registry.pid_path("demo").is_file()336         assert "not running" in capsys.readouterr().out337 338     def test_stop_a_dead_app_cleans_the_stale_pidfile(self, tmp_path: Path, capsys) -> None:339         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))340         cli.registry.run_dir.mkdir(parents=True)341         cli.registry.pid_path("demo").write_text("999999999", encoding="utf-8")342         assert cli.run(["stop", "demo"]) == 0343         assert not cli.registry.pid_path("demo").is_file()344         assert "not running" in capsys.readouterr().out345 346     def test_remove_refuses_a_running_app(self, tmp_path: Path, capsys) -> None:347         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))348         cli.registry.save("demo", {"source": "./a.py"})349         cli.registry.write_pid("demo", os.getpid())350         assert cli.run(["remove", "demo"]) == 1351         assert "stop it first" in capsys.readouterr().err352         assert cli.registry.names() == ["demo"]353 354     def test_remove_drops_a_stopped_app(self, tmp_path: Path, capsys) -> None:355         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))356         cli.registry.save("demo", {"source": "./a.py"})357         assert cli.run(["remove", "demo"]) == 0358         assert cli.registry.names() == []359         assert "removed" in capsys.readouterr().out360 361     def test_remove_an_unregistered_name_is_an_error(self, tmp_path: Path, capsys) -> None:362         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))363         assert cli.run(["remove", "ghost"]) == 1364         assert "not registered" in capsys.readouterr().err365 366 367 class TestReloadPayload:368     """What crosses the process boundary — the supervisor itself is not started."""369 370     def test_a_config_source_travels_as_an_absolute_path(self, tmp_path: Path, monkeypatch) -> None:371         module = tmp_path / "config.py"372         module.write_text(CONFIG_RECIPE)373         monkeypatch.chdir(tmp_path)374         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))375         launcher = ServerLauncher(parse(cli, ["serve", "config.py", "--reload"]), cli.registry)376         assert launcher.launcher_payload == {"config": str(module.resolve())}377         assert launcher.reload_dir == str(tmp_path.resolve())378 379     def test_a_file_target_travels_absolute_too(self, tmp_path: Path, monkeypatch) -> None:380         module = tmp_path / "hello.py"381         module.write_text(APP_MODULE)382         monkeypatch.chdir(tmp_path)383         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))384         launcher = ServerLauncher(parse(cli, ["serve", "application=hello.py:Hello"]), cli.registry)385         assert launcher.launcher_payload == {"application": f"{module.resolve()}:Hello"}386         assert launcher.reload_dir == str(tmp_path.resolve())387 388     def test_a_dotted_target_watches_the_working_directory(self, tmp_path: Path, monkeypatch) -> None:389         monkeypatch.chdir(tmp_path)390         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))391         options = parse(cli, ["serve", "application=genro_asgi.application:BaseApplication"])392         launcher = ServerLauncher(options, cli.registry)393         payload = launcher.launcher_payload394         assert payload == {"application": "genro_asgi.application:BaseApplication"}395         assert launcher.reload_dir == str(tmp_path.resolve())396 397     def test_only_the_explicit_host_and_port_are_carried(self, tmp_path: Path) -> None:398         module = tmp_path / "config.py"399         module.write_text(CONFIG_RECIPE)400         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))401         options = parse(cli, ["serve", str(module), "--reload", "--port", "7100"])402         payload = ServerLauncher(options, cli.registry).launcher_payload403         assert payload == {"config": str(module.resolve()), "port": 7100}404 405     def test_the_reload_boot_exports_the_payload_and_binds_the_configured_address(406         self, tmp_path: Path, monkeypatch, capsys407     ) -> None:408         module = tmp_path / "config.py"409         module.write_text(CONFIG_RECIPE)410         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))411         launched: dict = {}412         monkeypatch.setattr(413             "genro_asgi.reloading.uvicorn.run",414             lambda target, **kwargs: launched.update(target=target, **kwargs),415         )416         # monkeypatch owns the variable, so the value the launcher writes is417         # restored after the test instead of leaking into the environment.418         monkeypatch.setenv(LAUNCHER_ENV, "")419         assert cli.run(["serve", str(module), "--reload", "--name", "demo"]) == 0420         assert json.loads(os.environ[LAUNCHER_ENV]) == {421             "config": str(module.resolve()),422             "save_session": str(tmp_path / "sessions" / "demo.pickle"),423             "host": "10.0.0.1",424             "port": 8321,425         }426         assert launched == {427             "target": "genro_asgi.__main__:factory",428             "factory": True,429             "reload": True,430             "reload_dirs": [str(tmp_path.resolve())],431             "reload_excludes": None,432             "host": "10.0.0.1",433             "port": 8321,434         }435         assert not cli.registry.pid_path("demo").is_file()436         assert "10.0.0.1:8321" in capsys.readouterr().out437 438 439 class TestFactory:440     """The rebuild on the far side of the boundary."""441 442     def test_a_config_payload_rebuilds_the_configured_server(self, tmp_path: Path, monkeypatch) -> None:443         module = tmp_path / "config.py"444         module.write_text(CONFIG_RECIPE)445         monkeypatch.setenv(LAUNCHER_ENV, json.dumps({"config": str(module)}))446         server = factory()447         assert (server.config_host, server.config_port) == ("10.0.0.1", 8321)448 449     def test_an_application_payload_rebuilds_the_named_application(self, tmp_path: Path, monkeypatch) -> None:450         module = tmp_path / "hello.py"451         module.write_text(APP_MODULE)452         monkeypatch.setenv(LAUNCHER_ENV, json.dumps({"application": f"{module}:Hello"}))453         server = factory()454         assert "Hello" in {type(app).__name__ for app in server.applications.values()}455 456     def test_explicit_host_and_port_in_the_payload_win_over_the_recipe(457         self, tmp_path: Path, monkeypatch458     ) -> None:459         module = tmp_path / "config.py"460         module.write_text(CONFIG_RECIPE)461         payload = {"config": str(module), "host": "0.0.0.0", "port": 7100}462         monkeypatch.setenv(LAUNCHER_ENV, json.dumps(payload))463         server = factory()464         assert (server.config_host, server.config_port) == ("0.0.0.0", 7100)465 466     def test_a_config_payload_with_a_sibling_import_resolves(467         self, tmp_path: Path, monkeypatch468     ) -> None:469         # The reloaded process starts fresh: factory() re-inserts the config's470         # directory exactly as the parent's launcher did.471         (tmp_path / "sibling_reload.py").write_text(APP_MODULE.replace("Hello", "Hot"))472         module = tmp_path / "config.py"473         module.write_text(474             "from genro_asgi.config import AsgiConfigBuilder\n"475             "from sibling_reload import Hot\n"476             "\n"477             "\n"478             "class ServerConfiguration(AsgiConfigBuilder):\n"479             "    def main(self, root):\n"480             "        cfg = root.configuration()\n"481             "        cfg.applications().application(code='hot', mount='', app_class=Hot)\n"482         )483         monkeypatch.setattr(sys, "path", [p for p in sys.path if p != str(tmp_path)])484         monkeypatch.setenv(LAUNCHER_ENV, json.dumps({"config": str(module)}))485         server = factory()486         assert "hot" in server.applications487 488     def test_a_missing_variable_names_it(self, monkeypatch) -> None:489         monkeypatch.delenv(LAUNCHER_ENV, raising=False)490         with pytest.raises(CliError, match=f"{LAUNCHER_ENV} is not set"):491             factory()492 493     def test_malformed_json_is_an_error(self, monkeypatch) -> None:494         monkeypatch.setenv(LAUNCHER_ENV, "{not json")495         with pytest.raises(CliError, match="is not valid JSON"):496             factory()497 498     def test_a_payload_without_a_source_key_is_an_error(self, monkeypatch) -> None:499         monkeypatch.setenv(LAUNCHER_ENV, json.dumps({"host": "0.0.0.0"}))500         with pytest.raises(CliError, match="neither an 'application' nor a 'config' key"):501             factory()502 503     def test_save_session_in_the_payload_arms_the_snapshot(504         self, tmp_path: Path, monkeypatch505     ) -> None:506         module = tmp_path / "config.py"507         module.write_text(CONFIG_RECIPE)508         snapshot = str(tmp_path / "sessions" / "demo.pickle")509         payload = {"config": str(module), "save_session": snapshot}510         monkeypatch.setenv(LAUNCHER_ENV, json.dumps(payload))511         assert factory().save_session == Path(snapshot)512 513 514 class TestDebugAndReloadTrigger:515     def test_debug_travels_from_the_flag_to_the_server(self, tmp_path: Path) -> None:516         module = tmp_path / "config.py"517         module.write_text(CONFIG_RECIPE)518         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))519         options = parse(cli, ["serve", str(module), "--debug", "sql,timing"])520         server = ServerLauncher(options, cli.registry).build_server()521         assert server.debug == "sql,timing"522 523     def test_a_bare_debug_flag_reads_true_and_its_absence_false(self, tmp_path: Path) -> None:524         module = tmp_path / "config.py"525         module.write_text(CONFIG_RECIPE)526         cli = Cli(registry=AppsRegistry(base_dir=tmp_path))527         flagged = parse(cli, ["serve", str(module), "--debug"])528         plain = parse(cli, ["serve", str(module)])529         assert ServerLauncher(flagged, cli.registry).build_server().debug is True530         assert ServerLauncher(plain, cli.registry).build_server().debug is False531 532     def test_the_reloaded_child_declares_its_exits_save(self, tmp_path: Path, monkeypatch) -> None:533         from genro_asgi.lifespan import QUITTING, STOPPING534 535         module = tmp_path / "config.py"536         module.write_text(CONFIG_RECIPE)537         monkeypatch.setenv(LAUNCHER_ENV, json.dumps({"config": str(module), "debug": True}))538         server = factory()539         assert server.shutdown_mode == QUITTING540         assert server.debug is True541         # and outside the supervisor the default stays the dry exit542         from genro_asgi import BaseServer543         from genro_asgi.application import BaseApplication544         assert BaseServer(applications=[BaseApplication(mount="")]).shutdown_mode == STOPPING