Skip to content

Commit

Permalink
Migrate CI/CD from AppVeyor to GitHub Actions (#85)
Browse files Browse the repository at this point in the history
- GitHub Actions workflow created to replace AppVeyor.
- Key test-related dependencies upgraded.
- Support for python 3.11 added.
- Certain time-sensitive tests updated with relaxed timeouts.
- Explicit specification of the minor version number of DSDL types removed.

---------

Co-authored-by: Pavel Kirienko <[email protected]>
  • Loading branch information
clyde-johnston and pavel-kirienko authored Jan 24, 2024
1 parent 53b7e54 commit 9e9cd2f
Show file tree
Hide file tree
Showing 35 changed files with 450 additions and 407 deletions.
95 changes: 0 additions & 95 deletions .appveyor.yml

This file was deleted.

102 changes: 102 additions & 0 deletions .github/workflows/test-and-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: 'Test and Release Yakut'
on: push

# Ensures that only one workflow is running at a time
concurrency:
group: ${{ github.workflow_sha }}
cancel-in-progress: true

jobs:
yakut-test:
name: Test Yakut
strategy:
fail-fast: false
matrix:
# The Windows NPcap runner is an ordinary Windows machine with the NPcap driver installed manually.
# We chose to do it this way because NPcap driver installation requires a reboot, which is difficult to
# automate. The NPcap driver is required for the Cyphal/UDP transport tests to work.
os: [ubuntu-22.04, windows-2019-npcap]
python: ['3.8', '3.9', '3.10', '3.11']
exclude: # We don't test Windows with old Python versions because it takes too much effort.
- os: windows-2019-npcap
python: 3.8
- os: windows-2019-npcap
python: 3.9
runs-on: ${{ matrix.os }}
steps:
- name: Check out
uses: actions/checkout@v3

- name: Install Python3
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}

- name: Log Python version
run: python --version

- name: Install dependencies
# language=bash
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
sudo apt-get --ignore-missing update || true
sudo apt-get install -y linux-*-extra-$(uname -r) ncat
sudo apt-get install -y libsdl2-2.0-0 # For PySDL2. On Windows/macOS the binaries are pulled from PyPI.
sudo apt-get install -y libasound2-dev # For RtMidi.
fi
git submodule update --init --recursive
python -m pip install --upgrade pip setuptools nox
shell: bash

- name: Run build and test
# language=bash
run: |
nox --non-interactive --session test --python ${{ matrix.python }}
nox --non-interactive --session lint
shell: bash
env:
FORCE_COLOR: 1

- name: Upload diagnostics
uses: actions/upload-artifact@v3
if: (success() || failure())
with:
# The matrix is shown for convenience but this is fragile because the values may not be string-convertible.
# Shall it break one day, feel free to remove the matrix from here.
# The job status is per matrix item, which is super convenient.
name: ${{github.job}}-#${{strategy.job-index}}-${{job.status}}-${{join(matrix.*, ',')}}
path: "**/*.log"
retention-days: 7

yakut-release:
name: Release Yakut
runs-on: ubuntu-latest
if: contains(github.event.head_commit.message, '#release') || contains(github.ref, '/main')
needs: yakut-test
steps:
- name: Check out
uses: actions/checkout@v3

- name: Create distribution wheel
# language=bash
run: |
git submodule update --init --recursive
python -m pip install --upgrade pip setuptools wheel twine
python setup.py sdist bdist_wheel
- name: Get release version
run: echo "yakut_version=$(cat yakut/VERSION)" >> $GITHUB_ENV

