Skip to content

Commit 2a4f224

Browse files
authored
Merge pull request #3 from mxstack/test/add-test-suite-and-quality-config
Add test suite and CI configuration
2 parents 53b3b6d + 97df2af commit 2a4f224

File tree

9 files changed

+852
-59
lines changed

9 files changed

+852
-59
lines changed

.github/workflows/test.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
15+
fail-fast: false
16+
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Set up Python ${{ matrix.python-version }}
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: ${{ matrix.python-version }}
24+
allow-prereleases: true
25+
26+
- name: Install uv
27+
run: |
28+
curl -LsSf https://astral.sh/uv/install.sh | sh
29+
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
30+
31+
- name: Install dependencies
32+
run: |
33+
uv pip install --system -e ".[test]"
34+
uv pip install --system ruff isort
35+
36+
- name: Run ruff check
37+
run: ruff check src/
38+
39+
- name: Run isort check
40+
run: isort --check src/
41+
42+
- name: Run tests with coverage
43+
run: pytest --cov=mxrepo --cov-report=term-missing --cov-report=xml
44+
45+
- name: Upload coverage to Codecov
46+
uses: codecov/codecov-action@v4
47+
if: matrix.python-version == '3.13'
48+
with:
49+
file: ./coverage.xml
50+
fail_ci_if_error: false
51+
continue-on-error: true

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22
__pycache__
33
/.mxmake/
44
/requirements-mxdev.txt
5+
/.claude/
6+
CLAUDE.md

Makefile

Lines changed: 123 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
#
44
# DOMAINS:
55
#: core.base
6+
#: core.help
67
#: core.mxenv
78
#: core.mxfiles
89
#: core.packages
910
#: qa.isort
1011
#: qa.ruff
12+
#: qa.test
1113
#
1214
# SETTINGS (ALL CHANGES MADE BELOW SETTINGS WILL BE LOST)
1315
##############################################################################
@@ -41,26 +43,30 @@ EXTRA_PATH?=
4143

4244
# Primary Python interpreter to use. It is used to create the
4345
# virtual environment if `VENV_ENABLED` and `VENV_CREATE` are set to `true`.
46+
# If global `uv` is used, this value is passed as `--python VALUE` to the venv creation.
47+
# uv then downloads the Python interpreter if it is not available.
48+
# for more on this feature read the [uv python documentation](https://docs.astral.sh/uv/concepts/python-versions/)
4449
# Default: python3
4550
PRIMARY_PYTHON?=python3
4651

4752
# Minimum required Python version.
48-
# Default: 3.9
53+
# Default: 3.10
4954
PYTHON_MIN_VERSION?=3.9
5055

5156
# Install packages using the given package installer method.
52-
# Supported are `pip` and `uv`. If uv is used, its global availability is
53-
# checked. Otherwise, it is installed, either in the virtual environment or
54-
# using the `PRIMARY_PYTHON`, dependent on the `VENV_ENABLED` setting. If
55-
# `VENV_ENABLED` and uv is selected, uv is used to create the virtual
56-
# environment.
57+
# Supported are `pip` and `uv`. When `uv` is selected, a global installation
58+
# is auto-detected and used if available. Otherwise, uv is installed in the
59+
# virtual environment or using `PRIMARY_PYTHON`, depending on the
60+
# `VENV_ENABLED` setting.
5761
# Default: pip
5862
PYTHON_PACKAGE_INSTALLER?=uv
5963

60-
# Flag whether to use a global installed 'uv' or install
61-
# it in the virtual environment.
62-
# Default: false
63-
MXENV_UV_GLOBAL?=true
64+
# Python version for UV to install/use when creating virtual
65+
# environments with global UV. Passed to `uv venv -p VALUE`. Supports version
66+
# specs like `3.11`, `3.14`, `cpython@3.14`. Defaults to PRIMARY_PYTHON value
67+
# for backward compatibility.
68+
# Default: $(PRIMARY_PYTHON)
69+
UV_PYTHON?=$(PRIMARY_PYTHON)
6470

6571
# Flag whether to use virtual environment. If `false`, the
6672
# interpreter according to `PRIMARY_PYTHON` found in `PATH` is used.
@@ -114,6 +120,28 @@ PROJECT_CONFIG?=mx.ini
114120
# Default: false
115121
PACKAGES_ALLOW_PRERELEASES?=false
116122

123+
## qa.test
124+
125+
# The command which gets executed. Defaults to the location the
126+
# :ref:`run-tests` template gets rendered to if configured.
127+
# Default: .mxmake/files/run-tests.sh
128+
TEST_COMMAND?=.mxmake/files/run-tests.sh
129+
130+
# Additional Python requirements for running tests to be
131+
# installed (via pip).
132+
# Default: pytest
133+
TEST_REQUIREMENTS?=pytest
134+
135+
# Additional make targets the test target depends on.
136+
# No default value.
137+
TEST_DEPENDENCY_TARGETS?=
138+
139+
## core.help
140+
141+
# Request to show all targets, descriptions and arguments for a given domain.
142+
# No default value.
143+
HELP_DOMAIN?=
144+
117145
##############################################################################
118146
# END SETTINGS - DO NOT EDIT BELOW THIS LINE
119147
##############################################################################
@@ -152,7 +180,7 @@ $(SENTINEL): $(firstword $(MAKEFILE_LIST))
152180
# mxenv
153181
##############################################################################
154182

