Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Give each test an empty TimeNet home and no other ``TIMENET_*`` setting."""

import os

import pytest

from timenet.config import TimeNetSettings


@pytest.fixture(autouse=True)
def _timenet_state(tmp_path_factory, monkeypatch):
"""Set an empty temporary TimeNet home and clear other ``TIMENET_*`` variables.

Ignore settings from ``.env`` files.
"""
for name in [name for name in os.environ if name.startswith("TIMENET_")]:
monkeypatch.delenv(name)
monkeypatch.setitem(TimeNetSettings.model_config, "env_file", None)
monkeypatch.setenv("TIMENET_HOME", str(tmp_path_factory.mktemp("timenet-home")))
2 changes: 0 additions & 2 deletions packages/timenet-connectors/tests/test_build_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,6 @@ def test_build_in_process_round_trips_the_connector_default_zarr_backend(tmp_pat
# Only BuildOptions.quiet silences the child. An SDK build without a progress callback still prints.
@pytest.mark.parametrize("quiet", [False, True])
def test_build_runs_an_isolated_child_with_the_release_and_the_pins(tmp_path, monkeypatch, quiet):
monkeypatch.delenv("TIMENET_ISOLATION", raising=False)
monkeypatch.setattr(backend_module, "run_pipeline", _unexpected_pipeline)
calls = []
monkeypatch.setattr(
Expand All @@ -148,7 +147,6 @@ def test_build_runs_an_isolated_child_with_the_release_and_the_pins(tmp_path, mo


def test_build_rejects_another_version(tmp_path, monkeypatch):
monkeypatch.setenv("TIMENET_ISOLATION", "on")
monkeypatch.setattr(backend_module, "run_isolated_build", _unexpected_isolation)
monkeypatch.setattr(backend_module, "run_pipeline", _unexpected_pipeline)
release = DatasetRef.parse("timenet/hello-world@9.9.9")
Expand Down
8 changes: 1 addition & 7 deletions packages/timenet-connectors/tests/test_build_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ def _unexpected(*args, **kwargs):


def test_build_runs_isolated_by_default(monkeypatch, tmp_path):
monkeypatch.delenv("TIMENET_ISOLATION", raising=False)
calls = []
monkeypatch.setattr(backend_module, "run_isolated_build", _isolated_child(calls))

Expand All @@ -67,9 +66,7 @@ def test_build_runs_in_process_without_isolation(monkeypatch, tmp_path, how):
calls = []
monkeypatch.setattr(backend_module, "run_isolated_build", _unexpected)
monkeypatch.setattr(backend_module, "run_pipeline", _in_process(calls))
if how == "flag":
monkeypatch.delenv("TIMENET_ISOLATION", raising=False)
else:
if how == "setting":
monkeypatch.setenv("TIMENET_ISOLATION", "off")

result = runner.invoke(
Expand Down Expand Up @@ -104,7 +101,6 @@ def test_build_without_isolation_shows_each_build_line_once(monkeypatch, tmp_pat


def test_a_build_that_reuses_the_release_says_so_on_stderr(monkeypatch, tmp_path):
monkeypatch.delenv("TIMENET_ISOLATION", raising=False)
# The line names a long path, so the console must not wrap it.
monkeypatch.setattr(cli_module.console, "rich", Console(stderr=True, width=1000))
calls = []
Expand All @@ -122,7 +118,6 @@ def test_a_build_that_reuses_the_release_says_so_on_stderr(monkeypatch, tmp_path


def test_build_takes_an_exact_release_and_refuses_another_version(monkeypatch, tmp_path):
monkeypatch.delenv("TIMENET_ISOLATION", raising=False)
calls = []
monkeypatch.setattr(backend_module, "run_isolated_build", _isolated_child(calls))
result = runner.invoke(app, ["build", "timenet/hello-world@1.0.0", "--out", str(tmp_path)])
Expand Down Expand Up @@ -162,7 +157,6 @@ def test_build_rejects_an_id_that_no_card_declares_and_names_the_known_ids(tmp_p

@pytest.mark.parametrize("quiet", [False, True])
def test_build_forwards_quiet_to_the_isolated_child(monkeypatch, tmp_path, quiet):
monkeypatch.delenv("TIMENET_ISOLATION", raising=False)
# The console is a process-wide singleton; monkeypatch restores it for the tests that follow.
monkeypatch.setattr(cli_module.console, "quiet", False)
calls = []
Expand Down
6 changes: 0 additions & 6 deletions packages/timenet-connectors/tests/test_build_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,6 @@ def child(release, store_root, *, pinned_by, **_):
else:
_store_from_card(release.dataset_id, store_root, force=False)

monkeypatch.delenv("TIMENET_ISOLATION", raising=False)
monkeypatch.setattr(backend_module, "run_isolated_build", child)

if backend == ValuesBackend.PARQUET:
Expand Down Expand Up @@ -510,7 +509,6 @@ def child_builds(monkeypatch) -> list:
Returns:
The list that receives one ``(dataset_id, force)`` entry for each build.
"""
monkeypatch.delenv("TIMENET_ISOLATION", raising=False)
calls = []
monkeypatch.setattr(backend_module, "run_isolated_build", _prepared_builds(calls))
return calls
Expand All @@ -519,7 +517,6 @@ def child_builds(monkeypatch) -> list:
def test_build_prepares_parents_before_the_isolated_child(tmp_path, monkeypatch):
cards = _install_cards(monkeypatch, child={"parent": PARENT, "grand": GRAND}, parent={})
monkeypatch.setattr(cli_module, "known_ids", lambda: list(cards))
monkeypatch.delenv("TIMENET_ISOLATION", raising=False)
origin = LocalRegistry(tmp_path / "origin")
origin.put_dataset(make_dataset())
out = tmp_path / "out"
Expand All @@ -541,7 +538,6 @@ def test_build_prepares_parents_before_the_isolated_child(tmp_path, monkeypatch)
@pytest.mark.parametrize("obtain", ["fetch_from", "build_parents"])
def test_load_obtains_a_missing_parent_before_it_builds_and_reads(tmp_path, monkeypatch, obtain):
cards = _install_cards(monkeypatch, child={"parent": PARENT}, parent={})
monkeypatch.delenv("TIMENET_ISOLATION", raising=False)
monkeypatch.setenv("TIMENET_HOME", str(tmp_path / "home"))
monkeypatch.setenv("TIMENET_REGISTRY", str(tmp_path / "out"))
calls = []
Expand Down Expand Up @@ -601,7 +597,6 @@ def child(command, env):
result = runner.invoke(app, argv)
return "", result.exit_code

