Skip to content

Commit ea94159

Browse files
authored
Add release workflow (#10)
1 parent f5668e4 commit ea94159

File tree

3 files changed

+142
-40
lines changed

3 files changed

+142
-40
lines changed

.github/workflows/release.yml

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
name: Publish release
2+
3+
# Whenever we publish a release we want to:
4+
# * Upload a new version to PyPI
5+
# * Upload the built files to the GitHub release
6+
#
7+
# The process for making a release is therefore to:
8+
# 1. Modify the version specified in the pyproject.toml
9+
# 2. Make a release using the GitHub web interface
10+
#
11+
# This workflow was shamelessly taken (and modified) from
12+
# https://github.com/NGSolve/ngsPETSc/blob/main/.github/workflows/release.yml and
13+
# https://github.com/ArjanCodes/examples/blob/main/2024/publish_pypi/release.yaml
14+
15+
on:
16+
release:
17+
types: [published]
18+
19+
env:
20+
PACKAGE_NAME: "mpi-pytest"
21+
22+
jobs:
23+
build:
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@v4
27+
28+
- name: Set up Python
29+
uses: actions/setup-python@v5
30+
with:
31+
python-version: "3.12"
32+
33+
- name: Build mpi-pytest
34+
run: |
35+
pip install build
36+
python -m build .
37+
38+
- name: Upload artifacts
39+
uses: actions/upload-artifact@v4
40+
with:
41+
name: dist
42+
path: dist/
43+
44+
pypi-publish:
45+
name: Upload release to PyPI
46+
needs: build
47+
runs-on: ubuntu-latest
48+
environment:
49+
name: release
50+
permissions:
51+
id-token: write
52+
steps:
53+
- name: Download artifacts
54+
uses: actions/download-artifact@v4
55+
with:
56+
name: dist
57+
path: dist/
58+
59+
- name: Publish distribution to PyPI
60+
uses: pypa/gh-action-pypi-publish@release/v1
61+
62+
github-release:
63+
name: Upload artifacts to GitHub release
64+
needs: build
65+
runs-on: ubuntu-latest
66+
permissions:
67+
contents: write
68+
steps:
69+
- uses: actions/checkout@v4
70+
71+
- name: Download artifacts
72+
uses: actions/download-artifact@v4
73+
with:
74+
name: dist
75+
path: dist/
76+
77+
- name: Upload artifacts to GitHub release
78+
env:
79+
GH_TOKEN: ${{ github.token }}
80+
run: gh release upload ${{ github.event.tag_name }} dist/*

README.md

Lines changed: 54 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
1-
# pytest-mpi
1+
# mpi-pytest
22

33
Pytest plugin that lets you run tests in parallel with MPI.
44

5-
## Installation
5+
`mpi-pytest` provides:
66

7-
To install `pytest-mpi` simply run:
7+
* A `parallel` marker indicating that the test must be run under MPI.
8+
* A `parallel_assert` function for evaluating assertions in a deadlock-safe way.
89

9-
```
10-
$ python -m pip install /path/to/pytest-mpi-repo
11-
```
12-
13-
## Usage
10+
## `parallel` marker
1411

1512
Writing a parallel test simply requires marking the test with the `parallel` marker:
1613

@@ -49,35 +46,33 @@ and renamed to, in this case, `test_my_code_on_variable_nprocs[nprocs=1]`,
4946
`test_my_code_on_variable_nprocs[nprocs=2]` and
5047
`test_my_code_on_variable_nprocs[nprocs=3]`.
5148

52-
### Extra markers
53-
54-
When running the code with these `parallel` markers, `pytest-mpi` adds extra markers
49+
When running the code with these `parallel` markers, `mpi-pytest` adds extra markers
5550
to each test to allow one to select all tests with a particular number of processors.
56-
For example, to select all parallel tests on 3 processors, one should run
51+
For example, to select all parallel tests on 3 processors, one should run:
5752

5853
```bash
59-
$ pytest -m "parallel[3]"
54+
$ mpiexec -n 3 pytest -m parallel[3]
6055
```
6156

62-
For serial tests - those either unmarked or marked `@pytest.mark.parallel(1)` - one
63-
can select these by running
57+
Serial tests - those either unmarked or marked `@pytest.mark.parallel(1)` - can
58+
be selected by running:
6459

6560
```bash
6661
$ pytest -m "not parallel or parallel[1]"
6762
```
6863

6964
### Forking mode
7065

71-
`pytest-mpi` can be run in one of two modes: forking or non-forking. The former
66+
`mpi-pytest` can be used in one of two modes: forking or non-forking. The former
7267
works as follows:
7368

74-
1. The user calls `pytest` (not `mpiexec -n <# proc> pytest`!). This launches
69+
1. The user calls `pytest` (not `mpiexec -n <# proc> pytest`). This launches
7570
the "parent" `pytest` process.
7671
2. This parent `pytest` process collects all the tests and begins to run them.
7772
3. When a test is found with the `parallel` marker, rather than executing the
7873
function as before, a subprocess is forked calling
79-
`mpiexec -np <# proc> pytest this_specific_test_file.py::this_specific_test`.
80-
This produces `<# proc>` "child" `pytest` processes that execute the
74+
`mpiexec -n <# proc> pytest this_specific_test_file.py::this_specific_test`.
75+
This produces `<# proc>` 'child' `pytest` processes that execute the
8176
test together.
8277
4. If this terminates successfully then the test is considered to have passed.
8378

@@ -87,35 +82,59 @@ This is convenient for development for a number of reasons:
8782
* It is not necessary to wrap `pytest` invocations with `mpiexec` calls, and
8883
all parallel and serial tests can be run at once.
8984

90-
However, the forking mode of `pytest-mpi` is restricted in that only one mainstream
91-
MPI distribution ([MPICH](https://www.mpich.org/)) supports nested calls to
92-
`MPI_Init`. If your "parent" `pytest` process initialises MPI (for instance by
93-
executing `from petsc4py import PETSc`) then this will cause non-MPICH MPI
94-
distributions to crash. Further, forking a subprocess can be expensive since a
95-
completely fresh Python interpreter is launched each time.
85+
There are however a number of downsides:
86+
87+
* Only one mainstream MPI distribution ([MPICH](https://www.mpich.org/)) supports
88+
nested calls to `MPI_Init`. If your 'parent' `pytest` process initialises MPI
89+
(for instance by executing `from mpi4py import MPI`) then this will cause non-MPICH
90+
MPI distributions to crash.
91+
* Forking a subprocess can be expensive since a completely fresh Python interpreter
92+
is launched each time.
93+
* Sandboxing each test means that polluted global state at the end of a test cannot
94+
be detected.
9695

9796
### Non-forking mode
9897

99-
With these significant limitations in mind, `pytest-mpi` therefore also supports
98+
With these significant limitations in mind, `mpi-pytest` therefore also supports
10099
a non-forking mode. To use it, one simply needs to wrap the `pytest` invocation
101100
with `mpiexec`, no additional configuration is necessary. For example, to run
102101
all of the parallel tests on 2 ranks one needs to execute:
103102

104103
```bash
105-
$ mpiexec -n 2 pytest -m "parallel[2]"
104+
$ mpiexec -n 2 pytest -m parallel[2]
106105
```
107106

108-
This approach is agnostic to MPI distribution, and free from the forking startup
109-
overhead, but has a number of disadvantages:
107+
## `parallel_assert`
108+
109+
Using regular `assert` statements can be unsafe in parallel codes. Consider the
110+
code:
111+
112+
```py
113+
@pytest.mark.parallel(2)
114+
def test_something():
115+
# this will only fail on *some* ranks
116+
assert COMM_WORLD.rank == 0
110117

111-
* `pytest-xdist` is strictly disallowed as threading would lead to deadlocks. It
112-
is therefore impossible to take full advantage of machines with many cores.
113-
* A different `mpiexec` instruction is required for each level of parallelism.
114-
Attempting to run with `mpiexec` with a mismatching number of processes to the
115-
parallel marker will result in an error.
118+
# this will hang
119+
COMM_WORLD.barrier()
120+
```
121+
122+
One can see that failing assertions on some ranks but not others will violate SPMD
123+
and lead to deadlocks. To avoid this, `mpi-pytest` provides a `parallel_assert`
124+
function used as follows:
125+
126+
```py
127+
from pytest_mpi import parallel_assert
128+
129+
@pytest.mark.parallel(2)
130+
def test_something():
131+
# this will fail on *all* ranks
132+
parallel_assert(lambda: COMM_WORLD.rank == 0)
133+
...
134+
```
116135

117136
## Configuration
118137

119-
`pytest-mpi` respects the environment variable `PYTEST_MPI_MAX_NPROCS`, which defines
138+
`mpi-pytest` respects the environment variable `PYTEST_MPI_MAX_NPROCS`, which defines
120139
the maximum number of processes that can be requested by a parallel marker. If this
121140
value is exceeded an error will be raised.

pyproject.toml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
[build-system]
2-
requires = ["hatchling", "editables"]
3-
build-backend = "hatchling.build"
2+
requires = ["setuptools"]
3+
build-backend = "setuptools.build_meta"
44

55
[project]
6-
name = "pytest-mpi"
7-
version = "0.1"
6+
name = "mpi-pytest"
7+
# <year.month.patch>
8+
version = "2025.2.0"
89
dependencies = ["mpi4py", "pytest"]
910
authors = [
1011
{ name="Connor Ward", email="[email protected]" },
12+
{ name="Jack Betteridge", email="[email protected]" },
1113
]
1214
description = "A pytest plugin for executing tests in parallel with MPI"
1315
readme = "README.md"
16+
license = { file = "LICENSE" }
1417
requires-python = ">=3.7"
1518
classifiers = [
1619
"Programming Language :: Python :: 3",
1720
"Framework :: Pytest",
1821
]
1922

2023
[project.entry-points.pytest11]
21-
pytest-mpi = "pytest_mpi.plugin"
24+
mpi-pytest = "pytest_mpi.plugin"

0 commit comments

Comments
 (0)