- name: Upload distribution
run: |
python -m twine upload dist/*
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN_YAKUT }}

- name: Push version tag
uses: mathieudutour/[email protected]
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
custom_tag: ${{ env.yakut_version }}
tag_prefix: ''
3 changes: 3 additions & 0 deletions .idea/dictionaries/pavel.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This document is intended for developers only.
## Testing

Install dependencies into your current Python environment: `pip install .`
Aside from that, you will need to install other dependencies listed in `.appveyor.yml`
Aside from that, you will need to install other dependencies listed in the CI/CD workflow files
(e.g., [Ncat](https://nmap.org/ncat/); for Debian-based distros try `apt install ncat`).

Write unit tests as functions without arguments prefixed with ``_unittest_``;
Expand Down Expand Up @@ -37,6 +37,14 @@ If you want to start from scratch, use `clean`:
nox -s clean
```

Positional arguments given to Nox are passed over to PyTest as-is,
which can be used to run tests selectively or bail at the first failure:

```bash
nox -s test -- -x yakut/param/formatter.py -k test_format_param
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PyTest options
```

#### Running tests/linters selectively from a virtual environment created by Nox

Running the full test suite using Nox takes too much time for interactive testing during development.
Expand Down
34 changes: 20 additions & 14 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
assert DEPS_DIR.is_dir(), "Invalid configuration"


PYTHONS = ["3.8", "3.9", "3.10"]
PYTHONS = ["3.8", "3.9", "3.10", "3.11"]


@nox.session(python=False)
Expand Down Expand Up @@ -50,9 +50,9 @@ def test(session):

# Now we can install dependencies for the full integration test suite.
session.install(
"pytest ~= 7.1",
"pytest-asyncio == 0.18",
"coverage ~= 6.3",
"pytest ~= 7.4",
"pytest-asyncio ~= 0.21.0",
"coverage ~= 7.4",
)

# The test suite generates a lot of temporary files, so we change the working directory.
Expand All @@ -64,20 +64,26 @@ def test(session):
if not (tmp_dir / fn).exists():
(tmp_dir / fn).symlink_to(ROOT_DIR / fn)

# The directories to test may be overridden if needed when invoking Nox.
src_dirs = [(ROOT_DIR / t) for t in (session.posargs or ["yakut", "tests"])]
if sys.platform.startswith("linux"):
# Enable packet capture for the Python executable. This is necessary for commands that rely on low-level
# network packet capture, such as the Monitor when used with Cyphal/UDP.
# We do it here because the sudo may ask the user for the password; doing that from the suite is inconvenient.
session.run("sudo", "setcap", "cap_net_raw+eip", str(Path(session.bin, "python").resolve()), external=True)

src_dirs = [(ROOT_DIR / t) for t in ["yakut", "tests"]]

# Run PyTest and make a code coverage report.
env = {
"PYTHONPATH": str(DEPS_DIR),
"PATH": os.pathsep.join([session.env["PATH"], str(DEPS_DIR)]),
}
# Positional arguments passed to Nox after "--" are forwarded to PyTest as-is.
session.run(
"pytest",
*map(str, src_dirs),
# Log format cannot be specified in setup.cfg https://github.com/pytest-dev/pytest/issues/3062
r"--log-file-format=%(asctime)s %(levelname)-3.3s %(name)s: %(message)s",
env=env,
*session.posargs,
env={
"PYTHONPATH": str(DEPS_DIR),
"PATH": os.pathsep.join([session.env["PATH"], str(DEPS_DIR)]),
},
)

# The coverage threshold is intentionally set low for interactive runs because when running locally
Expand All @@ -98,14 +104,14 @@ def test(session):
# 1. It requires access to the code generated by the test suite.
# 2. It has to be run separately per Python version we support.
# If the interpreter is not CPython, this may need to be conditionally disabled.
session.install("mypy == 0.991")
session.install("mypy ~= 1.8")
session.run("mypy", "--strict", *map(str, src_dirs))


@nox.session(reuse_venv=True)
def lint(session):
session.install("pylint == 2.13.*")
session.install("pylint ~= 3.0.3")
session.run("pylint", "yakut", "tests")

session.install("black == 22.*")
session.install("black ~= 23.12")
session.run("black", "--check", ".")
9 changes: 7 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,16 @@ testpaths = yakut tests
python_files = *.py
python_classes = _UnitTest
python_functions = _unittest_
log_level = DEBUG
log_level = INFO
log_cli_level = WARNING
log_cli = true
log_file = pytest.log
log_file_level = DEBUG
# Unraisable exceptions are filtered because PyTest yields false-positives coming from PyCyphal.
addopts = --doctest-modules -v -p no:unraisableexception
asyncio_mode = auto
filterwarnings =
ignore:.*SDL2.*:UserWarning

# ---------------------------------------- MYPY ----------------------------------------
[mypy]
Expand Down Expand Up @@ -187,7 +190,9 @@ disable=
eval-used,
unspecified-encoding,
not-callable,
unbalanced-tuple-unpacking
unbalanced-tuple-unpacking,
no-name-in-module,
isinstance-second-argument-not-valid-type,

[pylint.REPORTS]
output-format=colorized
Expand Down
2 changes: 1 addition & 1 deletion tests/cmd/accommodate.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def _unittest_accommodate_swarm(transport_factory: TransportFactory, compiled_ds
# We spawn a lot of processes here, which might strain the test system a little, so beware. I've tested it
# with 120 processes and it made my workstation (24 GB RAM ~4 GHz Core i7) struggle to the point of being
# unable to maintain sufficiently real-time operation for the test to pass. Hm.
used_node_ids = list(range(10))
used_node_ids = list(range(5))
pubs = [
Subprocess.cli(
f"--transport={transport_factory(idx).expression}",
Expand Down
Loading

0 comments on commit 9e9cd2f

Please sign in to comment.