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