monkeypatch.delenv("TIMENET_ISOLATION", raising=False)
monkeypatch.setattr(env_module, "_run_build", child)
monkeypatch.setattr(cli_module, "known_ids", lambda: list(cards))
monkeypatch.setattr(backend_module, "run_pipeline", run_pipeline)
Expand Down Expand Up @@ -666,7 +661,6 @@ def _remote_out(tmp_path, monkeypatch, cards) -> tuple[_Hosted, LocalRegistry, l
monkeypatch.setattr(cli_module, "_resolve_target", lambda target: remote)
monkeypatch.setattr(cli_module, "known_ids", lambda: list(cards))
monkeypatch.setenv("TIMENET_HOME", str(tmp_path / "home"))
monkeypatch.delenv("TIMENET_ISOLATION", raising=False)
calls = []
monkeypatch.setattr(backend_module, "run_isolated_build", _prepared_builds(calls))
return remote, LocalRegistry(tmp_path / "home/registry"), calls
Expand Down
2 changes: 0 additions & 2 deletions packages/timenet-connectors/tests/test_package_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ def test_build_then_load_round_trip(monkeypatch, tmp_path):
def test_build_runs_in_an_isolated_environment_by_default(monkeypatch, tmp_path):
# The parent reads only the card. It does not import the connector, which needs dependencies
# that only the environment of the child has.
monkeypatch.delenv("TIMENET_ISOLATION", raising=False)
monkeypatch.setattr(backend_module, "resolve", _unexpected_resolve)
calls = []

Expand All @@ -58,7 +57,6 @@ def fake_isolated(release, store_root, *, force=False, keep_cache=False, quiet=F


def test_build_rejects_a_version_the_connector_does_not_declare(monkeypatch, tmp_path):
monkeypatch.delenv("TIMENET_ISOLATION", raising=False)
monkeypatch.setattr(backend_module, "resolve", _unexpected_resolve)
monkeypatch.setattr(backend_module, "run_isolated_build", _unexpected_isolation)

Expand Down
9 changes: 9 additions & 0 deletions packages/timenet/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
import sys

from hypothesis import HealthCheck, settings
import pytest

from timenet.testing import install_backends


# Make the test helpers of this directory (e.g. ``_demo_child``) importable by name under pytest's
Expand All @@ -24,3 +27,9 @@
suppress_health_check=[HealthCheck.too_slow],
)
settings.load_profile(os.environ.get("HYPOTHESIS_PROFILE", "ci"))


@pytest.fixture(autouse=True)
def _no_installed_connectors(monkeypatch):
"""Install no builder backends, so that no test reads the connector packages of the environment."""
install_backends(monkeypatch)
6 changes: 1 addition & 5 deletions packages/timenet/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@
import typer
from typer.testing import CliRunner

from timenet import builders
from timenet.cache import cached_datasets, raw_cache_size
from timenet.cli.app import _enum_list, app
from timenet.dataset import TimeFDataset
from timenet.errors import TimeNetDatasetNotFoundError
from timenet.reader import TimeFReader
from timenet.registry import open_local_version
from timenet.testing import install_backends, make_dataset
from timenet.testing import make_dataset
from timenet.types import Access, Domain
from timenet.types.reference_keys import ParentAlias
from timenet.types.references import DatasetRef
Expand Down Expand Up @@ -154,8 +153,6 @@ def test_info_lists_the_access_page_of_each_release_that_sets_one(tmp_path):


def test_download_command_copies_into_the_store_and_then_has_nothing_to_do(registry_root, tmp_path, monkeypatch):
# The default sources end with the installed connectors. Keep the real ones out of the resolution.
monkeypatch.setattr(builders, "entry_points", lambda group: [])
# The last line names a long path, so the console must not wrap it.
monkeypatch.setattr(_cli_module.ui, "rich", Console(stderr=True, width=1000))
monkeypatch.setenv("TIMENET_HOME", str(tmp_path / "home"))
Expand All @@ -180,7 +177,6 @@ def test_download_command_copies_into_the_store_and_then_has_nothing_to_do(regis


def test_download_command_with_force_copies_a_held_release_again(registry_root, tmp_path, monkeypatch):
install_backends(monkeypatch)
monkeypatch.setenv("TIMENET_HOME", str(tmp_path / "home"))
monkeypatch.setenv("TIMENET_REGISTRY", str(tmp_path / "store"))
command = ["download", "timenet/hello-world", "--registry", str(registry_root)]
Expand Down
15 changes: 4 additions & 11 deletions packages/timenet/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from pydantic import ValidationError
import pytest

from timenet import builders as builders_module
from timenet.client import TimeNet, default_sources
from timenet.connectors import BaseConnector
from timenet.dataset import Record, RegularAxis, Signal, Source, TimeFDataset
Expand Down Expand Up @@ -65,8 +64,7 @@ def build(self, release, store, *, force, pinned_by, options):
store.put_dataset(dataset, force=force, values_backend=self.BACKEND, pinned_by=pinned_by or None)


def test_a_child_reads_its_missing_payload_source_from_a_later_registry(tmp_path, monkeypatch):
monkeypatch.setattr(builders_module, "entry_points", lambda group: [])
def test_a_child_reads_its_missing_payload_source_from_a_later_registry(tmp_path):
origin, reference = _child_of_the_demo(tmp_path / "origin")
cache = tmp_path / "cache"
shutil.copytree(origin.root / "test/child/1.0.0", cache / "test/child/1.0.0")
Expand Down Expand Up @@ -221,7 +219,6 @@ def test_the_default_list_follows_timenet_registry(tmp_path, monkeypatch, regist


def test_timenet_registry_env_selects_a_local_registry(tmp_path, monkeypatch):
monkeypatch.setattr(builders_module, "entry_points", lambda group: [])
monkeypatch.setenv("TIMENET_HOME", str(tmp_path / "home"))
monkeypatch.setenv("TIMENET_REGISTRY", str(tmp_path / "home" / "registry"))
dataset = make_dataset()
Expand Down Expand Up @@ -363,8 +360,7 @@ def test_download_of_latest_fails_when_a_connector_declares_a_newer_version(regi
assert client.download("timenet/hello-world") == store.root / "timenet/hello-world/1.0.0"


def test_download_of_a_release_that_no_source_provides_names_each_source(tmp_path, monkeypatch):
monkeypatch.setattr(builders_module, "entry_points", lambda group: [])
def test_download_of_a_release_that_no_source_provides_names_each_source(tmp_path):
sources = [LocalRegistry(tmp_path / "store"), LocalRegistry(tmp_path / "empty"), InstalledConnectors()]

with pytest.raises(TimeNetDatasetNotFoundError) as caught:
Expand Down Expand Up @@ -438,7 +434,6 @@ def test_download_names_the_store_when_no_other_registry_provides_its_copy(tmp_p

@pytest.mark.parametrize("case", ["force", "damaged"])
def test_download_counts_a_registry_over_the_directory_of_the_store_as_the_store(tmp_path, monkeypatch, case):
monkeypatch.setattr(builders_module, "entry_points", lambda group: [])
root = tmp_path / "reg"
LocalRegistry(root).put_dataset(make_dataset())
directory = root / "timenet/hello-world/1.0.0"
Expand Down Expand Up @@ -773,8 +768,7 @@ def test_load_names_the_buildable_version_when_a_pin_cannot_be_built(registry_ro


@pytest.mark.parametrize("connectors", [True, False])
def test_a_release_that_no_source_provides_names_how_to_get_it(tmp_path, monkeypatch, connectors):
monkeypatch.setattr(builders_module, "entry_points", lambda group: [])
def test_a_release_that_no_source_provides_names_how_to_get_it(tmp_path, connectors):
sources = [
LocalRegistry(tmp_path / "reg"),
LocalRegistry(tmp_path / "empty"),
Expand Down Expand Up @@ -858,8 +852,7 @@ def test_load_rejects_a_credentialed_dataset_from_a_hosted_registry(tmp_path):
assert not store.root.exists()


def test_the_access_hint_says_that_no_installed_connector_builds_the_release(tmp_path, monkeypatch):
monkeypatch.setattr(builders_module, "entry_points", lambda group: [])
def test_the_access_hint_says_that_no_installed_connector_builds_the_release(tmp_path):
card = _card("org/gated", Access.CREDENTIALED, "https://physionet.example/dua")
client = TimeNet(sources=[LocalRegistry(tmp_path / "store"), _hosted(tmp_path, card), InstalledConnectors()])

Expand Down
55 changes: 55 additions & 0 deletions packages/timenet/tests/test_conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Tests of the root ``conftest.py``: each test gets its own state."""

import os
from pathlib import Path
import subprocess
import sys

from timenet.config import settings


pytest_plugins = ["pytester"]

ROOT_CONFTEST = Path(__file__).resolve().parents[3] / "conftest.py"

PATHS = ("registry_path", "storage_dir", "cache_dir", "control_cache_dir")
"""The settings properties that point at local state."""


def test_each_test_starts_without_the_timenet_variables_of_the_shell(pytester, monkeypatch):
for name, value in {
"TIMENET_REGISTRY": "timenet://",
"TIMENET_STORAGE": "/shell/storage",
"TIMENET_CACHE": "/shell/cache",
"TIMENET_TOKEN": "secret",
"TIMENET_ISOLATION": "off",
}.items():
monkeypatch.setenv(name, value)
pytester.makeconftest(ROOT_CONFTEST.read_text())
pytester.makepyfile(
"""
import os

def test_only_the_home_is_set():
names = sorted(name for name in os.environ if name.startswith("TIMENET_"))
assert names == ["TIMENET_HOME"]
"""
)
pytester.runpytest().assert_outcomes(passed=1)


def test_every_settings_path_lies_in_the_home_of_the_test_also_in_a_child_process(tmp_path):
home = Path(os.environ["TIMENET_HOME"])
in_process = [getattr(settings(), name) for name in PATHS]
code = f"from timenet.config import settings; print(*(getattr(settings(), name) for name in {PATHS}), sep='\\n')"
child = subprocess.run([sys.executable, "-c", code], cwd=tmp_path, capture_output=True, text=True, check=True)
in_child = [Path(line) for line in child.stdout.splitlines()]
assert len(in_child) == len(in_process) == 4
assert all(path.is_relative_to(home) for path in [*in_process, *in_child])


def test_a_dotenv_file_in_the_working_directory_does_not_change_the_settings(monkeypatch, tmp_path):
(tmp_path / ".env").write_text("TIMENET_REGISTRY=timenet://\nTIMENET_TOKEN=secret\nTIMENET_ISOLATION=off\n")
monkeypatch.chdir(tmp_path)
cfg = settings()
assert (cfg.registry, cfg.token, cfg.isolation) == (None, None, "on")
9 changes: 0 additions & 9 deletions packages/timenet/tests/test_settings_remote.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
from timenet.config import settings


def test_token_defaults_to_none(monkeypatch, tmp_path):
# A .env in the working directory supplies a token of its own, and pydantic-settings reads it
# when the environment variable is absent. Run from an empty directory so the assertion tests
# the default rather than whatever the checkout happens to sit next to.
monkeypatch.chdir(tmp_path)
monkeypatch.delenv("TIMENET_TOKEN", raising=False)
assert settings().token is None


def test_token_read_from_env(monkeypatch):
monkeypatch.setenv("TIMENET_TOKEN", "tok_abc")
assert settings().token == "tok_abc"
4 changes: 1 addition & 3 deletions packages/timenet/tests/test_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import pytest

from timenet import builders
from timenet.errors import (
TimeFValidationError,
TimeNetRegistryError,
Expand Down Expand Up @@ -132,8 +131,7 @@ def test_installed_connectors_answer_not_found_for_another_version(monkeypatch):
assert InstalledConnectors().resolve("x/y", None) == NotFound()


def test_installed_connectors_without_a_package_answer_not_found(monkeypatch):
monkeypatch.setattr(builders, "entry_points", lambda group: [])
def test_installed_connectors_without_a_package_answer_not_found():
assert InstalledConnectors().resolve("timenet/hello-world", None) == NotFound()
assert "no connector package is installed" in str(InstalledConnectors())

Expand Down
Loading