v0.165.0
  1import os
  2from collections.abc import Generator
  3from typing import Any
  4
  5from opentelemetry.sdk.metrics.export import InMemoryMetricReader
  6from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
  7    InMemorySpanExporter,
  8)
  9from plain.runtime import settings as plain_settings
 10from plain.runtime import setup
 11from plain.test.otel import install_test_meter, install_test_tracer
 12
 13import pytest
 14
 15from .browser import TestBrowser
 16
 17
 18def pytest_configure(config: Any) -> None:
 19    # Ensure tests run with PLAIN_ENV=test so plain.dev's dotenv loader picks
 20    # up `.env.test*` and skips `.env.local` for determinism. `plain test`
 21    # already sets this via the CLI dispatcher; this covers direct `pytest`.
 22    os.environ.setdefault("PLAIN_ENV", "test")
 23
 24    # Opportunistically load `.env.test*` via plain.dev's loader if it's
 25    # installed. plain.pytest doesn't require plain.dev, so we skip silently
 26    # when it's absent — install plain.dev if you want `.env` loading under
 27    # direct `pytest` invocations. Narrow to ModuleNotFoundError so a broken
 28    # plain.dev install (or a renamed symbol) surfaces instead of being
 29    # swallowed.
 30    try:
 31        from plain.dev.dotenv import load_dotenv_files
 32    except ModuleNotFoundError:
 33        pass
 34    else:
 35        load_dotenv_files()
 36
 37    # Run Plain setup before anything else
 38    setup()
 39
 40
 41class SettingsProxy:
 42    def __init__(self) -> None:
 43        self._original: dict[str, Any] = {}
 44
 45    def __getattr__(self, name: str) -> Any:
 46        return getattr(plain_settings, name)
 47
 48    def __setattr__(self, name: str, value: Any) -> None:
 49        if name.startswith("_"):
 50            super().__setattr__(name, value)
 51        else:
 52            if name not in self._original:
 53                self._original[name] = getattr(plain_settings, name, None)
 54            setattr(plain_settings, name, value)
 55
 56    def _restore(self) -> None:
 57        for key, value in self._original.items():
 58            setattr(plain_settings, key, value)
 59
 60
 61@pytest.fixture
 62def settings() -> Generator[SettingsProxy]:
 63    proxy = SettingsProxy()
 64    yield proxy
 65    proxy._restore()
 66
 67
 68@pytest.fixture
 69def otel_spans() -> InMemorySpanExporter:
 70    """OpenTelemetry spans emitted during the test.
 71
 72    Returns the InMemorySpanExporter; call `.get_finished_spans()` to read.
 73    """
 74    exporter = install_test_tracer()
 75    exporter.clear()
 76    return exporter
 77
 78
 79@pytest.fixture
 80def otel_metrics() -> InMemoryMetricReader:
 81    """OpenTelemetry metrics emitted during the test.
 82
 83    Returns the InMemoryMetricReader; call `.get_metrics_data()` or
 84    `.collect()` to read. Drains any prior observations on entry.
 85    """
 86    reader = install_test_meter()
 87    reader.get_metrics_data()  # drain
 88    return reader
 89
 90
 91@pytest.fixture
 92def testbrowser(browser: Any, request: Any) -> Generator[TestBrowser]:
 93    """Use playwright and pytest-playwright to run browser tests against a test server."""
 94    try:
 95        # Check if isolated_db fixture is available from the plain-postgres package.
 96        # If it is, then we need to run a server that has a database connection to the isolated database for this test.
 97        request.getfixturevalue("isolated_db")
 98
 99        from plain.postgres import get_connection
100        from plain.postgres.database_url import build_database_url
101
102        # Get a database url for the isolated db that we can have the plain server connect to also.
103        database_url = build_database_url(get_connection().settings_dict)
104    except pytest.FixtureLookupError:
105        # isolated_db fixture not available, use empty database_url
106        database_url = ""
107
108    testbrowser = TestBrowser(browser=browser, database_url=database_url)
109
110    try:
111        testbrowser.run_server()
112        yield testbrowser
113    finally:
114        testbrowser.cleanup_server()