155-
export OS:=$(OS)
183+
OS?=
156184

157185
# Determine the executable path
158186
ifeq ("$(VENV_ENABLED)", "true")
@@ -168,26 +196,61 @@ else
168196
MXENV_PYTHON=$(PRIMARY_PYTHON)
169197
endif
170198

171-
# Determine the package installer
199+
# Determine the package installer with non-interactive flags
172200
ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv")
173-
PYTHON_PACKAGE_COMMAND=uv pip
201+
PYTHON_PACKAGE_COMMAND=uv pip --no-progress
174202
else
175203
PYTHON_PACKAGE_COMMAND=$(MXENV_PYTHON) -m pip
176204
endif
177205

206+
# Auto-detect global uv availability (simple existence check)
207+
ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv")
208+
UV_AVAILABLE:=$(shell command -v uv >/dev/null 2>&1 && echo "true" || echo "false")
209+
else
210+
UV_AVAILABLE:=false
211+
endif
212+
213+
# Determine installation strategy
214+
# depending on the PYTHON_PACKAGE_INSTALLER and UV_AVAILABLE
215+
# - both vars can be false or
216+
# - one of them can be true,
217+
# - but never boths.
218+
USE_GLOBAL_UV:=$(shell [[ "$(PYTHON_PACKAGE_INSTALLER)" == "uv" && "$(UV_AVAILABLE)" == "true" ]] && echo "true" || echo "false")
219+
USE_LOCAL_UV:=$(shell [[ "$(PYTHON_PACKAGE_INSTALLER)" == "uv" && "$(UV_AVAILABLE)" == "false" ]] && echo "true" || echo "false")
220+
221+
# Check if global UV is outdated (non-blocking warning)
222+
ifeq ("$(USE_GLOBAL_UV)","true")
223+
UV_OUTDATED:=$(shell uv self update --dry-run 2>&1 | grep -q "Would update" && echo "true" || echo "false")
224+
else
225+
UV_OUTDATED:=false
226+
endif
227+
178228
MXENV_TARGET:=$(SENTINEL_FOLDER)/mxenv.sentinel
179229
$(MXENV_TARGET): $(SENTINEL)
230+
# Validation: Check Python version if not using global uv
231+
ifneq ("$(USE_GLOBAL_UV)","true")
180232
@$(PRIMARY_PYTHON) -c "import sys; vi = sys.version_info; sys.exit(1 if (int(vi[0]), int(vi[1])) >= tuple(map(int, '$(PYTHON_MIN_VERSION)'.split('.'))) else 0)" \
181233
&& echo "Need Python >= $(PYTHON_MIN_VERSION)" && exit 1 || :
234+
else
235+
@echo "Using global uv for Python $(UV_PYTHON)"
236+
endif
237+
# Validation: Check VENV_FOLDER is set if venv enabled
182238
@[[ "$(VENV_ENABLED)" == "true" && "$(VENV_FOLDER)" == "" ]] \
183239
&& echo "VENV_FOLDER must be configured if VENV_ENABLED is true" && exit 1 || :
184-
@[[ "$(VENV_ENABLED)$(PYTHON_PACKAGE_INSTALLER)" == "falseuv" ]] \
240+
# Validation: Check uv not used with system Python
241+
@[[ "$(VENV_ENABLED)" == "false" && "$(PYTHON_PACKAGE_INSTALLER)" == "uv" ]] \
185242
&& echo "Package installer uv does not work with a global Python interpreter." && exit 1 || :
243+
# Warning: Notify if global UV is outdated
244+
ifeq ("$(UV_OUTDATED)","true")
245+
@echo "WARNING: A newer version of uv is available. Run 'uv self update' to upgrade."
246+
endif
247+
248+
# Create virtual environment
186249
ifeq ("$(VENV_ENABLED)", "true")
187250
ifeq ("$(VENV_CREATE)", "true")
188-
ifeq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvtrue")
189-
@echo "Setup Python Virtual Environment using package 'uv' at '$(VENV_FOLDER)'"
190-
@uv venv -p $(PRIMARY_PYTHON) --seed $(VENV_FOLDER)
251+
ifeq ("$(USE_GLOBAL_UV)","true")
252+
@echo "Setup Python Virtual Environment using global uv at '$(VENV_FOLDER)'"
253+
@uv venv --allow-existing --no-progress -p $(UV_PYTHON) --seed $(VENV_FOLDER)
191254
else
192255
@echo "Setup Python Virtual Environment using module 'venv' at '$(VENV_FOLDER)'"
193256
@$(PRIMARY_PYTHON) -m venv $(VENV_FOLDER)
@@ -197,10 +260,14 @@ endif
197260
else
198261
@echo "Using system Python interpreter"
199262
endif
200-
ifeq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvfalse")
201-
@echo "Install uv"
263+
264+
# Install uv locally if needed
265+
ifeq ("$(USE_LOCAL_UV)","true")
266+
@echo "Install uv in virtual environment"
202267
@$(MXENV_PYTHON) -m pip install uv
203268
endif
269+
270+
# Install/upgrade core packages
204271
@$(PYTHON_PACKAGE_COMMAND) install -U pip setuptools wheel
205272
@echo "Install/Update MXStack Python packages"
206273
@$(PYTHON_PACKAGE_COMMAND) install -U $(MXDEV) $(MXMAKE)
@@ -397,6 +464,43 @@ INSTALL_TARGETS+=packages
397464
DIRTY_TARGETS+=packages-dirty
398465
CLEAN_TARGETS+=packages-clean
399466

