Find a study wherever it is, and start on a port that is free
Docs / docs (push) Successful in 33s
Playwright Tests / test-playwright (1, 2) (push) Successful in 2m10s
Playwright Tests / test-playwright (2, 2) (push) Successful in 1m49s
pre-commit / pre-commit (push) Failing after 2m3s
Test Backend / test-backend (push) Successful in 2m32s
Compose Smoke Test / test-compose (push) Successful in 31s
Playwright Tests / merge-reports (push) Successful in 1m9s
Docs / docs (push) Successful in 33s
Playwright Tests / test-playwright (1, 2) (push) Successful in 2m10s
Playwright Tests / test-playwright (2, 2) (push) Successful in 1m49s
pre-commit / pre-commit (push) Failing after 2m3s
Test Backend / test-backend (push) Successful in 2m32s
Compose Smoke Test / test-compose (push) Successful in 31s
Playwright Tests / merge-reports (push) Successful in 1m9s
Three things the one-folder-per-study layout ran into. **Discovery walks down.** A plain directory is now walked all the way, so `fluksio sync dev` finds `dev/s1_baseline/study.py` and naming each study is no longer the price of the layout. Hidden directories, `__pycache__`, `node_modules` and virtualenvs are left alone, and a package is taken whole. Two files that would import under one module name are refused, naming both: Python keeps one module per name, so the second would silently *be* the first — and a node's generated body imports by that name, so a worker would run the wrong study's code. The message says the fix, which is an `__init__.py` per study directory. A module that raises while importing is now a sentence naming the file rather than an importlib traceback. **`run` and `sweep` sync downwards too**, so the flow is found from the repository root without the sync-then-`--no-sync` two-step. A study that will not import is a warning rather than a stopped run, since a walk meets every study and a half-finished one two directories away is not this run's problem. The upload was already a no-op for a flow nothing changed in, so what the walk costs is import time — `--sync PATH` narrows it, and skipping unchanged subtrees would need a cache keyed on file state that is deliberately not here. **`serve` moves off a busy default port** — 8001, 8002, up to twenty — says which it took, and writes that one into `client.json`. A port given with `--port` still fails when it is taken, because naming one is asking for it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A9Hdrmf2cwNABCnE5x9UJa
This commit is contained in:
@@ -388,3 +388,79 @@ def test_an_engine_without_the_route_is_named_rather_than_404() -> None:
|
||||
assert "engine is 0.1.4" in _too_old(Old())
|
||||
assert "engine is older" in _too_old(Ancient())
|
||||
assert "pip install -U fluksio" in _too_old(Ancient())
|
||||
|
||||
|
||||
def test_a_study_in_a_subfolder_is_found(tmp_path) -> None:
|
||||
"""One directory per study is a layout; naming each is bookkeeping."""
|
||||
from fluksio.sdk.cli import _below
|
||||
|
||||
(tmp_path / "dev" / "s1").mkdir(parents=True)
|
||||
(tmp_path / "dev" / "s2" / "inner").mkdir(parents=True)
|
||||
(tmp_path / "results").mkdir()
|
||||
(tmp_path / "pkg").mkdir()
|
||||
(tmp_path / ".hidden").mkdir()
|
||||
(tmp_path / "dev" / "s1" / "study.py").write_text("")
|
||||
(tmp_path / "dev" / "s2" / "inner" / "probe.py").write_text("")
|
||||
(tmp_path / "results" / "notes.py").write_text("")
|
||||
(tmp_path / "pkg" / "__init__.py").write_text("")
|
||||
(tmp_path / "pkg" / "deep.py").write_text("")
|
||||
(tmp_path / ".hidden" / "skip.py").write_text("")
|
||||
|
||||
found = {path.relative_to(tmp_path).as_posix() for path in _below(tmp_path)}
|
||||
|
||||
# However deep, plus packages whole — and nothing under a dot directory.
|
||||
assert found == {
|
||||
"dev/s1/study.py",
|
||||
"dev/s2/inner/probe.py",
|
||||
"results/notes.py",
|
||||
"pkg",
|
||||
}
|
||||
|
||||
|
||||
def test_two_files_of_one_name_are_refused(tmp_path) -> None:
|
||||
"""Python keeps one module per name, and a node's body imports by it."""
|
||||
import pytest
|
||||
|
||||
from fluksio.sdk import SyncError
|
||||
from fluksio.sdk.cli import _import, _module_of
|
||||
|
||||
for study in ("s1", "s2"):
|
||||
(tmp_path / study).mkdir()
|
||||
(tmp_path / study / "study.py").write_text("VALUE = 1\n")
|
||||
|
||||
first = tmp_path / "s1" / "study.py"
|
||||
second = tmp_path / "s2" / "study.py"
|
||||
_import(*_module_of(first), first)
|
||||
with pytest.raises(SyncError, match="both import as 'study'"):
|
||||
_import(*_module_of(second), second)
|
||||
|
||||
|
||||
def test_run_and_sweep_take_what_to_sync() -> None:
|
||||
from fluksio.cli import _parser
|
||||
|
||||
parser = _parser()
|
||||
assert parser.parse_args(["sweep", "train", "--sync", "dev/s1"]).sync == ["dev/s1"]
|
||||
args, _ = parser.parse_known_args(["run", "train", "--sync", "dev/s1"])
|
||||
assert args.sync == ["dev/s1"]
|
||||
# Nothing named means this directory, downwards.
|
||||
assert parser.parse_args(["sweep", "train"]).sync == []
|
||||
|
||||
|
||||
def test_serve_moves_off_a_port_that_is_taken() -> None:
|
||||
"""A first start should not die on somebody else's dev server."""
|
||||
import socket
|
||||
|
||||
from fluksio.cli import DEFAULT_PORT, _free_port, _parser
|
||||
|
||||
with socket.socket() as held:
|
||||
held.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
held.bind(("127.0.0.1", 0))
|
||||
held.listen(1)
|
||||
taken = held.getsockname()[1]
|
||||
|
||||
assert _free_port("127.0.0.1", taken) == taken + 1
|
||||
|
||||
# A port that was asked for is not moved off: that is what asking means.
|
||||
assert _parser().parse_args(["serve"]).port is None
|
||||
assert _parser().parse_args(["serve", "--port", "9000"]).port == 9000
|
||||
assert DEFAULT_PORT == 8000
|
||||
|
||||
Reference in New Issue
Block a user