Skip to content

Commit

Permalink
test: refactor plugin setup
Browse files Browse the repository at this point in the history
I found out you can actually pass plugin instance to pytester when
running it inline: this means we can grab the list of spans in a
sub-pytest if we run it inline.

This simplifies the overall architecture to split the tests in two mode:
- either we run them in a subprocess to test the whole lifecycle (e.g.
  upload errors are printed)
- either we run them inprocess and we can read the list of spans

This will make it easier in the future to replace pytest-opentelemetry
and test the spans that are emitted.

Change-Id: I75fd5057fef668c8e6dcbfe1f240f260df7e0bde
  • Loading branch information
jd committed Dec 21, 2024
1 parent 660e1a7 commit d602afb
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 162 deletions.
17 changes: 5 additions & 12 deletions pytest_mergify/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import typing

import pytest
import _pytest.main
import _pytest.config
Expand All @@ -12,21 +10,16 @@


class PytestMergify:
__name__ = "PytestMergify"

mergify_tracer: MergifyTracer

# Do this after pytest-opentelemetry has setup things
@pytest.hookimpl(trylast=True)
def pytest_configure(self, config: _pytest.config.Config) -> None:
kwargs = {}
api_url = config.getoption("--mergify-api-url")
if api_url is None:
self.reconfigure()
else:
self.reconfigure(api_url=api_url)

def reconfigure(self, *args: typing.Any, **kwargs: typing.Any) -> None:
self.mergify_tracer = MergifyTracer(*args, **kwargs)
if api_url is not None:
kwargs["api_url"] = api_url
self.mergify_tracer = MergifyTracer(**kwargs)