467+
##############################################################################
468+
# test
469+
##############################################################################
470+
471+
TEST_TARGET:=$(SENTINEL_FOLDER)/test.sentinel
472+
$(TEST_TARGET): $(MXENV_TARGET)
473+
@echo "Install $(TEST_REQUIREMENTS)"
474+
@$(PYTHON_PACKAGE_COMMAND) install $(TEST_REQUIREMENTS)
475+
@touch $(TEST_TARGET)
476+
477+
.PHONY: test
478+
test: $(FILES_TARGET) $(SOURCES_TARGET) $(PACKAGES_TARGET) $(TEST_TARGET) $(TEST_DEPENDENCY_TARGETS)
479+
@test -z "$(TEST_COMMAND)" && echo "No test command defined" && exit 1 || :
480+
@echo "Run tests using $(TEST_COMMAND)"
481+
@/usr/bin/env bash -c "$(TEST_COMMAND)"
482+
483+
.PHONY: test-dirty
484+
test-dirty:
485+
@rm -f $(TEST_TARGET)
486+
487+
.PHONY: test-clean
488+
test-clean: test-dirty
489+
@test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y $(TEST_REQUIREMENTS) || :
490+
@rm -rf .pytest_cache
491+
492+
INSTALL_TARGETS+=$(TEST_TARGET)
493+
CLEAN_TARGETS+=test-clean
494+
DIRTY_TARGETS+=test-dirty
495+
496+
##############################################################################
497+
# help
498+
##############################################################################
499+
500+
.PHONY: help
501+
help: $(MXENV_TARGET)
502+
@mxmake help-generator
503+
400504
##############################################################################
401505
# Custom includes
402506
##############################################################################

mx.ini

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,12 @@
11
[settings]
2-
main-package = -e .
2+
main-package = -e .[test]
3+
4+
mxmake-templates =
5+
run-tests
6+
7+
mxmake-test-path = tests
8+
mxmake-test-runner = pytest
9+
mxmake-test-runner-args = --junitxml=reports/pyjunit.xml --cov=mxrepo --cov-report=term --cov-report=xml:reports/pycoverage.xml --cov-report=html:reports/pycoverage/html
10+
mxmake-source-path = src/heidi/cloud
11+
12+
[mxmake-run-tests]

pyproject.toml

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ classifiers = [
2424
dynamic = ["readme"]
2525

2626
[project.optional-dependencies]
27-
test = ["pytest"]
27+
test = [
28+
"pytest",
29+
"pytest-cov",
30+
"pytest-mock",
31+
]
2832

2933
[project.urls]
3034
Homepage = "https://github.com/mxstack/mxrepo"
@@ -55,5 +59,35 @@ ignore_missing_imports = true
5559
python_version = "3.9"
5660

5761
[tool.ruff]
58-
# Exclude a variety of commonly ignored directories.
59-
exclude = []
62+
line-length = 88
63+
exclude = [
64+
".git",
65+
".venv",
66+
"__pycache__",
67+
"build",
68+
"dist",
69+
]
70+
71+
[tool.ruff.lint]
72+
select = [
73+
"E", # pycodestyle errors
74+
"F", # pyflakes
75+
"B", # flake8-bugbear
76+
"UP", # pyupgrade
77+
"S", # bandit (security)
78+
]
79+
ignore = []
80+
81+
[tool.ruff.lint.per-file-ignores]
82+
"tests/*" = ["S101"] # Allow assert in tests
83+
84+
[tool.pytest.ini_options]
85+
testpaths = ["tests"]
86+
python_files = ["test_*.py"]
87+
python_classes = ["Test*"]
88+
python_functions = ["test_*"]
89+
addopts = [
90+
"--strict-markers",
91+
"--strict-config",
92+
"-ra",
93+
]

0 commit comments

Comments
 (0)