Skip to content

Commit 2d6db65

Browse files
authored
[CI] Refactor GHA workflow for cog unit tests (#673)
* [CI] Rework cog unit test to be aware of cog-specific deps This creates a new GH Actions workflow that replace the existing `cogUnitTests.yml`. It basically generates a `tox.ini` with `testenv`s corresponding to all cogs, with cog-specific deps, and run all tests within their directories. The workflow file also allows for easy Red bot (upstream) version anchoring, as well as having only tests for specific cogs run on `workflow_dispatch`. * [CI] Fix formatting for `gen_tox_ini.py` * [CI] Fix `Red-DiscordBot` tag in `cog_unit_test.yaml` There is no `v` prefix. * [CI] Make workflow cog unit test look at `cogs` dir This commit adds great flexibility to generator scripts and adjust the `cog_unit_test.yaml` workflow file to specify the cogs dir ("cogs") and INI template file paths. * [CI] Use `cog_` prefix for `tox` envs in cog unit test * [CI] Add a step in cog unit test workflow to show generated configs This helps with debugging. * [CI] Empty `requirements.txt` if `requirements` is not in `info.json` * [CI] Set name for the "set matrix output" step in cog unit test * [CI] Account for cogs without `info.json` in cog unit test * [CI] Let `tox` install Red deps directly via `git` * [CI] Fix `tox` Git dep path in cog unit test `tox.ini` template It is not supposed to be prefixed with `-r`. * [CI] Simplify and use absolute path for cogs dir This commit renders templates redundant. Tox env var substitution is enough to get the job done. * [CI] Correct INI paths in cog unit test workflow * [CI] Install Red dev deps in cog unit test workflow This is done just to get Red's development dependencies `pytest` and `tox`. * [CI] Remove `#egg` in package URL and install `tox` It turns out that `tox` is not included when installing `red-discordbot[dev]`. Also, `#egg` is deprecated. See pypa/pip#13157 for more details. * [CI] Use absolute paths for all paths in env in cog unit test workflow * [CI] Allow cogs with no tests to pass in cog unit test workflow * [CI] Ignore Pytest's exit code 5 in cog unit test workflow This exit code is used when Pytest does not find any tests. * [CI] Use extra target `test` instead of `dev` from `red-discordbot` `dev` will try to drag in `sphinx-prompt` 1.7.0 which is not compatible with Python 3.11. * [CI] Let tox allow bash and sh in cog unit test workflow * [CI] Use absolute paths in tox's `allowlist_externals` * [CI] Skip installing `red-discordbot` in cog unit test workflow Only `tox` is needed, and then `tox`, upon installing `red-discordbot[test]`, will use that environment to run `pytest`. * [CI] Simplify `workflow_dispatch` for cog unit test workflow If the ability to input cog names for manual runs is wanted later, it can then be implemented. Also, this commit cleans up the `discover` job and removes the installing of `jq` in `test` job. `jq` is available already. * [CI] Make `jq` output JSON list in one line in cog unit test workflow Without this, GitHub will run into this error: "Unable to process file command 'output' successfully." * [CI] Inline env vars in cog unit test workflow Except for `COGS_PATH`, all other environment variables can be inlined. * [CI] Exclude the cogs dir passed into `find` in cog unit test workflow This is a `find` gotcha. The flag `-mindepth 1` has to be there to fix this. * [CI] Extract and use Red version from `setup.py` * [CI] Clean up env vars in cog unit test workflow This commit also utilizes `COGS_REL_PATH` in `hashFiles` so that there is no longer the hardcoded `cogs` dir in `hashFiles` anymore. * [CI] Remove the "empty" comment in `jq` in cog unit test workflow * [CI] Collect only testful cogs in cog unit test workflow This commit relies on Pytest's test collection logic to extract the names of the cogs with tests. Tox is used to keep the `red-discordbot[test]` base dependency central in one INI file. Cogs with no tests will not be included in the `cogs` output of the `discover` job, meaning they will not get recognized by the `test` job. * [CI] Fix the `run` field of `collect-testful-cogs` * [CI] Correct `--cogs-dir` to `--cogs-path` in `cog_unit_test_tox.ini` * [CI] Rename `SCRIPT_PATH` to `SCRIPT_PY` in `cog_unit_test_tox.ini` It is simply because other envs and flags (like `PYTEST_INI`) end with their known extensions.
1 parent 1ab21e6 commit 2d6db65

File tree

5 files changed

+190
-26
lines changed

5 files changed

+190
-26
lines changed

.github/workflows/cogUnitTests.yml

Lines changed: 0 additions & 26 deletions
This file was deleted.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
name: Cog Unit Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- V3/develop
7+
- V3/testing
8+
- V3/prod
9+
pull_request:
10+
branches:
11+
- V3/develop
12+
- V3/testing
13+
- V3/prod
14+
workflow_dispatch:
15+
16+
env:
17+
COGS_REL_PATH: "cogs"
18+
19+
jobs:
20+
discover:
21+
runs-on: ubuntu-latest
22+
outputs:
23+
cogs: ${{ steps.collect-testful-cogs.outputs.cogs }}
24+
red-version: ${{ steps.extract-red-version.outputs.version }}
25+
env:
26+
# Keep this in sync with Red-DiscordBot's minimum supported version.
27+
# This is defined in various places, but extracting any of them is non-trivial.
28+
PYTHON_VERSION: "3.8"
29+
steps:
30+
- uses: actions/checkout@v4
31+
32+
- name: Extract Red version
33+
id: extract-red-version
34+
run: |
35+
version=$(python "${{ github.workspace }}/setup.py" --version 2> /dev/null)
36+
echo "version=${version}" >> $GITHUB_OUTPUT
37+
38+
- name: Set up Python
39+
uses: actions/setup-python@v5
40+
with:
41+
python-version: ${{ env.PYTHON_VERSION }}
42+
43+
- name: Cache pip + tox
44+
uses: actions/cache@v4
45+
with:
46+
path: |
47+
~/.cache/pip
48+
.tox
49+
key: "${{ runner.os }}-py${{ env.PYTHON_VERSION }}-red${{ steps.extract-red-version.outputs.version }}"
50+
51+
- name: Install tox
52+
run: pip install "tox"
53+
54+
- name: Collect testful cogs
55+
id: collect-testful-cogs
56+
env:
57+
COGS_PATH: "${{ github.workspace }}/${{ env.COGS_REL_PATH }}"
58+
OUTPUT_FILE: "/tmp/cog_unit_test_collect.json"
59+
PYTEST_INI: "${{ github.workspace }}/.github/workflows/configs/cog_unit_test_pytest.ini"
60+
SCRIPT_PY: "${{ github.workspace }}/.github/workflows/scripts/collect_testful_cogs.py"
61+
run: |
62+
tox run -c "${{ github.workspace }}/.github/workflows/configs/cog_unit_test_tox.ini" -e "collect"
63+
echo "cogs=$(jq -c '.' '/tmp/cog_unit_test_collect.json')" >> $GITHUB_OUTPUT
64+
65+
test:
66+
needs: discover
67+
runs-on: ubuntu-latest
68+
strategy:
69+
matrix:
70+
# Keep this in sync with Red-DiscordBot's supported versions.
71+
# They are defined in setup.py and pyproject.toml, but extracting them is non-trivial.
72+
python-version: ["3.8", "3.9", "3.10", "3.11"]
73+
cog: ${{ fromJson(needs.discover.outputs.cogs) }}
74+
steps:
75+
- uses: actions/checkout@v4
76+
77+
- name: Extract cog requirements
78+
env:
79+
COG_PATH: "${{ github.workspace }}/${{ env.COGS_REL_PATH }}/${{ matrix.cog }}"
80+
run: |
81+
if [ -f "${COG_PATH}/info.json" ]; then
82+
jq -r ".requirements[]?" "${COG_PATH}/info.json" > "${COG_PATH}/.requirements.txt"
83+
else
84+
touch "${COG_PATH}/.requirements.txt"
85+
fi
86+
87+
- name: Set up Python
88+
uses: actions/setup-python@v5
89+
with:
90+
python-version: ${{ matrix.python-version }}
91+
92+
- name: Cache pip + tox
93+
uses: actions/cache@v4
94+
with:
95+
path: |
96+
~/.cache/pip
97+
.tox
98+
key: "${{ runner.os }}-py${{ matrix.python-version }}-red${{ needs.discover.outputs.red-version }}-${{ matrix.cog }}-${{ hashFiles(format('{0}/{1}/.requirements.txt', env.COGS_REL_PATH, matrix.cog)) }}"
99+
restore-keys: "${{ runner.os }}-py${{ matrix.python-version }}-red${{ needs.discover.outputs.red-version }}-${{ matrix.cog }}-"
100+
101+
- name: Install tox
102+
run: pip install "tox"
103+
104+
- name: Run tox
105+
env:
106+
COG_PATH: "${{ github.workspace }}/${{ env.COGS_REL_PATH }}/${{ matrix.cog }}"
107+
COG_REQUIREMENTS_TXT: "${{ github.workspace }}/${{ env.COGS_REL_PATH }}/${{ matrix.cog }}/.requirements.txt"
108+
PYTEST_INI: "${{ github.workspace }}/.github/workflows/configs/cog_unit_test_pytest.ini"
109+
RED_DISCORDBOT_BRANCH: "${{ needs.discover.outputs.red-version }}"
110+
run: tox run -c "${{ github.workspace }}/.github/workflows/configs/cog_unit_test_tox.ini" -e "test"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[pytest]
2+
python_files = test*.py
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[tox]
2+
isolated_build = true
3+
skipsdist = true
4+
5+
[testenv]
6+
deps =
7+
red-discordbot[test] @ git+https://github.com/Cog-Creators/Red-DiscordBot@{env:RED_DISCORDBOT_BRANCH:V3/develop}
8+
9+
[testenv:collect]
10+
commands =
11+
python "{env:SCRIPT_PY:collect.py}" --cogs-path "{env:COGS_PATH:cogs}" --pytest-ini "{env:PYTEST_INI:pytest.ini}" --output-file "{env:OUTPUT_FILE:collect.json}" {posargs}
12+
13+
[testenv:test]
14+
deps =
15+
{[testenv]deps}
16+
--requirement "{env:COG_REQUIREMENTS_TXT:requirements.txt}"
17+
commands =
18+
pytest --config-file "{env:PYTEST_INI:pytest.ini}" "{env:COG_PATH}" {posargs}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/usr/bin/env python3
2+
import argparse
3+
import json
4+
from pathlib import Path
5+
import sys
6+
from typing import List, Set, Union
7+
8+
import pytest
9+
10+
11+
class CogCollector:
12+
"""Pytest plugin to collect cogs relative to a cogs directory."""
13+
14+
def __init__(
15+
self,
16+
cogs_path: Union[str, Path],
17+
) -> None:
18+
self.cogs_path = Path(cogs_path).resolve()
19+
self.collected_cogs: Set[str] = set()
20+
21+
def pytest_collection_modifyitems(
22+
self,
23+
session: pytest.Session,
24+
config: pytest.Config,
25+
items: List[pytest.Item],
26+
) -> None:
27+
for item in items:
28+
rel_path = item.path.relative_to(self.cogs_path)
29+
cog_name = rel_path.parts[0]
30+
self.collected_cogs.add(cog_name)
31+
32+
33+
def main():
34+
parser = argparse.ArgumentParser()
35+
parser.add_argument("--cogs-path", type=str, default="cogs", help="Path to cogs directory")
36+
parser.add_argument("--output-file", type=str, default=None, help="File to write output JSON")
37+
parser.add_argument("--pytest-ini", type=str, default=None, help="Path to pytest.ini file")
38+
39+
args = parser.parse_args()
40+
cogs_path: str = args.cogs_path
41+
42+
pytest_args = ["--collect-only", "--quiet"]
43+
if args.pytest_ini:
44+
pytest_args.extend(["--config-file", args.pytest_ini])
45+
pytest_args.append(str(Path(cogs_path).resolve()))
46+
47+
cog_collector_plugin = CogCollector(cogs_path)
48+
pytest_plugins = [cog_collector_plugin]
49+
50+
pytest.main(pytest_args, plugins=pytest_plugins)
51+
52+
if args.output_file:
53+
with open(file=args.output_file, mode="w") as f:
54+
json.dump(sorted(cog_collector_plugin.collected_cogs), f)
55+
else:
56+
json.dump(sorted(cog_collector_plugin.collected_cogs), sys.stdout)
57+
58+
59+
if __name__ == "__main__":
60+
main()

0 commit comments

Comments
 (0)