pytest-asyncio-concurrent 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytest_asyncio_concurrent-0.1.1/LICENSE +22 -0
- pytest_asyncio_concurrent-0.1.1/PKG-INFO +170 -0
- pytest_asyncio_concurrent-0.1.1/README.rst +117 -0
- pytest_asyncio_concurrent-0.1.1/pyproject.toml +116 -0
- pytest_asyncio_concurrent-0.1.1/pytest_asyncio_concurrent/__init__.py +8 -0
- pytest_asyncio_concurrent-0.1.1/pytest_asyncio_concurrent/plugin.py +270 -0
- pytest_asyncio_concurrent-0.1.1/pytest_asyncio_concurrent.egg-info/PKG-INFO +170 -0
- pytest_asyncio_concurrent-0.1.1/pytest_asyncio_concurrent.egg-info/SOURCES.txt +13 -0
- pytest_asyncio_concurrent-0.1.1/pytest_asyncio_concurrent.egg-info/dependency_links.txt +1 -0
- pytest_asyncio_concurrent-0.1.1/pytest_asyncio_concurrent.egg-info/entry_points.txt +2 -0
- pytest_asyncio_concurrent-0.1.1/pytest_asyncio_concurrent.egg-info/requires.txt +4 -0
- pytest_asyncio_concurrent-0.1.1/pytest_asyncio_concurrent.egg-info/top_level.txt +1 -0
- pytest_asyncio_concurrent-0.1.1/setup.cfg +4 -0
- pytest_asyncio_concurrent-0.1.1/tests/test_fixture.py +160 -0
- pytest_asyncio_concurrent-0.1.1/tests/test_grouping.py +144 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
The MIT License (MIT)
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2024 Zane Chen
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in
|
|
14
|
+
all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
22
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pytest-asyncio-concurrent
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Pytest plugin to execute python async tests concurrently.
|
|
5
|
+
Author-email: Zane Chen <czl970721@gmail.com>
|
|
6
|
+
Maintainer-email: Zane Chen <czl970721@gmail.com>
|
|
7
|
+
License:
|
|
8
|
+
The MIT License (MIT)
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2024 Zane Chen
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in
|
|
20
|
+
all copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
28
|
+
THE SOFTWARE.
|
|
29
|
+
|
|
30
|
+
Project-URL: Repository, https://github.com/czl9707/pytest-asyncio-concurrent
|
|
31
|
+
Project-URL: Homepage, https://github.com/czl9707/pytest-asyncio-concurrent
|
|
32
|
+
Project-URL: Issues, https://github.com/czl9707/pytest-asyncio-concurrent/issues
|
|
33
|
+
Classifier: Framework :: Pytest
|
|
34
|
+
Classifier: Development Status :: 4 - Beta
|
|
35
|
+
Classifier: Intended Audience :: Developers
|
|
36
|
+
Classifier: Topic :: Software Development :: Testing
|
|
37
|
+
Classifier: Operating System :: OS Independent
|
|
38
|
+
Classifier: Programming Language :: Python
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
45
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
46
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
47
|
+
Requires-Python: >=3.8
|
|
48
|
+
Description-Content-Type: text/x-rst
|
|
49
|
+
License-File: LICENSE
|
|
50
|
+
Requires-Dist: pytest>=6.2.0
|
|
51
|
+
Provides-Extra: testing
|
|
52
|
+
Requires-Dist: coverage>=7.6.0; extra == "testing"
|
|
53
|
+
|
|
54
|
+
=========================
|
|
55
|
+
pytest-asyncio-concurrent
|
|
56
|
+
=========================
|
|
57
|
+
|
|
58
|
+
.. image:: https://img.shields.io/pypi/v/pytest-asyncio-concurrent.svg
|
|
59
|
+
:target: https://pypi.org/project/pytest-asyncio-concurrent
|
|
60
|
+
:alt: PyPI version
|
|
61
|
+
|
|
62
|
+
.. image:: https://img.shields.io/pypi/pyversions/pytest-asyncio-concurrent.svg
|
|
63
|
+
:target: https://pypi.org/project/pytest-asyncio-concurrent
|
|
64
|
+
:alt: Python versions
|
|
65
|
+
|
|
66
|
+
.. image:: https://codecov.io/github/czl9707/pytest-asyncio-concurrent/graph/badge.svg?token=ENWHQBWQML
|
|
67
|
+
:target: https://codecov.io/gh/czl9707/pytest-asyncio-concurrent
|
|
68
|
+
:alt: Testing Coverage
|
|
69
|
+
|
|
70
|
+
.. image:: https://github.com/czl9707/pytest-asyncio-concurrent/actions/workflows/main.yml/badge.svg
|
|
71
|
+
:target: https://github.com/czl9707/pytest-asyncio-concurrent/actions/workflows/main.yml
|
|
72
|
+
:alt: See Build Status on GitHub Actions
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
System/Integration tests can take a really long time.
|
|
77
|
+
|
|
78
|
+
And ``pytest-asyncio-concurrent`` A pytest plugin is a solution for this by running asynchronous tests in true parallel, enabling faster execution for high I/O or network-bound test suites.
|
|
79
|
+
|
|
80
|
+
Unlike ``pytest-asyncio``, which runs async tests **sequentially**, ``pytest-asyncio-concurrent`` takes advantage of Python's asyncio capabilities to execute tests **concurrently** by specifying **async group**.
|
|
81
|
+
|
|
82
|
+
Note: This plugin would more or less `Break Test Isolation Principle` \(for none function scoped fixture\). Please make sure your tests is ok to run concurrently before you use this plugin.
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
Key Features
|
|
86
|
+
------------
|
|
87
|
+
|
|
88
|
+
- Giving the capability to run pytest async functions.
|
|
89
|
+
- Providing granular control over Concurrency
|
|
90
|
+
- Specifying Async Group to control tests that can run together.
|
|
91
|
+
- Specifying Timeout to avoid async tests taking forever. (Under Construction)
|
|
92
|
+
- Compatible with ``pytest-asyncio``.
|
|
93
|
+
|
|
94
|
+
Installation
|
|
95
|
+
------------
|
|
96
|
+
|
|
97
|
+
You can install "pytest-asyncio-concurrent" via `pip` from `PyPI`::
|
|
98
|
+
|
|
99
|
+
$ pip install pytest-asyncio-concurrent
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
Usage
|
|
103
|
+
-----
|
|
104
|
+
|
|
105
|
+
Run test Sequentially
|
|
106
|
+
|
|
107
|
+
.. code-block:: python
|
|
108
|
+
|
|
109
|
+
@pytest.mark.asyncio_concurrent
|
|
110
|
+
async def async_test_A():
|
|
111
|
+
res = await wait_for_something_async()
|
|
112
|
+
assert result.is_valid()
|
|
113
|
+
|
|
114
|
+
@pytest.mark.asyncio_concurrent
|
|
115
|
+
async def async_test_B():
|
|
116
|
+
res = await wait_for_something_async()
|
|
117
|
+
assert result.is_valid()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
Run tests Concurrently
|
|
121
|
+
|
|
122
|
+
.. code-block:: python
|
|
123
|
+
|
|
124
|
+
# the test below will run by itself
|
|
125
|
+
@pytest.mark.asyncio_concurrent
|
|
126
|
+
async def test_by_itself():
|
|
127
|
+
res = await wait_for_something_async()
|
|
128
|
+
assert result.is_valid()
|
|
129
|
+
|
|
130
|
+
# the two tests below will run concurrently
|
|
131
|
+
@pytest.mark.asyncio_concurrent(group="my_group")
|
|
132
|
+
async def test_groupA():
|
|
133
|
+
res = await wait_for_something_async()
|
|
134
|
+
assert result.is_valid()
|
|
135
|
+
|
|
136
|
+
@pytest.mark.asyncio_concurrent(group="my_group")
|
|
137
|
+
async def test_groupB():
|
|
138
|
+
res = await wait_for_something_async()
|
|
139
|
+
assert result.is_valid()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
Parametrized Tests
|
|
143
|
+
|
|
144
|
+
.. code-block:: python
|
|
145
|
+
|
|
146
|
+
# the parametrized tests below will run sequential
|
|
147
|
+
@pytest.mark.asyncio_concurrent
|
|
148
|
+
@pytest.parametrize("p", [0, 1, 2])
|
|
149
|
+
async def test_parametrize_sequential(p):
|
|
150
|
+
res = await wait_for_something_async()
|
|
151
|
+
assert result.is_valid()
|
|
152
|
+
|
|
153
|
+
# the parametrized tests below will run concurrently
|
|
154
|
+
@pytest.mark.asyncio_concurrent(group="my_group")
|
|
155
|
+
@pytest.parametrize("p", [0, 1, 2])
|
|
156
|
+
async def test_parametrize_concurrent():
|
|
157
|
+
res = await wait_for_something_async()
|
|
158
|
+
assert result.is_valid()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
Contributing
|
|
162
|
+
------------
|
|
163
|
+
|
|
164
|
+
Contributions are very welcome. Tests can be run with ``tox``, please ensure
|
|
165
|
+
the coverage at least stays the same before you submit a pull request.
|
|
166
|
+
|
|
167
|
+
License
|
|
168
|
+
-------
|
|
169
|
+
|
|
170
|
+
Distributed under the terms of the ``MIT`` license, "pytest-asyncio-concurrent" is free and open source software
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
=========================
|
|
2
|
+
pytest-asyncio-concurrent
|
|
3
|
+
=========================
|
|
4
|
+
|
|
5
|
+
.. image:: https://img.shields.io/pypi/v/pytest-asyncio-concurrent.svg
|
|
6
|
+
:target: https://pypi.org/project/pytest-asyncio-concurrent
|
|
7
|
+
:alt: PyPI version
|
|
8
|
+
|
|
9
|
+
.. image:: https://img.shields.io/pypi/pyversions/pytest-asyncio-concurrent.svg
|
|
10
|
+
:target: https://pypi.org/project/pytest-asyncio-concurrent
|
|
11
|
+
:alt: Python versions
|
|
12
|
+
|
|
13
|
+
.. image:: https://codecov.io/github/czl9707/pytest-asyncio-concurrent/graph/badge.svg?token=ENWHQBWQML
|
|
14
|
+
:target: https://codecov.io/gh/czl9707/pytest-asyncio-concurrent
|
|
15
|
+
:alt: Testing Coverage
|
|
16
|
+
|
|
17
|
+
.. image:: https://github.com/czl9707/pytest-asyncio-concurrent/actions/workflows/main.yml/badge.svg
|
|
18
|
+
:target: https://github.com/czl9707/pytest-asyncio-concurrent/actions/workflows/main.yml
|
|
19
|
+
:alt: See Build Status on GitHub Actions
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
System/Integration tests can take a really long time.
|
|
24
|
+
|
|
25
|
+
And ``pytest-asyncio-concurrent`` A pytest plugin is a solution for this by running asynchronous tests in true parallel, enabling faster execution for high I/O or network-bound test suites.
|
|
26
|
+
|
|
27
|
+
Unlike ``pytest-asyncio``, which runs async tests **sequentially**, ``pytest-asyncio-concurrent`` takes advantage of Python's asyncio capabilities to execute tests **concurrently** by specifying **async group**.
|
|
28
|
+
|
|
29
|
+
Note: This plugin would more or less `Break Test Isolation Principle` \(for none function scoped fixture\). Please make sure your tests is ok to run concurrently before you use this plugin.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
Key Features
|
|
33
|
+
------------
|
|
34
|
+
|
|
35
|
+
- Giving the capability to run pytest async functions.
|
|
36
|
+
- Providing granular control over Concurrency
|
|
37
|
+
- Specifying Async Group to control tests that can run together.
|
|
38
|
+
- Specifying Timeout to avoid async tests taking forever. (Under Construction)
|
|
39
|
+
- Compatible with ``pytest-asyncio``.
|
|
40
|
+
|
|
41
|
+
Installation
|
|
42
|
+
------------
|
|
43
|
+
|
|
44
|
+
You can install "pytest-asyncio-concurrent" via `pip` from `PyPI`::
|
|
45
|
+
|
|
46
|
+
$ pip install pytest-asyncio-concurrent
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
Usage
|
|
50
|
+
-----
|
|
51
|
+
|
|
52
|
+
Run test Sequentially
|
|
53
|
+
|
|
54
|
+
.. code-block:: python
|
|
55
|
+
|
|
56
|
+
@pytest.mark.asyncio_concurrent
|
|
57
|
+
async def async_test_A():
|
|
58
|
+
res = await wait_for_something_async()
|
|
59
|
+
assert result.is_valid()
|
|
60
|
+
|
|
61
|
+
@pytest.mark.asyncio_concurrent
|
|
62
|
+
async def async_test_B():
|
|
63
|
+
res = await wait_for_something_async()
|
|
64
|
+
assert result.is_valid()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
Run tests Concurrently
|
|
68
|
+
|
|
69
|
+
.. code-block:: python
|
|
70
|
+
|
|
71
|
+
# the test below will run by itself
|
|
72
|
+
@pytest.mark.asyncio_concurrent
|
|
73
|
+
async def test_by_itself():
|
|
74
|
+
res = await wait_for_something_async()
|
|
75
|
+
assert result.is_valid()
|
|
76
|
+
|
|
77
|
+
# the two tests below will run concurrently
|
|
78
|
+
@pytest.mark.asyncio_concurrent(group="my_group")
|
|
79
|
+
async def test_groupA():
|
|
80
|
+
res = await wait_for_something_async()
|
|
81
|
+
assert result.is_valid()
|
|
82
|
+
|
|
83
|
+
@pytest.mark.asyncio_concurrent(group="my_group")
|
|
84
|
+
async def test_groupB():
|
|
85
|
+
res = await wait_for_something_async()
|
|
86
|
+
assert result.is_valid()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
Parametrized Tests
|
|
90
|
+
|
|
91
|
+
.. code-block:: python
|
|
92
|
+
|
|
93
|
+
# the parametrized tests below will run sequential
|
|
94
|
+
@pytest.mark.asyncio_concurrent
|
|
95
|
+
@pytest.parametrize("p", [0, 1, 2])
|
|
96
|
+
async def test_parametrize_sequential(p):
|
|
97
|
+
res = await wait_for_something_async()
|
|
98
|
+
assert result.is_valid()
|
|
99
|
+
|
|
100
|
+
# the parametrized tests below will run concurrently
|
|
101
|
+
@pytest.mark.asyncio_concurrent(group="my_group")
|
|
102
|
+
@pytest.parametrize("p", [0, 1, 2])
|
|
103
|
+
async def test_parametrize_concurrent():
|
|
104
|
+
res = await wait_for_something_async()
|
|
105
|
+
assert result.is_valid()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
Contributing
|
|
109
|
+
------------
|
|
110
|
+
|
|
111
|
+
Contributions are very welcome. Tests can be run with ``tox``, please ensure
|
|
112
|
+
the coverage at least stays the same before you submit a pull request.
|
|
113
|
+
|
|
114
|
+
License
|
|
115
|
+
-------
|
|
116
|
+
|
|
117
|
+
Distributed under the terms of the ``MIT`` license, "pytest-asyncio-concurrent" is free and open source software
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"setuptools>=61.0",
|
|
4
|
+
]
|
|
5
|
+
build-backend = "setuptools.build_meta"
|
|
6
|
+
|
|
7
|
+
[project]
|
|
8
|
+
name = "pytest-asyncio-concurrent"
|
|
9
|
+
description = "Pytest plugin to execute python async tests concurrently."
|
|
10
|
+
version = "0.1.1"
|
|
11
|
+
readme = "README.rst"
|
|
12
|
+
requires-python = ">=3.8"
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Zane Chen", email = "czl970721@gmail.com" },
|
|
15
|
+
]
|
|
16
|
+
maintainers = [
|
|
17
|
+
{ name = "Zane Chen", email = "czl970721@gmail.com" },
|
|
18
|
+
]
|
|
19
|
+
license = {file = "LICENSE"}
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Framework :: Pytest",
|
|
22
|
+
"Development Status :: 4 - Beta",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"Topic :: Software Development :: Testing",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
"Programming Language :: Python",
|
|
27
|
+
"Programming Language :: Python :: 3.8",
|
|
28
|
+
"Programming Language :: Python :: 3.9",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13",
|
|
33
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
34
|
+
"License :: OSI Approved :: MIT License",
|
|
35
|
+
]
|
|
36
|
+
dependencies = [
|
|
37
|
+
"pytest>=6.2.0",
|
|
38
|
+
]
|
|
39
|
+
optional-dependencies.testing = [
|
|
40
|
+
"coverage>=7.6.0",
|
|
41
|
+
]
|
|
42
|
+
[project.urls]
|
|
43
|
+
Repository = "https://github.com/czl9707/pytest-asyncio-concurrent"
|
|
44
|
+
Homepage = "https://github.com/czl9707/pytest-asyncio-concurrent"
|
|
45
|
+
Issues = "https://github.com/czl9707/pytest-asyncio-concurrent/issues"
|
|
46
|
+
|
|
47
|
+
[project.entry-points.pytest11]
|
|
48
|
+
asyncio-concurrent = "pytest_asyncio_concurrent.plugin"
|
|
49
|
+
|
|
50
|
+
[tool.setuptools]
|
|
51
|
+
packages = [
|
|
52
|
+
"pytest_asyncio_concurrent",
|
|
53
|
+
]
|
|
54
|
+
include-package-data = true
|
|
55
|
+
license-files = [
|
|
56
|
+
"LICENSE",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[tool.black]
|
|
60
|
+
line-length = 100
|
|
61
|
+
|
|
62
|
+
[tool.flake8]
|
|
63
|
+
max-line-length = 100
|
|
64
|
+
|
|
65
|
+
[tool.coverage.run]
|
|
66
|
+
source = [
|
|
67
|
+
"pytest_asyncio_concurrent",
|
|
68
|
+
]
|
|
69
|
+
branch = true
|
|
70
|
+
data_file = "coverage/coverage"
|
|
71
|
+
omit = [
|
|
72
|
+
"*/_version.py",
|
|
73
|
+
]
|
|
74
|
+
parallel = true
|
|
75
|
+
concurrency = [
|
|
76
|
+
"multiprocessing"
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
[tool.coverage.report]
|
|
80
|
+
show_missing = true
|
|
81
|
+
|
|
82
|
+
[tool.bumpversion]
|
|
83
|
+
current_version = "0.1.1"
|
|
84
|
+
parse = """(?x)
|
|
85
|
+
(?P<major>0|[1-9]\\d*)\\.
|
|
86
|
+
(?P<minor>0|[1-9]\\d*)\\.
|
|
87
|
+
(?P<patch>0|[1-9]\\d*)
|
|
88
|
+
(?:
|
|
89
|
+
-prerelease-(?P<pre>0|[1-9]\\d*)
|
|
90
|
+
)?
|
|
91
|
+
"""
|
|
92
|
+
serialize = [
|
|
93
|
+
"{major}.{minor}.{patch}-prerelease-{pre}",
|
|
94
|
+
"{major}.{minor}.{patch}",
|
|
95
|
+
]
|
|
96
|
+
search = "{current_version}"
|
|
97
|
+
replace = "{new_version}"
|
|
98
|
+
regex = false
|
|
99
|
+
ignore_missing_version = false
|
|
100
|
+
ignore_missing_files = false
|
|
101
|
+
tag = true
|
|
102
|
+
sign_tags = false
|
|
103
|
+
tag_name = "v{new_version}"
|
|
104
|
+
tag_message = "Bump version: {current_version} → {new_version}"
|
|
105
|
+
allow_dirty = false
|
|
106
|
+
commit = true
|
|
107
|
+
message = "Bump version: {current_version} → {new_version}"
|
|
108
|
+
commit_args = ""
|
|
109
|
+
setup_hooks = []
|
|
110
|
+
pre_commit_hooks = []
|
|
111
|
+
post_commit_hooks = []
|
|
112
|
+
|
|
113
|
+
[[tool.bumpversion.files]]
|
|
114
|
+
filename = "pyproject.toml"
|
|
115
|
+
search = "version = \"{current_version}\""
|
|
116
|
+
replace = "version = \"{new_version}\""
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Any, Callable, Generator, List, Optional, Coroutine, Dict, cast
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from _pytest import scope, timing, outcomes, runner
|
|
7
|
+
from pytest import (
|
|
8
|
+
CallInfo,
|
|
9
|
+
ExceptionInfo,
|
|
10
|
+
FixtureDef,
|
|
11
|
+
Item,
|
|
12
|
+
Session,
|
|
13
|
+
Config,
|
|
14
|
+
Function,
|
|
15
|
+
Mark,
|
|
16
|
+
TestReport,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# =========================== # Config & Collection # =========================== #
|
|
21
|
+
def pytest_configure(config: Config) -> None:
|
|
22
|
+
config.addinivalue_line(
|
|
23
|
+
"markers",
|
|
24
|
+
"asyncio_concurrent(group, timeout): " "mark the tests to run concurrently",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.hookimpl(specname="pytest_runtestloop", wrapper=True)
|
|
29
|
+
def pytest_runtestloop_wrap_items_by_group(session: Session) -> Generator[None, Any, Any]:
|
|
30
|
+
"""
|
|
31
|
+
Group items with same asyncio concurrent group together,
|
|
32
|
+
so they can be executed together in outer loop.
|
|
33
|
+
"""
|
|
34
|
+
asycio_concurrent_groups: Dict[str, List[Function]] = {}
|
|
35
|
+
items = session.items
|
|
36
|
+
|
|
37
|
+
for item in items:
|
|
38
|
+
if _get_asyncio_concurrent_mark(item) is None:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
concurrent_group_name = _get_asyncio_concurrent_group(item)
|
|
42
|
+
if concurrent_group_name not in asycio_concurrent_groups:
|
|
43
|
+
asycio_concurrent_groups[concurrent_group_name] = []
|
|
44
|
+
asycio_concurrent_groups[concurrent_group_name].append(cast(Function, item))
|
|
45
|
+
|
|
46
|
+
for asyncio_items in asycio_concurrent_groups.values():
|
|
47
|
+
for item in asyncio_items:
|
|
48
|
+
items.remove(item)
|
|
49
|
+
|
|
50
|
+
for group_name, asyncio_items in asycio_concurrent_groups.items():
|
|
51
|
+
items.append(group_asyncio_concurrent_function(group_name, asyncio_items))
|
|
52
|
+
|
|
53
|
+
result = yield
|
|
54
|
+
|
|
55
|
+
groups = [item for item in items if isinstance(item, AsyncioConcurrentGroup)]
|
|
56
|
+
for group in groups:
|
|
57
|
+
items.remove(group)
|
|
58
|
+
for item in group._pytest_asyncio_concurrent_children:
|
|
59
|
+
items.append(item)
|
|
60
|
+
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AsyncioConcurrentGroup(Function):
|
|
65
|
+
_pytest_asyncio_concurrent_children: List[Function] = []
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def group_asyncio_concurrent_function(
|
|
69
|
+
group_name: str, children: List[Function]
|
|
70
|
+
) -> AsyncioConcurrentGroup:
|
|
71
|
+
parent = None
|
|
72
|
+
for childFunc in children:
|
|
73
|
+
p_it = childFunc.iter_parents()
|
|
74
|
+
next(p_it)
|
|
75
|
+
func_parent = next(p_it)
|
|
76
|
+
|
|
77
|
+
if not parent:
|
|
78
|
+
parent = func_parent
|
|
79
|
+
elif parent is not func_parent:
|
|
80
|
+
raise Exception("test case within same group should have same parent.")
|
|
81
|
+
|
|
82
|
+
_rewrite_function_scoped_fixture(childFunc)
|
|
83
|
+
|
|
84
|
+
g_function = AsyncioConcurrentGroup.from_parent(
|
|
85
|
+
parent,
|
|
86
|
+
name=f"ayncio_concurrent_test_group[{group_name}]",
|
|
87
|
+
callobj=lambda: None,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
g_function._pytest_asyncio_concurrent_children = children
|
|
91
|
+
return g_function
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _rewrite_function_scoped_fixture(item: Function):
|
|
95
|
+
for name, fixturedefs in item._request._arg2fixturedefs.items():
|
|
96
|
+
if hasattr(item, "callspec") and name in item.callspec.params.keys():
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
if fixturedefs[-1]._scope != scope.Scope.Function:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
new_fixdef = FixtureDef(
|
|
103
|
+
config=item.config,
|
|
104
|
+
baseid=fixturedefs[-1].baseid,
|
|
105
|
+
argname=fixturedefs[-1].argname,
|
|
106
|
+
func=fixturedefs[-1].func,
|
|
107
|
+
scope=fixturedefs[-1]._scope,
|
|
108
|
+
params=fixturedefs[-1].params,
|
|
109
|
+
ids=fixturedefs[-1].ids,
|
|
110
|
+
_ispytest=True,
|
|
111
|
+
)
|
|
112
|
+
fixturedefs = list(fixturedefs[0:-1]) + [new_fixdef]
|
|
113
|
+
item._request._arg2fixturedefs[name] = fixturedefs
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# =========================== # function call & setup & teardown #===========================#
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@pytest.hookimpl(specname="pytest_runtest_setup", wrapper=True)
|
|
120
|
+
def pytest_runtest_setup_group_children(item: Item) -> Generator[None, None, None]:
|
|
121
|
+
result = yield
|
|
122
|
+
|
|
123
|
+
if not isinstance(item, AsyncioConcurrentGroup):
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
for childFunc in item._pytest_asyncio_concurrent_children:
|
|
127
|
+
call = CallInfo.from_call(_pytest_simple_setup(childFunc), "setup")
|
|
128
|
+
report: TestReport = childFunc.ihook.pytest_runtest_makereport(item=childFunc, call=call)
|
|
129
|
+
childFunc.ihook.pytest_runtest_logreport(report=report)
|
|
130
|
+
|
|
131
|
+
return result
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _pytest_simple_setup(item: Item) -> Callable[[], None]:
|
|
135
|
+
def inner() -> None:
|
|
136
|
+
item.session._setupstate.stack[item] = ([item.teardown], None)
|
|
137
|
+
item.setup()
|
|
138
|
+
|
|
139
|
+
return inner
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@pytest.hookimpl(specname="pytest_pyfunc_call", wrapper=True)
|
|
143
|
+
def pytest_pyfunc_call_handle_group(pyfuncitem: Function) -> Generator[None, Any, Any]:
|
|
144
|
+
result = yield
|
|
145
|
+
if not isinstance(pyfuncitem, AsyncioConcurrentGroup):
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
coros: List[Coroutine] = []
|
|
149
|
+
loop = asyncio.get_event_loop()
|
|
150
|
+
|
|
151
|
+
for childFunc in pyfuncitem._pytest_asyncio_concurrent_children:
|
|
152
|
+
coros.append(_async_callinfo_from_call(_pytest_function_call_async(childFunc)))
|
|
153
|
+
|
|
154
|
+
call_result = loop.run_until_complete(asyncio.gather(*coros))
|
|
155
|
+
|
|
156
|
+
for childFunc, call in zip(pyfuncitem._pytest_asyncio_concurrent_children, call_result):
|
|
157
|
+
report: TestReport = childFunc.ihook.pytest_runtest_makereport(item=childFunc, call=call)
|
|
158
|
+
childFunc.ihook.pytest_runtest_logreport(report=report)
|
|
159
|
+
|
|
160
|
+
return result
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _pytest_function_call_async(item: Function) -> Callable[[], Coroutine]:
|
|
164
|
+
async def inner() -> Any:
|
|
165
|
+
testfunction = item.obj
|
|
166
|
+
testargs = {arg: item.funcargs[arg] for arg in item._fixtureinfo.argnames}
|
|
167
|
+
return await testfunction(**testargs)
|
|
168
|
+
|
|
169
|
+
return inner
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# referencing CallInfo.from_call
|
|
173
|
+
async def _async_callinfo_from_call(func: Callable[[], Coroutine]) -> CallInfo:
|
|
174
|
+
excinfo = None
|
|
175
|
+
start = timing.time()
|
|
176
|
+
precise_start = timing.perf_counter()
|
|
177
|
+
try:
|
|
178
|
+
result = await func()
|
|
179
|
+
except BaseException:
|
|
180
|
+
excinfo = ExceptionInfo.from_current()
|
|
181
|
+
if isinstance(excinfo.value, outcomes.Exit):
|
|
182
|
+
raise
|
|
183
|
+
result = None
|
|
184
|
+
|
|
185
|
+
precise_stop = timing.perf_counter()
|
|
186
|
+
duration = precise_stop - precise_start
|
|
187
|
+
stop = timing.time()
|
|
188
|
+
|
|
189
|
+
callInfo: CallInfo = CallInfo(
|
|
190
|
+
start=start,
|
|
191
|
+
stop=stop,
|
|
192
|
+
duration=duration,
|
|
193
|
+
when="call",
|
|
194
|
+
result=result,
|
|
195
|
+
excinfo=excinfo,
|
|
196
|
+
_ispytest=True,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return callInfo
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@pytest.hookimpl(specname="pytest_runtest_teardown", wrapper=True)
|
|
203
|
+
def pytest_runtest_teardown_group_children(
|
|
204
|
+
item: Item, nextitem: Optional[Item]
|
|
205
|
+
) -> Generator[None, None, None]:
|
|
206
|
+
if not isinstance(item, AsyncioConcurrentGroup):
|
|
207
|
+
return (yield)
|
|
208
|
+
|
|
209
|
+
for childFunc in item._pytest_asyncio_concurrent_children:
|
|
210
|
+
call = CallInfo.from_call(_pytest_simple_teardown(childFunc), "teardown")
|
|
211
|
+
report: TestReport = childFunc.ihook.pytest_runtest_makereport(item=childFunc, call=call)
|
|
212
|
+
childFunc.ihook.pytest_runtest_logreport(report=report)
|
|
213
|
+
|
|
214
|
+
return (yield)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _pytest_simple_teardown(item: Item) -> Callable[[], None]:
|
|
218
|
+
def inner() -> None:
|
|
219
|
+
finalizers, _ = item.session._setupstate.stack.pop(item)
|
|
220
|
+
these_exceptions = []
|
|
221
|
+
while finalizers:
|
|
222
|
+
fin = finalizers.pop()
|
|
223
|
+
try:
|
|
224
|
+
fin()
|
|
225
|
+
except Exception as e:
|
|
226
|
+
these_exceptions.append(e)
|
|
227
|
+
|
|
228
|
+
if len(these_exceptions) == 1:
|
|
229
|
+
raise these_exceptions[0]
|
|
230
|
+
elif these_exceptions:
|
|
231
|
+
msg = f"Errors during tearing down {item}"
|
|
232
|
+
raise BaseExceptionGroup(msg, these_exceptions[::-1])
|
|
233
|
+
|
|
234
|
+
return inner
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# =========================== # reporting #===========================#
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@pytest.hookimpl(specname="pytest_runtest_protocol", tryfirst=True)
|
|
241
|
+
def pytest_runtest_protocol_skip_logging_for_group(
|
|
242
|
+
item: Item, nextitem: Optional[Item]
|
|
243
|
+
) -> Optional[bool]:
|
|
244
|
+
if not isinstance(item, AsyncioConcurrentGroup):
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
for childFunc in item._pytest_asyncio_concurrent_children:
|
|
248
|
+
childFunc.ihook.pytest_runtest_logstart(
|
|
249
|
+
nodeid=childFunc.nodeid, location=childFunc.location
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
runner.runtestprotocol(item, nextitem=nextitem, log=False) # disable logging for group function
|
|
253
|
+
|
|
254
|
+
for childFunc in item._pytest_asyncio_concurrent_children:
|
|
255
|
+
childFunc.ihook.pytest_runtest_logfinish(
|
|
256
|
+
nodeid=childFunc.nodeid, location=childFunc.location
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
return True
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _get_asyncio_concurrent_mark(item: Item) -> Optional[Mark]:
|
|
263
|
+
return item.get_closest_marker("asyncio_concurrent")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _get_asyncio_concurrent_group(item: Item) -> str:
|
|
267
|
+
marker = item.get_closest_marker("asyncio_concurrent")
|
|
268
|
+
assert marker is not None
|
|
269
|
+
|
|
270
|
+
return marker.kwargs.get("group", f"anonymous_[{uuid.uuid4()}]")
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pytest-asyncio-concurrent
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Pytest plugin to execute python async tests concurrently.
|
|
5
|
+
Author-email: Zane Chen <czl970721@gmail.com>
|
|
6
|
+
Maintainer-email: Zane Chen <czl970721@gmail.com>
|
|
7
|
+
License:
|
|
8
|
+
The MIT License (MIT)
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2024 Zane Chen
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in
|
|
20
|
+
all copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
28
|
+
THE SOFTWARE.
|
|
29
|
+
|
|
30
|
+
Project-URL: Repository, https://github.com/czl9707/pytest-asyncio-concurrent
|
|
31
|
+
Project-URL: Homepage, https://github.com/czl9707/pytest-asyncio-concurrent
|
|
32
|
+
Project-URL: Issues, https://github.com/czl9707/pytest-asyncio-concurrent/issues
|
|
33
|
+
Classifier: Framework :: Pytest
|
|
34
|
+
Classifier: Development Status :: 4 - Beta
|
|
35
|
+
Classifier: Intended Audience :: Developers
|
|
36
|
+
Classifier: Topic :: Software Development :: Testing
|
|
37
|
+
Classifier: Operating System :: OS Independent
|
|
38
|
+
Classifier: Programming Language :: Python
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
45
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
46
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
47
|
+
Requires-Python: >=3.8
|
|
48
|
+
Description-Content-Type: text/x-rst
|
|
49
|
+
License-File: LICENSE
|
|
50
|
+
Requires-Dist: pytest>=6.2.0
|
|
51
|
+
Provides-Extra: testing
|
|
52
|
+
Requires-Dist: coverage>=7.6.0; extra == "testing"
|
|
53
|
+
|
|
54
|
+
=========================
|
|
55
|
+
pytest-asyncio-concurrent
|
|
56
|
+
=========================
|
|
57
|
+
|
|
58
|
+
.. image:: https://img.shields.io/pypi/v/pytest-asyncio-concurrent.svg
|
|
59
|
+
:target: https://pypi.org/project/pytest-asyncio-concurrent
|
|
60
|
+
:alt: PyPI version
|
|
61
|
+
|
|
62
|
+
.. image:: https://img.shields.io/pypi/pyversions/pytest-asyncio-concurrent.svg
|
|
63
|
+
:target: https://pypi.org/project/pytest-asyncio-concurrent
|
|
64
|
+
:alt: Python versions
|
|
65
|
+
|
|
66
|
+
.. image:: https://codecov.io/github/czl9707/pytest-asyncio-concurrent/graph/badge.svg?token=ENWHQBWQML
|
|
67
|
+
:target: https://codecov.io/gh/czl9707/pytest-asyncio-concurrent
|
|
68
|
+
:alt: Testing Coverage
|
|
69
|
+
|
|
70
|
+
.. image:: https://github.com/czl9707/pytest-asyncio-concurrent/actions/workflows/main.yml/badge.svg
|
|
71
|
+
:target: https://github.com/czl9707/pytest-asyncio-concurrent/actions/workflows/main.yml
|
|
72
|
+
:alt: See Build Status on GitHub Actions
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
System/Integration tests can take a really long time.
|
|
77
|
+
|
|
78
|
+
And ``pytest-asyncio-concurrent`` A pytest plugin is a solution for this by running asynchronous tests in true parallel, enabling faster execution for high I/O or network-bound test suites.
|
|
79
|
+
|
|
80
|
+
Unlike ``pytest-asyncio``, which runs async tests **sequentially**, ``pytest-asyncio-concurrent`` takes advantage of Python's asyncio capabilities to execute tests **concurrently** by specifying **async group**.
|
|
81
|
+
|
|
82
|
+
Note: This plugin would more or less `Break Test Isolation Principle` \(for none function scoped fixture\). Please make sure your tests is ok to run concurrently before you use this plugin.
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
Key Features
|
|
86
|
+
------------
|
|
87
|
+
|
|
88
|
+
- Giving the capability to run pytest async functions.
|
|
89
|
+
- Providing granular control over Concurrency
|
|
90
|
+
- Specifying Async Group to control tests that can run together.
|
|
91
|
+
- Specifying Timeout to avoid async tests taking forever. (Under Construction)
|
|
92
|
+
- Compatible with ``pytest-asyncio``.
|
|
93
|
+
|
|
94
|
+
Installation
|
|
95
|
+
------------
|
|
96
|
+
|
|
97
|
+
You can install "pytest-asyncio-concurrent" via `pip` from `PyPI`::
|
|
98
|
+
|
|
99
|
+
$ pip install pytest-asyncio-concurrent
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
Usage
|
|
103
|
+
-----
|
|
104
|
+
|
|
105
|
+
Run test Sequentially
|
|
106
|
+
|
|
107
|
+
.. code-block:: python
|
|
108
|
+
|
|
109
|
+
@pytest.mark.asyncio_concurrent
|
|
110
|
+
async def async_test_A():
|
|
111
|
+
res = await wait_for_something_async()
|
|
112
|
+
assert result.is_valid()
|
|
113
|
+
|
|
114
|
+
@pytest.mark.asyncio_concurrent
|
|
115
|
+
async def async_test_B():
|
|
116
|
+
res = await wait_for_something_async()
|
|
117
|
+
assert result.is_valid()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
Run tests Concurrently
|
|
121
|
+
|
|
122
|
+
.. code-block:: python
|
|
123
|
+
|
|
124
|
+
# the test below will run by itself
|
|
125
|
+
@pytest.mark.asyncio_concurrent
|
|
126
|
+
async def test_by_itself():
|
|
127
|
+
res = await wait_for_something_async()
|
|
128
|
+
assert result.is_valid()
|
|
129
|
+
|
|
130
|
+
# the two tests below will run concurrently
|
|
131
|
+
@pytest.mark.asyncio_concurrent(group="my_group")
|
|
132
|
+
async def test_groupA():
|
|
133
|
+
res = await wait_for_something_async()
|
|
134
|
+
assert result.is_valid()
|
|
135
|
+
|
|
136
|
+
@pytest.mark.asyncio_concurrent(group="my_group")
|
|
137
|
+
async def test_groupB():
|
|
138
|
+
res = await wait_for_something_async()
|
|
139
|
+
assert result.is_valid()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
Parametrized Tests
|
|
143
|
+
|
|
144
|
+
.. code-block:: python
|
|
145
|
+
|
|
146
|
+
# the parametrized tests below will run sequential
|
|
147
|
+
@pytest.mark.asyncio_concurrent
|
|
148
|
+
@pytest.parametrize("p", [0, 1, 2])
|
|
149
|
+
async def test_parametrize_sequential(p):
|
|
150
|
+
res = await wait_for_something_async()
|
|
151
|
+
assert result.is_valid()
|
|
152
|
+
|
|
153
|
+
# the parametrized tests below will run concurrently
|
|
154
|
+
@pytest.mark.asyncio_concurrent(group="my_group")
|
|
155
|
+
@pytest.parametrize("p", [0, 1, 2])
|
|
156
|
+
async def test_parametrize_concurrent():
|
|
157
|
+
res = await wait_for_something_async()
|
|
158
|
+
assert result.is_valid()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
Contributing
|
|
162
|
+
------------
|
|
163
|
+
|
|
164
|
+
Contributions are very welcome. Tests can be run with ``tox``, please ensure
|
|
165
|
+
the coverage at least stays the same before you submit a pull request.
|
|
166
|
+
|
|
167
|
+
License
|
|
168
|
+
-------
|
|
169
|
+
|
|
170
|
+
Distributed under the terms of the ``MIT`` license, "pytest-asyncio-concurrent" is free and open source software
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.rst
|
|
3
|
+
pyproject.toml
|
|
4
|
+
pytest_asyncio_concurrent/__init__.py
|
|
5
|
+
pytest_asyncio_concurrent/plugin.py
|
|
6
|
+
pytest_asyncio_concurrent.egg-info/PKG-INFO
|
|
7
|
+
pytest_asyncio_concurrent.egg-info/SOURCES.txt
|
|
8
|
+
pytest_asyncio_concurrent.egg-info/dependency_links.txt
|
|
9
|
+
pytest_asyncio_concurrent.egg-info/entry_points.txt
|
|
10
|
+
pytest_asyncio_concurrent.egg-info/requires.txt
|
|
11
|
+
pytest_asyncio_concurrent.egg-info/top_level.txt
|
|
12
|
+
tests/test_fixture.py
|
|
13
|
+
tests/test_grouping.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pytest_asyncio_concurrent
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from textwrap import dedent
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_fixture_handling(pytester: pytest.Pytester):
|
|
6
|
+
"""Make sure that pytest accepts our fixture."""
|
|
7
|
+
|
|
8
|
+
pytester.makeconftest(
|
|
9
|
+
dedent(
|
|
10
|
+
"""\
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def fixture_a():
|
|
15
|
+
yield 1
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def fixture_b():
|
|
20
|
+
yield 2
|
|
21
|
+
"""
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
pytester.makepyfile(
|
|
26
|
+
dedent(
|
|
27
|
+
"""\
|
|
28
|
+
import asyncio
|
|
29
|
+
import pytest
|
|
30
|
+
|
|
31
|
+
@pytest.mark.asyncio_concurrent
|
|
32
|
+
async def test_fixture_multi(fixture_a, fixture_b):
|
|
33
|
+
await asyncio.sleep(1)
|
|
34
|
+
assert fixture_a == 1
|
|
35
|
+
assert fixture_b == 2
|
|
36
|
+
"""
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
result = pytester.runpytest()
|
|
41
|
+
|
|
42
|
+
result.assert_outcomes(passed=1)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_fixture_scopes(pytester: pytest.Pytester):
|
|
46
|
+
"""Make sure that pytest accepts our fixture."""
|
|
47
|
+
|
|
48
|
+
pytester.makeconftest(
|
|
49
|
+
dedent(
|
|
50
|
+
"""\
|
|
51
|
+
import pytest
|
|
52
|
+
|
|
53
|
+
@pytest.fixture(scope="function")
|
|
54
|
+
def fixture_function():
|
|
55
|
+
yield "fixture_function"
|
|
56
|
+
|
|
57
|
+
@pytest.fixture(scope="class")
|
|
58
|
+
def fixture_class():
|
|
59
|
+
yield "fixture_class"
|
|
60
|
+
|
|
61
|
+
@pytest.fixture(scope="module")
|
|
62
|
+
def fixture_module():
|
|
63
|
+
yield "fixture_module"
|
|
64
|
+
|
|
65
|
+
@pytest.fixture(scope="session")
|
|
66
|
+
def fixture_session():
|
|
67
|
+
yield "fixture_session"
|
|
68
|
+
"""
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
pytester.makepyfile(
|
|
73
|
+
dedent(
|
|
74
|
+
"""\
|
|
75
|
+
import asyncio
|
|
76
|
+
import pytest
|
|
77
|
+
|
|
78
|
+
@pytest.mark.asyncio_concurrent
|
|
79
|
+
async def test_fixture_multi(
|
|
80
|
+
fixture_function,
|
|
81
|
+
fixture_class,
|
|
82
|
+
fixture_module,
|
|
83
|
+
fixture_session
|
|
84
|
+
):
|
|
85
|
+
await asyncio.sleep(1)
|
|
86
|
+
assert fixture_function == "fixture_function"
|
|
87
|
+
assert fixture_class == "fixture_class"
|
|
88
|
+
assert fixture_module == "fixture_module"
|
|
89
|
+
assert fixture_session == "fixture_session"
|
|
90
|
+
"""
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
result = pytester.runpytest()
|
|
95
|
+
|
|
96
|
+
result.assert_outcomes(passed=1)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_fixture_teardown(pytester: pytest.Pytester):
|
|
100
|
+
"""Make sure that pytest accepts our fixture."""
|
|
101
|
+
|
|
102
|
+
pytester.makeconftest(
|
|
103
|
+
dedent(
|
|
104
|
+
"""\
|
|
105
|
+
import pytest
|
|
106
|
+
|
|
107
|
+
@pytest.fixture(scope="function")
|
|
108
|
+
def fixture_function():
|
|
109
|
+
yield []
|
|
110
|
+
|
|
111
|
+
@pytest.fixture(scope="module")
|
|
112
|
+
def fixture_module():
|
|
113
|
+
yield []
|
|
114
|
+
"""
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
pytester.makepyfile(
|
|
119
|
+
testA=dedent(
|
|
120
|
+
"""\
|
|
121
|
+
import asyncio
|
|
122
|
+
import pytest
|
|
123
|
+
|
|
124
|
+
@pytest.mark.asyncio_concurrent(group="any")
|
|
125
|
+
@pytest.mark.parametrize("p", [1, 2, 3])
|
|
126
|
+
async def test_fixture_multi(fixture_function, fixture_module, p):
|
|
127
|
+
await asyncio.sleep(p)
|
|
128
|
+
|
|
129
|
+
fixture_module.append(p)
|
|
130
|
+
fixture_function.append(p)
|
|
131
|
+
|
|
132
|
+
assert len(fixture_function) == 1
|
|
133
|
+
assert len(fixture_module) == p
|
|
134
|
+
"""
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
pytester.makepyfile(
|
|
139
|
+
testB=dedent(
|
|
140
|
+
"""\
|
|
141
|
+
import asyncio
|
|
142
|
+
import pytest
|
|
143
|
+
|
|
144
|
+
@pytest.mark.asyncio_concurrent
|
|
145
|
+
@pytest.mark.parametrize("p", [1, 2, 3])
|
|
146
|
+
async def test_fixture_multi(fixture_function, fixture_module, p):
|
|
147
|
+
await asyncio.sleep(p)
|
|
148
|
+
|
|
149
|
+
fixture_module.append(p)
|
|
150
|
+
fixture_function.append(p)
|
|
151
|
+
|
|
152
|
+
assert len(fixture_function) == 1
|
|
153
|
+
assert len(fixture_module) == p
|
|
154
|
+
"""
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
result = pytester.runpytest("testA.py", "testB.py")
|
|
159
|
+
|
|
160
|
+
result.assert_outcomes(passed=6)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from textwrap import dedent
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_groups_different(pytester: pytest.Pytester):
|
|
6
|
+
"""Make sure group with different group exceuted seperately."""
|
|
7
|
+
|
|
8
|
+
pytester.makepyfile(
|
|
9
|
+
dedent(
|
|
10
|
+
"""\
|
|
11
|
+
import asyncio
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
@pytest.mark.asyncio_concurrent(group="A")
|
|
15
|
+
async def test_group_A():
|
|
16
|
+
await asyncio.sleep(2)
|
|
17
|
+
assert 1 == 1
|
|
18
|
+
|
|
19
|
+
@pytest.mark.asyncio_concurrent(group="B")
|
|
20
|
+
async def test_group_B():
|
|
21
|
+
await asyncio.sleep(1)
|
|
22
|
+
assert 1 == 1
|
|
23
|
+
"""
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
result = pytester.runpytest()
|
|
28
|
+
|
|
29
|
+
assert result.duration >= 3
|
|
30
|
+
result.assert_outcomes(passed=2)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_groups_anonymous(pytester: pytest.Pytester):
|
|
34
|
+
"""Make sure tests without group specified treated as different group"""
|
|
35
|
+
|
|
36
|
+
pytester.makepyfile(
|
|
37
|
+
dedent(
|
|
38
|
+
"""\
|
|
39
|
+
import asyncio
|
|
40
|
+
import pytest
|
|
41
|
+
|
|
42
|
+
@pytest.mark.asyncio_concurrent
|
|
43
|
+
async def test_group_A():
|
|
44
|
+
await asyncio.sleep(2)
|
|
45
|
+
assert 1 == 1
|
|
46
|
+
|
|
47
|
+
@pytest.mark.asyncio_concurrent
|
|
48
|
+
async def test_group_B():
|
|
49
|
+
await asyncio.sleep(1)
|
|
50
|
+
assert 1 == 1
|
|
51
|
+
"""
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
result = pytester.runpytest()
|
|
56
|
+
|
|
57
|
+
assert result.duration >= 3
|
|
58
|
+
result.assert_outcomes(passed=2)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_groups_same(pytester: pytest.Pytester):
|
|
62
|
+
"""Make sure group with same group exceuted together."""
|
|
63
|
+
|
|
64
|
+
pytester.makepyfile(
|
|
65
|
+
dedent(
|
|
66
|
+
"""\
|
|
67
|
+
import asyncio
|
|
68
|
+
import pytest
|
|
69
|
+
|
|
70
|
+
@pytest.mark.asyncio_concurrent(group="A")
|
|
71
|
+
async def test_group_anonymous_A():
|
|
72
|
+
await asyncio.sleep(2)
|
|
73
|
+
assert 1 == 1
|
|
74
|
+
|
|
75
|
+
@pytest.mark.asyncio_concurrent(group="A")
|
|
76
|
+
async def test_group_anonymous_B():
|
|
77
|
+
await asyncio.sleep(1)
|
|
78
|
+
assert 1 == 1
|
|
79
|
+
"""
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
result = pytester.runpytest()
|
|
84
|
+
|
|
85
|
+
assert result.duration < 3
|
|
86
|
+
result.assert_outcomes(passed=2)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_parametrize_without_group(pytester: pytest.Pytester):
|
|
90
|
+
"""Make sure parametrized tests without group specified treated as different group"""
|
|
91
|
+
|
|
92
|
+
pytester.makepyfile(
|
|
93
|
+
dedent(
|
|
94
|
+
"""\
|
|
95
|
+
import asyncio
|
|
96
|
+
import pytest
|
|
97
|
+
|
|
98
|
+
g = 0
|
|
99
|
+
|
|
100
|
+
@pytest.mark.parametrize("p", [0, 1, 2])
|
|
101
|
+
@pytest.mark.asyncio_concurrent
|
|
102
|
+
async def test_parametrize_no_group(p):
|
|
103
|
+
global g
|
|
104
|
+
await asyncio.sleep(p)
|
|
105
|
+
|
|
106
|
+
assert g == p
|
|
107
|
+
g += 1
|
|
108
|
+
"""
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
result = pytester.runpytest()
|
|
113
|
+
|
|
114
|
+
result.assert_outcomes(passed=3)
|
|
115
|
+
assert result.duration >= 3
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_parametrize_with_group(pytester: pytest.Pytester):
|
|
119
|
+
"""Make sure parametrized tests with group specified executed together"""
|
|
120
|
+
|
|
121
|
+
pytester.makepyfile(
|
|
122
|
+
dedent(
|
|
123
|
+
"""\
|
|
124
|
+
import asyncio
|
|
125
|
+
import pytest
|
|
126
|
+
|
|
127
|
+
g = 0
|
|
128
|
+
|
|
129
|
+
@pytest.mark.parametrize("p", [0, 1, 2])
|
|
130
|
+
@pytest.mark.asyncio_concurrent(group="any")
|
|
131
|
+
async def test_parametrize_with_group(p):
|
|
132
|
+
global g
|
|
133
|
+
await asyncio.sleep(p)
|
|
134
|
+
|
|
135
|
+
assert g == p
|
|
136
|
+
g += 1
|
|
137
|
+
"""
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
result = pytester.runpytest()
|
|
142
|
+
|
|
143
|
+
result.assert_outcomes(passed=3)
|
|
144
|
+
assert result.duration < 3
|