File size: 2,843 Bytes
f5f3483
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# Copyright 2024 The etils Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Pytest utils."""

from __future__ import annotations

import contextlib
import dataclasses
from typing import Any, Iterator

import pytest


# ==== Hermetic tests ===

_SKIP_NON_HERMETIC = False

# Non hermetic tests are explicitly marked and skipped if `_SKIP_NON_HERMETIC`
# is True.
non_hermetic = pytest.mark.skipif(
    _SKIP_NON_HERMETIC,
    reason='Non-hermetic test skipped.',
)

# ==== Subtests ===

_curr_context = None


@dataclasses.dataclass
class _SubtestContext:
  """Context of a test using `subtests` fixture.

  Attributes:
    subtests: Reference to the original `subtests` fixture output
    names: Stack of current nested `subtests.test`
  """

  subtests: Any
  names: list[str] = dataclasses.field(default_factory=list)


@contextlib.contextmanager
def subtest(name: str) -> Iterator[None]:
  """Contextmanager for a new subtest. To use with `with_subtests` fixture."""
  if not _curr_context:
    raise AssertionError(
        '`epy.testing.subtest` can only be called inside a '
        '`with_subtests` context.'
    )
  name = str(name)
  _curr_context.names.append(name)
  subtest_name = '/'.join(_curr_context.names)
  try:
    with _curr_context.subtests.test(msg=subtest_name):
      yield
  finally:
    out_name = _curr_context.names.pop()
    assert out_name == name  # Sanity check


@pytest.fixture
def with_subtests(subtests) -> Iterator[None]:
  """Fixture which activate subtests for global usage.

  This fixture is a small wrapper around `subtests` pytest extension fixing
  2 issues:

  * Global usage: https://github.com/pytest-dev/pytest-subtests/issues/44
  * Nested report: https://github.com/pytest-dev/pytest-subtests/issues/45

  Usage:

  ```python
  with_subtests = epy.testing.with_subtests  # Required to register the fixture

  @pytest.mark.usefixtures('with_subtests')
  def my_test():
    with epy.testing.subtest('a'):
      with epy.testing.subtest('b'):
        assert False
  ```

  Args:
    subtests: Subtest fixture

  Yields:
    None
  """
  global _curr_context
  if _curr_context is not None:
    raise AssertionError('Conflicting `subtests` context.')
  new_context = _SubtestContext(subtests=subtests)
  try:
    _curr_context = new_context
    yield
  finally:
    _curr_context = None