diff --git a/.gitignore b/.gitignore index 46bc38313..927093831 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ dist/ build/ venv/ +.venv/ +.tox/ +.python-version /site/ /.cache/ .pytest_cache/ diff --git a/.justfile b/.justfile index c419685ed..04186b588 100644 --- a/.justfile +++ b/.justfile @@ -9,9 +9,10 @@ # * frontend/.justfile – Building and running the frontend # * mkdocs/.justfile – Building and previewing the docs site # * website/.justfile – Building and previewing the React landing page +# * .tox.justfile – Running Python tests via tox -default: - @just --list +# Run tests via tox +mod tox '.tox.justfile' set allow-duplicate-recipes @@ -22,3 +23,8 @@ import "frontend/.justfile" import "mkdocs/.justfile" import "website/.justfile" + +[default] +[private] +default: + @just --list --list-submodules --unsorted diff --git a/.tox.justfile b/.tox.justfile new file mode 100644 index 000000000..f7db5d823 --- /dev/null +++ b/.tox.justfile @@ -0,0 +1,44 @@ +# Keep in sync with pyproject.toml and .github/workflows/build-artifacts.yml +supported_python_versions := '3.10,3.11,3.12,3.13,3.14' + +install_tox_msg := ''' + tox is not installed; to install it using uv: + uv tool install tox --with tox-uv +''' + +python_version_msg := ''' + .python-version does not exist; to create (replace 3.14 with the desired version): + * using uv: + uv python pin 3.14 + * if uv is not used: + echo 3.14 > .python-version +''' + +[doc(''' + Run tests using the default environment + The default Python version is set via .python-version file +''')] +[positional-arguments] +test-default *args: _check_tox_installed + @if [ ! -f .python-version ]; then echo '{{python_version_msg}}' >&2; exit 1; fi + tox run -e default "${@}" + +[doc('Run tests against all supported Python versions')] +[positional-arguments] +test-supported *args: _check_tox_installed + tox run -e {{supported_python_versions}} "${@}" + +[doc(''' + Run tests using the current environment (no venv isolation) + All dependencies must be already installed; a dummy environment is still created +''')] +[positional-arguments] +test-current *args: _check_tox_installed + tox run -e dummy --skip-env-install "${@}" + +@_check_tox_installed: + if ! command -v 'tox' >/dev/null 2>&1; then echo '{{install_tox_msg}}' >&2; exit 1; fi + +[default] +_list: + @just --list {{module_path()}} --unsorted diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1782025bb..4cbab5660 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,6 +63,36 @@ Use the `--runpostgres` flag to run the tests against Postgres as well: uv run pytest src/tests --runpostgres ``` +Alternatively, you can run tests via [tox](https://tox.wiki/) inside an isolated environment ensuring that the `dstack` package itself is built correctly and all requirements are specified and correct. tox and [just](https://just.systems/) must be already installed. + +* Run tests in the default environment: + + ```shell + just tox::test-default + ``` + + The Python version of the default environment is configured via the `.python-version` file in the repo root directory. Note, the same file [is used by uv](https://docs.astral.sh/uv/concepts/python-versions/#python-version-files). + +* Run tests against all currently supported Python versions: + + ```shell + just tox::test-supported + ``` + +* Run tests in the _current_ environment: + + ```shell + just tox::test-current + ``` + + It saves about 30-60 seconds at the cost of Python environment isolation (defeating the core purpose of tox) — no packages are built or installed, and, as a consequence, all dependencies must be already installed, but, unlike the plain pytest command, the process environment variables are still isolated. + +It's possible to pass pytest arguments after the `--` separator (the arguments _before_ the separator are `tox run` arguments): + +```shell +just tox::test-default -- -vvv --last-failed +``` + ## Add a new backend If you'd like to integrate a new cloud provider to `dstack`, follow [contributing/BACKENDS.md](contributing/BACKENDS.md). diff --git a/pyproject.toml b/pyproject.toml index 60b233223..818082fc4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -145,9 +145,38 @@ filterwarnings = [ "ignore:^The @wait_container_is_ready decorator:DeprecationWarning", ] +[tool.tox] +env_list = ["default"] + +[tool.tox.env_run_base] +description = "Run tests under {base_python}" +extras = ["all"] +dependency_groups = ["test"] +commands = [["pytest", { replace = "posargs", extend = true }]] + +# `tox run [-e default]` -- run tests inside an isolated environment +# with Python version read from `.python-version` file +[tool.tox.env.default] +base_python_file = [".python-version"] + +# `tox run -e dummy --skip-env-install` -- run tests inside the current environment +# all dependencies must be already installed; a dummy environment is still created +[tool.tox.env.dummy] +description = "Run tests inside the current environment" +# Must be called with --skip-env-install +dependency_groups = ["nonexistent-group-to-trigger-failure"] +allowlist_externals = ["pytest"] + [dependency-groups] dev = [ "pre-commit>=4.2.0", + "ruff==0.12.7", # should match .pre-commit-config.yaml + "pyinstrument>=5.0.0", + "kubernetes-stubs-elephant-fork>=35.0.0.post3", + {include-group = "test"}, + {include-group = "docs"}, +] +test = [ "pytest~=8.0", "pytest-asyncio>=0.25.2", "pytest-mock>=3.14.0", @@ -155,16 +184,12 @@ dev = [ "pytest-socket>=0.7.0", "pytest-env>=1.1.0", "pytest-unordered>=0.7.0", - "httpbin>=0.10.2", # indirect to make compatible with Werkzeug 3 + "pytest-xdist>=3.6.1", "requests-mock>=1.12.1", - "openai>=1.68.2", "freezegun>=1.5.1", - "ruff==0.12.7", # should match .pre-commit-config.yaml + "httpbin>=0.10.2", # indirect to make compatible with Werkzeug 3 + "openai>=1.68.2", "testcontainers>=4.9.2", - "pytest-xdist>=3.6.1", - "pyinstrument>=5.0.0", - "kubernetes-stubs-elephant-fork>=35.0.0.post3", - {include-group = "docs"}, ] docs = [ "dstack[server]",