def pytest_terminal_summary(
self, terminalreporter: _pytest.terminal.TerminalReporter
Expand Down Expand Up @@ -92,4 +85,4 @@ def pytest_addoption(parser: _pytest.config.argparsing.Parser) -> None:


def pytest_configure(config: _pytest.config.Config) -> None:
config.pluginmanager.register(PytestMergify())
config.pluginmanager.register(PytestMergify(), name="PytestMergify")
61 changes: 32 additions & 29 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,49 @@
import collections.abc
import typing

import pytest
import _pytest.pytester
from opentelemetry.sdk import trace

import _pytest.config

from pytest_mergify import tracer
import pytest_mergify

pytest_plugins = ["pytester"]


ReconfigureT = typing.Callable[[dict[str, str]], None]


@pytest.fixture
def reconfigure_mergify_tracer(
pytestconfig: _pytest.config.Config,
@pytest.fixture(autouse=True)
def set_api_url(
monkeypatch: pytest.MonkeyPatch,
) -> collections.abc.Generator[ReconfigureT, None, None]:
) -> None:
# Always override API
monkeypatch.setenv("MERGIFY_API_URL", "http://localhost:9999")

plugin = pytestconfig.pluginmanager.get_plugin("PytestMergify")
assert plugin is not None
old_tracer: tracer.MergifyTracer = plugin.mergify_tracer

def _reconfigure(env: dict[str, str]) -> None:
# Set environment variables
for key, value in env.items():
monkeypatch.setenv(key, value)
plugin.reconfigure()
PytesterWithSpanReturnT = tuple[_pytest.pytester.RunResult, list[trace.ReadableSpan]]

yield _reconfigure
if plugin.mergify_tracer.tracer_provider is not None:
plugin.mergify_tracer.tracer_provider.shutdown()
plugin.mergify_tracer = old_tracer

class PytesterWithSpanT(typing.Protocol):
def __call__(self, code: str = ..., /) -> PytesterWithSpanReturnT: ...


_DEFAULT_PYTESTER_CODE = "def test_pass(): pass"


@pytest.fixture
def reconfigure_mergify_tracer_gha(
reconfigure_mergify_tracer: ReconfigureT,
) -> None:
reconfigure_mergify_tracer(
{"GITHUB_ACTIONS": "true", "GITHUB_REPOSITORY": "Mergifyio/pytest-mergify"}
)
def pytester_with_spans(
pytester: _pytest.pytester.Pytester, monkeypatch: pytest.MonkeyPatch
) -> PytesterWithSpanT:
def _run(
code: str = _DEFAULT_PYTESTER_CODE,
) -> PytesterWithSpanReturnT:
monkeypatch.setenv("_PYTEST_MERGIFY_TEST", "true")
plugin = pytest_mergify.PytestMergify()
pytester.makepyfile(code)
result = pytester.runpytest_inprocess(plugins=[plugin])
if code is _DEFAULT_PYTESTER_CODE:
result.assert_outcomes(passed=1)
assert plugin.mergify_tracer.exporter is not None
spans: list[trace.ReadableSpan] = (
plugin.mergify_tracer.exporter.get_finished_spans() # type: ignore[attr-defined]
)
return result, spans

return _run
61 changes: 25 additions & 36 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

import pytest_mergify

from tests import conftest


def test_plugin_is_loaded(pytestconfig: _pytest.config.Config) -> None:
plugin = pytestconfig.pluginmanager.get_plugin("pytest_mergify")
Expand All @@ -32,16 +30,13 @@ def test_foo():


def test_with_token_gha(
pytester: Pytester, reconfigure_mergify_tracer: conftest.ReconfigureT
pytester: Pytester,
monkeypatch: pytest.MonkeyPatch,
) -> None:
reconfigure_mergify_tracer(
{
"CI": "1",
"GITHUB_REPOSITORY": "Mergifyio/pytest-mergify",
"MERGIFY_TOKEN": "foobar",
"GITHUB_ACTIONS": "true",
},
)
monkeypatch.setenv("CI", "1")
monkeypatch.setenv("GITHUB_ACTIONS", "true")
monkeypatch.setenv("GITHUB_REPOSITORY", "Mergifyio/pytest-mergify")
monkeypatch.setenv("MERGIFY_TOKEN", "foobar")
pytester.makepyfile(
"""
def test_foo():
Expand All @@ -60,21 +55,25 @@ def test_foo():


def test_repo_name_github_actions(
pytestconfig: _pytest.config.Config,
reconfigure_mergify_tracer_gha: None,
pytester: Pytester,
monkeypatch: pytest.MonkeyPatch,
) -> None:
plugin = pytestconfig.pluginmanager.get_plugin("PytestMergify")
assert plugin is not None
monkeypatch.setenv("CI", "true")
monkeypatch.setenv("GITHUB_ACTIONS", "true")
monkeypatch.setenv("GITHUB_REPOSITORY", "Mergifyio/pytest-mergify")
plugin = pytest_mergify.PytestMergify()
pytester.makepyfile("")
pytester.runpytest_inprocess(plugins=[plugin])
assert plugin.mergify_tracer.repo_name == "Mergifyio/pytest-mergify"


def test_with_token_no_ci_provider(
pytester: Pytester,
reconfigure_mergify_tracer: conftest.ReconfigureT,
monkeypatch: pytest.MonkeyPatch,
) -> None:
reconfigure_mergify_tracer(
{"MERGIFY_TOKEN": "x", "CI": "1", "GITHUB_ACTIONS": "false"}
)
monkeypatch.setenv("MERGIFY_TOKEN", "x")
monkeypatch.setenv("CI", "1")
monkeypatch.setenv("GITHUB_ACTIONS", "false")
pytester.makepyfile(
"""
def test_foo():
Expand All @@ -91,27 +90,17 @@ def test_foo():

def test_errors_logs(
pytester: _pytest.pytester.Pytester,
reconfigure_mergify_tracer: conftest.ReconfigureT,
monkeypatch: pytest.MonkeyPatch,
) -> None:
# This will try to upload traces, but we don't have a real exporter so it will log errors.
reconfigure_mergify_tracer(
{
"MERGIFY_TOKEN": "true",
"CI": "1",
"GITHUB_ACTIONS": "true",
"GITHUB_REPOSITORY": "foo/bar",
}
)
monkeypatch.setenv("MERGIFY_TOKEN", "x")
monkeypatch.setenv("CI", "1")
monkeypatch.setenv("GITHUB_ACTIONS", "true")
monkeypatch.setenv("GITHUB_REPOSITORY", "foo/bar")
pytester.makepyfile(
"""
import pytest
from pytest_mergify import utils
def test_span(pytestconfig):
plugin = pytestconfig.pluginmanager.get_plugin("PytestMergify")
assert plugin is not None
assert plugin.mergify_tracer.exporter is not None
def test_pass():
pass
"""
)
result = pytester.runpytest_subprocess()
Expand Down
90 changes: 27 additions & 63 deletions tests/test_resources.py
Original file line number Diff line number Diff line change
@@ -1,82 +1,46 @@
import _pytest.pytester
import _pytest.config
import re
import typing

import pytest


from tests import conftest

from pytest_mergify import utils


def test_span_resources_attributes_ci(
pytester: _pytest.pytester.Pytester,
reconfigure_mergify_tracer: conftest.ReconfigureT,
pytester_with_spans: conftest.PytesterWithSpanT,
) -> None:
reconfigure_mergify_tracer({"_PYTEST_MERGIFY_TEST": "true"})
pytester.makepyfile(
"""
import pytest
from pytest_mergify import utils
def test_span(pytestconfig):
plugin = pytestconfig.pluginmanager.get_plugin("PytestMergify")
assert plugin is not None
assert plugin.mergify_tracer.exporter is not None
spans = plugin.mergify_tracer.exporter.get_finished_spans()
assert spans[0].resource.attributes["cicd.provider.name"] == utils.get_ci_provider()
"""
result, spans = pytester_with_spans()
assert all(
span.resource.attributes["cicd.provider.name"] == utils.get_ci_provider()
for span in spans
)
result = pytester.runpytest_subprocess()
result.assert_outcomes(passed=1)


def test_span_resources_attributes_pytest(
pytester: _pytest.pytester.Pytester,
reconfigure_mergify_tracer: conftest.ReconfigureT,
pytester_with_spans: conftest.PytesterWithSpanT,
) -> None:
reconfigure_mergify_tracer({"_PYTEST_MERGIFY_TEST": "true"})
pytester.makepyfile(
"""
import re
import pytest
def test_span(pytestconfig):
plugin = pytestconfig.pluginmanager.get_plugin("PytestMergify")
assert plugin is not None
assert plugin.mergify_tracer.exporter is not None
spans = plugin.mergify_tracer.exporter.get_finished_spans()
assert spans[0].resource.attributes["test.framework"] == "pytest"
assert re.match(r"\d\.", spans[0].resource.attributes["test.framework.version"])
"""
result, spans = pytester_with_spans()
assert all(
re.match(
r"\d\.",
typing.cast(str, span.resource.attributes["test.framework.version"]),
)
for span in spans
)
result = pytester.runpytest_subprocess()
result.assert_outcomes(passed=1)


def test_span_github_actions(
pytester: _pytest.pytester.Pytester,
reconfigure_mergify_tracer: conftest.ReconfigureT,
monkeypatch: pytest.MonkeyPatch,
pytester_with_spans: conftest.PytesterWithSpanT,
) -> None:
# Do a partial reconfig, half GHA, half local to have spans
reconfigure_mergify_tracer(
{
"GITHUB_ACTIONS": "true",
"GITHUB_REPOSITORY": "Mergifyio/pytest-mergify",
"_PYTEST_MERGIFY_TEST": "true",
},
)
pytester.makepyfile(
"""
import pytest
from pytest_mergify import utils
def test_span(pytestconfig):
plugin = pytestconfig.pluginmanager.get_plugin("PytestMergify")
assert plugin is not None
assert plugin.mergify_tracer.exporter is not None
spans = plugin.mergify_tracer.exporter.get_finished_spans()
assert spans[0].resource.attributes["vcs.repository.name"] == "Mergifyio/pytest-mergify"
"""
monkeypatch.setenv("GITHUB_ACTIONS", "true")
monkeypatch.setenv("GITHUB_REPOSITORY", "Mergifyio/pytest-mergify")
result, spans = pytester_with_spans()
assert (
spans[0].resource.attributes["vcs.repository.name"]
== "Mergifyio/pytest-mergify"
)
result = pytester.runpytest_subprocess()
result.assert_outcomes(passed=1)
29 changes: 7 additions & 22 deletions tests/test_spans.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,12 @@
import _pytest.pytester
import _pytest.config

from tests import conftest


def test_span(
pytester: _pytest.pytester.Pytester,
reconfigure_mergify_tracer: conftest.ReconfigureT,
pytester_with_spans: conftest.PytesterWithSpanT,
) -> None:
reconfigure_mergify_tracer({"_PYTEST_MERGIFY_TEST": "true"})
pytester.makepyfile(
"""
import pytest
from pytest_mergify import utils
def test_span(pytestconfig):
plugin = pytestconfig.pluginmanager.get_plugin("PytestMergify")
assert plugin is not None
assert plugin.mergify_tracer.exporter is not None
spans = plugin.mergify_tracer.exporter.get_finished_spans()
assert any(s.name == "pytestconfig setup" for s in spans)
"""
)
result = pytester.runpytest_subprocess()
result.assert_outcomes(passed=1)
result, spans = pytester_with_spans()
assert any(s.name == "test run" for s in spans)
assert any(s.name == "test_span.py::test_pass" for s in spans)
assert any(s.name == "test_span.py::test_pass::setup" for s in spans)
assert any(s.name == "test_span.py::test_pass::call" for s in spans)
assert any(s.name == "test_span.py::test_pass::teardown" for s in spans)

0 comments on commit d602afb

Please sign in to comment.