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()