├── output └── .gitignore ├── .dockerignore ├── requirements.txt ├── .gitignore ├── entrypoint.bash ├── startup-mysql.sql ├── startup-oracle.sql ├── .env ├── .hadolint.yml ├── .editorconfig ├── packages.txt ├── .github └── workflows │ └── lint.yml ├── LICENSE ├── .pre-commit-config.yaml ├── settings.py ├── Containerfile ├── compose.yml └── README.md /output/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .dockerignore 3 | output/ 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | black 2 | pre-commit 3 | unittest-xml-reporting 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .idea/ 3 | .mypy_cache/ 4 | .ruff_cache/ 5 | -------------------------------------------------------------------------------- /entrypoint.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python -Wall runtests.py ${@} 4 | -------------------------------------------------------------------------------- /startup-mysql.sql: -------------------------------------------------------------------------------- 1 | GRANT ALL PRIVILEGES ON *.* TO 'django'@'%' WITH GRANT OPTION; 2 | -------------------------------------------------------------------------------- /startup-oracle.sql: -------------------------------------------------------------------------------- 1 | ALTER SESSION SET CONTAINER=FREEPDB1; 2 | CREATE USER django IDENTIFIED BY django; 3 | GRANT DBA TO django; 4 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PYTHON_IMPLEMENTATION=python 2 | PYTHON_VERSION=3.13 3 | MARIADB_VERSION=10.6 4 | MYSQL_VERSION=8.4 5 | ORACLE_VERSION=23.5.0.0 6 | POSTGRESQL_VERSION=15 7 | POSTGIS_VERSION=3.2 8 | -------------------------------------------------------------------------------- /.hadolint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ignored: 3 | - DL3013 # Don't complain when upgrading to latest version of pip 4 | - DL3022 # Due to use of additional_contexts in compose.yaml 5 | - DL3042 # Caching is desired with RUN --mount=type=cache 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{yaml,yml}] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /packages.txt: -------------------------------------------------------------------------------- 1 | binutils 2 | build-essential 3 | default-libmysqlclient-dev 4 | default-mysql-client 5 | gdal-bin 6 | gettext 7 | git 8 | libaio-dev 9 | libenchant-2-dev 10 | libgdal-dev 11 | libgeoip-dev 12 | libmemcached-dev 13 | libpq-dev 14 | libproj-dev 15 | libsqlite3-mod-spatialite 16 | pkg-config 17 | unzip 18 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lint 3 | 4 | on: # yamllint disable-line rule:truthy 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | permissions: 15 | contents: read 16 | 17 | env: 18 | COLUMNS: '120' 19 | FORCE_COLOR: '1' 20 | 21 | jobs: 22 | pre-commit: 23 | runs-on: ubuntu-latest 24 | name: pre-commit 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | with: 29 | persist-credentials: false 30 | - name: Set up Python 31 | uses: actions/setup-python@v5 32 | with: 33 | python-version: '3.13' 34 | cache: pip 35 | - name: Install packages 36 | run: python -m pip install --upgrade pip pre-commit 37 | - name: Run linting tools 38 | run: pre-commit run --all-files 39 | env: 40 | PRE_COMMIT_COLOR: always 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Django Software Foundation and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Django nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ci: 3 | autoupdate_schedule: weekly 4 | repos: 5 | - repo: meta 6 | hooks: 7 | - id: check-hooks-apply 8 | - id: check-useless-excludes 9 | - repo: https://github.com/pre-commit/pre-commit-hooks 10 | rev: v5.0.0 11 | hooks: 12 | - id: check-added-large-files 13 | - id: check-builtin-literals 14 | - id: check-case-conflict 15 | - id: check-docstring-first 16 | - id: check-executables-have-shebangs 17 | - id: check-merge-conflict 18 | - id: check-shebang-scripts-are-executable 19 | # - id: check-toml 20 | - id: check-vcs-permalinks 21 | - id: check-yaml 22 | - id: debug-statements 23 | - id: detect-private-key 24 | - id: end-of-file-fixer 25 | - id: file-contents-sorter 26 | args: [--unique] 27 | files: ^(?:packages|requirements)\.txt$ 28 | - id: fix-byte-order-marker 29 | - id: fix-encoding-pragma 30 | args: [--remove] 31 | - id: requirements-txt-fixer 32 | - id: trailing-whitespace 33 | - repo: https://github.com/astral-sh/ruff-pre-commit 34 | rev: v0.8.2 35 | hooks: 36 | - id: ruff 37 | args: [--exit-non-zero-on-fix, --fix] 38 | - id: ruff-format 39 | args: [--check] 40 | - repo: https://github.com/adrienverge/yamllint.git 41 | rev: v1.35.1 42 | hooks: 43 | - id: yamllint 44 | args: [--strict] 45 | - repo: https://github.com/hadolint/hadolint 46 | rev: v2.12.0 47 | hooks: 48 | - id: hadolint-docker 49 | name: hadolint 50 | - repo: https://github.com/woodruffw/zizmor-pre-commit 51 | rev: v0.9.2 52 | hooks: 53 | - id: zizmor 54 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def _build_databases_setting(): 5 | engine = os.environ["DATABASE_ENGINE"] 6 | host = os.environ.get("DATABASE_HOST", "") 7 | name = os.environ.get("DATABASE_NAME", "") 8 | settings = {} 9 | 10 | for n, alias in enumerate(("default", "other"), start=1): 11 | settings[alias] = entry = {"ENGINE": engine} 12 | 13 | if not engine.endswith((".sqlite", ".spatialite")): 14 | entry |= { 15 | "HOST": host, 16 | "NAME": "django" if n < 2 else f"django{n}", 17 | "USER": "django", 18 | "PASSWORD": "django", 19 | } 20 | 21 | if engine.endswith(".mysql"): 22 | entry["TEST"] = {"CHARSET": "utf8mb4"} 23 | 24 | if engine.endswith(".oracle"): 25 | entry |= { 26 | "NAME": name, 27 | "TEST": { 28 | "USER": f"{alias}_test", 29 | "TBLSPACE": f"{alias}_test_tbls", 30 | "TBLSPACE_TMP": f"{alias}_test_tbls_tmp", 31 | }, 32 | } 33 | 34 | return settings 35 | 36 | 37 | CACHES = { 38 | "default": { 39 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", 40 | }, 41 | "pymemcache": { 42 | "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", 43 | "LOCATION": "memcached-1:11211", 44 | "KEY_PREFIX": "pymemcache:", 45 | }, 46 | "pylibmc": { 47 | "BACKEND": "django.core.cache.backends.memcached.PyLibMCCache", 48 | "LOCATION": "memcached-2:11211", 49 | "KEY_PREFIX": "pylibmc:", 50 | }, 51 | "redis-py": { 52 | "BACKEND": "django.core.cache.backends.redis.RedisCache", 53 | "LOCATION": "redis://redis:6379", 54 | "KEY_PREFIX": "redis:", 55 | }, 56 | } 57 | 58 | DATABASES = _build_databases_setting() 59 | 60 | PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] 61 | 62 | SECRET_KEY = "django_tests_secret_key" 63 | 64 | USE_TZ = False 65 | 66 | if os.environ.get("XUNIT", "0").lower() in {"1", "on", "true", "yes"}: 67 | TEST_RUNNER = "xmlrunner.extra.djangotestrunner.XMLTestRunner" 68 | TEST_OUTPUT_DIR = "/django/output/xunit" 69 | -------------------------------------------------------------------------------- /Containerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1.12 2 | 3 | ARG PYTHON_IMPLEMENTATION=python 4 | ARG PYTHON_VERSION=3.13 5 | FROM ${PYTHON_IMPLEMENTATION}:${PYTHON_VERSION}-slim-bookworm 6 | 7 | LABEL org.opencontainers.image.authors="Django Software Foundation" 8 | LABEL org.opencontainers.image.url="https://github.com/django/django-docker-box" 9 | LABEL org.opencontainers.image.documentation="https://github.com/django/django-docker-box" 10 | LABEL org.opencontainers.image.source="https://github.com/django/django-docker-box" 11 | LABEL org.opencontainers.image.vendor="Django Software Foundation" 12 | LABEL org.opencontainers.image.licenses="BSD-3-Clause" 13 | LABEL org.opencontainers.image.title="Django Docker Box" 14 | LABEL org.opencontainers.image.description="Container image for developing and testing Django." 15 | 16 | SHELL ["/bin/bash", "-o", "errexit", "-o", "nounset", "-o", "pipefail", "-o", "xtrace", "-c"] 17 | 18 | ENV DEBIAN_FRONTEND=noninteractive 19 | ENV PYTHONUNBUFFERED=1 20 | 21 | # Force colored output for various tooling in CI. 22 | ENV COLUMNS=120 23 | ENV FORCE_COLOR=1 24 | ENV TERM="xterm-256color" 25 | 26 | # Create user and prepare directories. 27 | RUN < /etc/apt/apt.conf.d/keep-cache 39 | apt-get update --quiet --yes 40 | xargs --arg-file=/django/packages.txt apt-get install --no-install-recommends --yes 41 | EOF 42 | 43 | # Install all Python requirements in a single command. 44 | COPY --chown=django:django requirements.txt /django/requirements/extra.txt 45 | COPY --chown=django:django --from=src tests/requirements/ /django/requirements/ 46 | COPY --chown=django:django --from=src docs/requirements.txt /django/requirements/docs.txt 47 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/ 48 | RUN --mount=type=cache,target=/root/.cache/uv <- 73 | -c archive_mode=off 74 | -c checkpoint_completion_target=0.9 75 | -c checkpoint_timeout=900 76 | -c fsync=off 77 | -c full_page_writes=off 78 | -c max_replication_slots=0 79 | -c max_wal_senders=0 80 | -c max_wal_size=4096 81 | -c synchronous_commit=off 82 | -c wal_level=minimal 83 | # 13+: -c wal_keep_size=0 84 | 85 | x-memcached: &memcached-base 86 | image: memcached:alpine 87 | deploy: 88 | mode: global 89 | restart: unless-stopped 90 | healthcheck: 91 | test: echo stats | nc 127.0.0.1 11211 92 | interval: 5s 93 | timeout: 5s 94 | retries: 3 95 | start_period: 10s 96 | start_interval: 1s 97 | command: >- 98 | --conn-limit=1024 99 | --memory-limit=64 100 | --threads=4 101 | 102 | x-redis: &redis-base 103 | image: redis:alpine 104 | deploy: 105 | mode: global 106 | restart: unless-stopped 107 | healthcheck: 108 | test: redis-cli ping | grep -i pong 109 | interval: 5s 110 | timeout: 5s 111 | retries: 3 112 | start_period: 10s 113 | start_interval: 1s 114 | 115 | x-cache-depends: &depends-on-caches 116 | memcached-1: 117 | condition: service_healthy 118 | memcached-2: 119 | condition: service_healthy 120 | redis: 121 | condition: service_healthy 122 | 123 | x-selenium-base: &selenium-base 124 | shm_size: 2gb 125 | environment: 126 | - SE_EVENT_BUS_HOST=selenium-hub 127 | - SE_EVENT_BUS_PUBLISH_PORT=4442 128 | - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 129 | - SE_NODE_GRID_URL=http://selenium-hub:4444 130 | depends_on: 131 | selenium-hub: 132 | condition: service_started 133 | deploy: 134 | mode: global 135 | restart: unless-stopped 136 | 137 | volumes: 138 | user-cache: 139 | 140 | services: 141 | 142 | # Services: Databases 143 | 144 | mariadb-db: 145 | <<: *mysql-base 146 | image: &mariadb-image mariadb:${MARIADB_VERSION} 147 | command: &mariadb-command >- 148 | --innodb-fast-shutdown=3 149 | --innodb-flush-log-at-trx-commit=0 150 | --innodb-flush-method=nosync 151 | --innodb-random-read-ahead 152 | --skip-innodb-doublewrite 153 | --skip-innodb-file-per-table 154 | --skip-innodb-flush-sync 155 | --skip-innodb-use-atomic-writes 156 | --skip-name-resolve 157 | healthcheck: &mariadb-healthcheck 158 | <<: *mysql-base-healthcheck 159 | test: healthcheck.sh --connect 160 | 161 | mariadb-gis-db: 162 | <<: *mysql-base 163 | image: *mariadb-image 164 | command: *mariadb-command 165 | healthcheck: *mariadb-healthcheck 166 | 167 | mysql-db: 168 | <<: *mysql-base 169 | image: &mysql-image mysql:${MYSQL_VERSION} 170 | command: &mysql-command >- 171 | --innodb-flush-log-at-trx-commit=0 172 | --innodb-flush-method=nosync 173 | --innodb-random-read-ahead 174 | --skip-innodb-doublewrite 175 | --skip-innodb-extend-and-initialize 176 | --skip-innodb-file-per-table 177 | --skip-innodb-flush-sync 178 | --skip-name-resolve 179 | healthcheck: &mysql-healthcheck 180 | <<: *mysql-base-healthcheck 181 | test: mysqladmin ping --silent 182 | 183 | mysql-gis-db: 184 | <<: *mysql-base 185 | image: *mysql-image 186 | command: *mysql-command 187 | healthcheck: *mysql-healthcheck 188 | 189 | oracle-db: 190 | <<: *oracle-base 191 | # yamllint disable-line rule:line-length 192 | image: &oracle-image container-registry.oracle.com/database/free:${ORACLE_VERSION}-lite 193 | 194 | oracle-gis-db: 195 | <<: *oracle-base 196 | image: *oracle-image 197 | 198 | postgresql-db: 199 | <<: *postgresql-base 200 | image: postgres:${POSTGRESQL_VERSION}-alpine 201 | 202 | postgresql-gis-db: 203 | <<: *postgresql-base 204 | image: postgis/postgis:${POSTGRESQL_VERSION}-${POSTGIS_VERSION}-alpine 205 | 206 | # Services: Caches 207 | 208 | memcached-1: 209 | <<: *memcached-base 210 | 211 | memcached-2: 212 | <<: *memcached-base 213 | 214 | redis: 215 | <<: *redis-base 216 | 217 | # Services: Selenium 218 | 219 | selenium-chrome: 220 | <<: *selenium-base 221 | image: selenium/node-chrome 222 | 223 | selenium-edge: 224 | <<: *selenium-base 225 | image: selenium/node-edge 226 | 227 | selenium-firefox: 228 | <<: *selenium-base 229 | image: selenium/node-firefox 230 | 231 | selenium-hub: 232 | image: selenium/hub 233 | ports: 234 | - "4442-4444:4442-4444" 235 | deploy: 236 | mode: global 237 | restart: unless-stopped 238 | healthcheck: 239 | test: /opt/bin/check-grid.sh 240 | interval: 15s 241 | timeout: 30s 242 | retries: 5 243 | 244 | # Commands: Tests 245 | 246 | mariadb: 247 | <<: *base 248 | depends_on: 249 | <<: *depends-on-caches 250 | mariadb-db: 251 | condition: service_healthy 252 | environment: 253 | - DATABASE_ENGINE=django.db.backends.mysql 254 | - DATABASE_HOST=mariadb-db 255 | 256 | mysql: 257 | <<: *base 258 | depends_on: 259 | <<: *depends-on-caches 260 | mysql-db: 261 | condition: service_healthy 262 | environment: 263 | - DATABASE_ENGINE=django.db.backends.mysql 264 | - DATABASE_HOST=mysql-db 265 | 266 | oracle: 267 | <<: *base 268 | depends_on: 269 | <<: *depends-on-caches 270 | oracle-db: 271 | condition: service_healthy 272 | environment: 273 | - DATABASE_ENGINE=django.db.backends.oracle 274 | - DATABASE_NAME=oracle-db:1521/freepdb1 275 | 276 | postgresql: 277 | <<: *base 278 | depends_on: 279 | <<: *depends-on-caches 280 | postgresql-db: 281 | condition: service_healthy 282 | environment: 283 | - DATABASE_ENGINE=django.db.backends.postgresql 284 | - DATABASE_HOST=postgresql-db 285 | 286 | sqlite: 287 | <<: *base 288 | depends_on: 289 | <<: *depends-on-caches 290 | environment: 291 | - DATABASE_ENGINE=django.db.backends.sqlite3 292 | 293 | # Commands: Tests: GIS 294 | 295 | mariadb-gis: 296 | <<: *base 297 | depends_on: 298 | <<: *depends-on-caches 299 | mariadb-gis-db: 300 | condition: service_healthy 301 | environment: 302 | - DATABASE_ENGINE=django.contrib.gis.db.backends.mysql 303 | - DATABASE_HOST=mariadb-gis-db 304 | 305 | mysql-gis: 306 | <<: *base 307 | depends_on: 308 | <<: *depends-on-caches 309 | mysql-gis-db: 310 | condition: service_healthy 311 | environment: 312 | - DATABASE_ENGINE=django.contrib.gis.db.backends.mysql 313 | - DATABASE_HOST=mysql-gis-db 314 | 315 | oracle-gis: 316 | <<: *base 317 | depends_on: 318 | <<: *depends-on-caches 319 | oracle-gis-db: 320 | condition: service_healthy 321 | environment: 322 | - DATABASE_ENGINE=django.contrib.gis.db.backends.oracle 323 | - DATABASE_NAME=oracle-gis-db:1521/freepdb1 324 | 325 | postgresql-gis: 326 | <<: *base 327 | depends_on: 328 | <<: *depends-on-caches 329 | postgresql-gis-db: 330 | condition: service_healthy 331 | environment: 332 | - DATABASE_ENGINE=django.contrib.gis.db.backends.postgis 333 | - DATABASE_HOST=postgresql-gis-db 334 | 335 | sqlite-gis: 336 | <<: *base 337 | depends_on: 338 | <<: *depends-on-caches 339 | environment: 340 | - DATABASE_ENGINE=django.contrib.gis.db.backends.spatialite 341 | 342 | # Commands: Tests: Selenium 343 | 344 | chrome: 345 | <<: *base 346 | entrypoint: >- 347 | /django/entrypoint.bash 348 | --selenium=chrome 349 | --selenium-hub=http://selenium-hub:4444/wd/hub 350 | depends_on: 351 | selenium-hub: 352 | condition: service_healthy 353 | selenium-chrome: 354 | condition: service_started 355 | environment: 356 | - DATABASE_ENGINE=django.db.backends.sqlite3 357 | 358 | edge: 359 | <<: *base 360 | entrypoint: >- 361 | /django/entrypoint.bash 362 | --selenium=edge 363 | --selenium-hub=http://selenium-hub:4444/wd/hub 364 | depends_on: 365 | selenium-hub: 366 | condition: service_healthy 367 | selenium-edge: 368 | condition: service_started 369 | environment: 370 | - DATABASE_ENGINE=django.db.backends.sqlite3 371 | 372 | firefox: 373 | <<: *base 374 | entrypoint: >- 375 | /django/entrypoint.bash 376 | --selenium=firefox 377 | --selenium-hub=http://selenium-hub:4444/wd/hub 378 | depends_on: 379 | selenium-hub: 380 | condition: service_healthy 381 | selenium-firefox: 382 | condition: service_started 383 | environment: 384 | - DATABASE_ENGINE=django.db.backends.sqlite3 385 | 386 | # Commands: Other 387 | 388 | pre-commit: 389 | <<: *base 390 | entrypoint: pre-commit run --all-files 391 | working_dir: /django/source 392 | environment: 393 | # XXX: Disable eslint due to issues finding dependencies. 394 | # See https://github.com/django/django/pull/18162 395 | SKIP: eslint 396 | 397 | sphinx: 398 | <<: *base 399 | entrypoint: make 400 | working_dir: /django/source/docs 401 | environment: 402 | BUILDDIR: /django/output/docs 403 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Toolbox 2 | 3 | Tooling and test execution support for [Django][0] :unicorn: 4 | 5 | :heart: Support Django development by [donating][1] to the Django Software Foundation. 6 | 7 | 8 | ## Highlights 9 | 10 | - :test\_tube: Test supported core database backends — MariaDB, MySQL, Oracle, PostgreSQL, SQLite 11 | - :earth\_africa: Test supported core geospatial backends — MariaDB, MySQL, Oracle, PostGIS, SpatiaLite 12 | - :globe\_with\_meridians: Test user interfaces in different browsers using Selenium — Chrome, Edge, Firefox 13 | - :snake: Test using different Python interpreters — CPython, PyPy 14 | - :broom: Execute linting and formatting tools on the Django repository 15 | - :books: Build the project documentation using Sphinx and run spelling and link checkers 16 | 17 | 18 | ## Quickstart 19 | 20 | 1. Make sure that you have Docker installed. 21 | 22 | 2. Clone this repository as well as the Django repository, e.g. 23 | 24 | ```console 25 | $ mkdir ~/Sources 26 | $ cd ~/Sources 27 | $ git clone https://github.com/django/django.git 28 | $ git clone https://github.com/django/django-docker-box.git 29 | $ cd django-docker-box 30 | ``` 31 | 32 | > [!IMPORTANT] 33 | > As long as the two repositories are adjacent the Django source repository will be discovered. 34 | > A different path can be specified by setting the `DJANGO_PATH` environment variable. 35 | 36 | 3. Build the image: 37 | 38 | ```console 39 | $ docker compose build sqlite 40 | ``` 41 | 42 | 4. Run the tests: 43 | 44 | ```console 45 | $ docker compose run --rm sqlite 46 | ``` 47 | 48 | 49 | ## Running Tests 50 | 51 | All of the test commands detailed below can be passed additional arguments that 52 | are provided to the `runtests.py` entrypoint. You can see a list of these 53 | arguments by running the following command: 54 | 55 | ```console 56 | $ docker compose run --rm sqlite --help 57 | ``` 58 | 59 | ### Standard Tests 60 | 61 | To run the standard set of tests you can use the following commands: 62 | 63 | ```console 64 | $ docker compose run --rm mariadb 65 | $ docker compose run --rm mysql 66 | $ docker compose run --rm oracle 67 | $ docker compose run --rm postgresql 68 | $ docker compose run --rm sqlite 69 | ``` 70 | 71 | Each of the above commands will run the test suite for a different supported 72 | database. 73 | 74 | More information about [running the unit tests][7] for Django can be found in 75 | the documentation. 76 | 77 | 78 | ### Geospatial Tests 79 | 80 | To run tests on geospatial features you can use the following commands: 81 | 82 | ```console 83 | $ docker compose run --rm mariadb-gis 84 | $ docker compose run --rm mysql-gis 85 | $ docker compose run --rm oracle-gis 86 | $ docker compose run --rm postgresql-gis 87 | $ docker compose run --rm sqlite-gis 88 | ``` 89 | 90 | Each of the above commands will run the test suite for a different supported 91 | geospatial database. 92 | 93 | > [!TIP] 94 | > To only run the subset of tests for geospatial features, pass `gis_tests` as 95 | > an argument to specify that only that folder of tests should be collected, 96 | > e.g. 97 | > 98 | > ```console 99 | > $ docker compose run --rm sqlite-gis gis_tests 100 | > ``` 101 | 102 | More information about [running the GeoDjango tests][9] for Django can be found 103 | in the documentation. 104 | 105 | 106 | ### User Interface Tests 107 | 108 | To run tests on user interfaces you can use the following commands: 109 | 110 | ```console 111 | $ docker compose run --rm chrome 112 | $ docker compose run --rm edge 113 | $ docker compose run --rm firefox 114 | ``` 115 | 116 | Each of the above commands will run the subset of user interface tests for a 117 | different supported web browser. The tests are executed using Selenium. 118 | 119 | To capture screenshots of certain test cases used for comparison to avoid 120 | regressions, the `--screenshots` flag can be passed. 121 | 122 | More information about [running the Selenium tests][8] for Django can be found 123 | in the documentation. 124 | 125 | 126 | ## Running Tools 127 | 128 | 129 | ### Linting & Formatting 130 | 131 | Django uses the following linting and formatting tools: `black`, `flake8`, 132 | `isort`, and `eslint`. To ensure that the correct versions are used, Django 133 | also supports using `pre-commit` which is the mechanism provided here: 134 | 135 | ```console 136 | $ docker compose run --rm pre-commit 137 | ``` 138 | 139 | You can run individual tools by passing them as an argument: 140 | 141 | ```console 142 | $ docker compose run --rm pre-commit black 143 | $ docker compose run --rm pre-commit blacken-docs 144 | $ docker compose run --rm pre-commit isort 145 | $ docker compose run --rm pre-commit flake8 146 | $ docker compose run --rm pre-commit eslint # XXX: Currently not working. 147 | ``` 148 | 149 | More information about Django's [coding style][5] can be found in the 150 | documentation. 151 | 152 | ### Building Documentation 153 | 154 | Documentation for Django is built using Sphinx. Run the following to see the 155 | available commands: 156 | 157 | ```console 158 | $ docker compose run --rm sphinx 159 | ``` 160 | 161 | You may find the following builders particularly useful when working on 162 | documentation improvements: 163 | 164 | ```console 165 | $ docker compose run --rm sphinx dirhtml 166 | $ docker compose run --rm sphinx spelling 167 | $ docker compose run --rm sphinx linkcheck 168 | ``` 169 | 170 | The `BUILDDIR` environment variable has been set to generate output into the 171 | `./output/docs` path under this repository instead of the usual location in the 172 | Django source repository. You can alter this environment variable to generate 173 | to a different path if required. 174 | 175 | More information about [writing documentation][6] for Django can be found in 176 | the documentation. 177 | 178 | 179 | ### Other 180 | 181 | To enter a shell within the container, run: 182 | 183 | ```console 184 | $ docker compose run --rm --entrypoint=bash sqlite 185 | ``` 186 | 187 | ## Configuration 188 | 189 | The build of the container image can be customized by setting the following 190 | environment variables: 191 | 192 | | Environment Variable | Default Value | Description | 193 | | ----------------------- | ------------- | ---------------------------------------------------- | 194 | | `DJANGO_PATH` | `../django` | Path to the Django repostory on your local machine | 195 | | `PYTHON_IMPLEMENTATION` | `python` | Implementation of Python to use — `python` or `pypy` | 196 | | `PYTHON_VERSION` | `3.13` | Version of Python container image to use | 197 | 198 | The versions of various backend services can be switched by setting these environment variables: 199 | 200 | | Environment Variable | Default Value | Description | 201 | | ----------------------- | ------------- | ---------------------------------------------------- | 202 | | `MARIADB_VERSION` | `10.6` | Version of MariaDB container image to use | 203 | | `MYSQL_VERSION` | `8.0` | Version of MySQL container image to use | 204 | | `ORACLE_VERSION` | `23.5.0.0` | Version of Oracle container image to use | 205 | | `POSTGRESQL_VERSION` | `14` | Version of PostgreSQL container image to use | 206 | | `POSTGIS_VERSION` | `3.1` | Version of PostGIS extension to use | 207 | 208 | 209 | ### Python Versions 210 | 211 | The `PYTHON_VERSION` environment variable controls which version of Python you 212 | are running the tests against, e.g. 213 | 214 | ```console 215 | $ PYTHON_VERSION=3.13 docker compose run --rm sqlite 216 | ``` 217 | 218 | In addition, it's possible to select a different implementation of Python, i.e. 219 | PyPy instead of CPython, by setting the `PYTHON_IMPLEMENTATION` environment 220 | variable, e.g. 221 | 222 | ```console 223 | $ PYTHON_IMPLEMENTATION=pypy docker compose run --rm sqlite 224 | ``` 225 | 226 | Be warned, however, that support for PyPy is not as complete and there are more 227 | restrictions with respect to the range of versions available. 228 | 229 | ### Database Versions 230 | 231 | Most database container images are pulled from [Docker Hub][2]. Oracle database 232 | is pulled from the [Oracle Container Registry][3]. 233 | 234 | You can switch the version of the database you test against by changing the 235 | appropriate environment variable. Available options and their defaults can be 236 | found in the [configuration section](#Configuration). 237 | 238 | > [!WARNING] 239 | > Be aware that only a single version of a particular database may be running 240 | > at one time, so you will need to ensure that you tear down the previously 241 | > running instance before starting up the new one, e.g. 242 | > 243 | > ```console 244 | > $ docker compose ps --format='{{.Image}}' postgresql-db 245 | > postgres:13-alpine 246 | > $ docker compose down postgresql-db 247 | > [+] Running 1/1 248 | > ✔ Container django-docker-box-postgresql-db-1 Removed 0.2s 249 | > $ POSTGRESQL_VERSION=17 docker compose up --detach postgresql-db 250 | > [+] Running 1/1 251 | > ✔ Container django-docker-box-postgresql-db-1 Started 0.3s 252 | > $ docker compose ps --format='{{.Image}}' postgresql-db 253 | > postgres:17-alpine 254 | > ``` 255 | > 256 | > Alternatively, run the following to tear down the whole stack before bringing 257 | > up new containers running different versions: 258 | > 259 | > ```console 260 | > $ docker compose down 261 | > ``` 262 | 263 | > [!NOTE] 264 | > 265 | > Unlike other GIS database backends, for PostgreSQL with PostGIS you will need 266 | > to specify both versions: 267 | > 268 | > ```console 269 | > $ POSTGRESQL_VERSION=17 POSTGIS_VERSION=3.5 docker compose up --detach postgresql-gis-db 270 | > ``` 271 | 272 | To determine what database versions can be used you can check the release notes 273 | for the branch of Django that you have checked out, or alternatively there is 274 | the [supported database versions][4] page on Django's Trac Wiki. 275 | 276 | 277 | ### Other Versions 278 | 279 | For the Memcached, Redis, and Selenium container images, the latest container 280 | image tag is always used. 281 | 282 | Where possible, for backend services, we also use Alpine images where available 283 | for smaller image size and sometimes improved performance. 284 | 285 | 286 | ## Roadmap 287 | 288 | The following list is a collection of ideas for improvements that could be made 289 | with no promises that they'll be delivered: 290 | 291 | - Add a monthly scheduled full test matrix execution using GitHub Actions 292 | - Add support for some third-party databases, e.g. CockroachDB, SQL Server 293 | - Add support for test coverage execution and report generation 294 | - Add support for running accessibility tooling and report generation 295 | - Support report generation during monthly runs and publish to GitHub Pages 296 | - Publish pre-built container images to the GitHub Container Registry 297 | - Support testing against different versions of SQLite and SpatiaLite 298 | - Support running with Podman in addition to Docker 299 | - Support generating screenshots into `./output/screenshots/` 300 | 301 | 302 | [0]: https://www.djangoproject.com/ 303 | [1]: https://www.djangoproject.com/fundraising/ 304 | [2]: https://hub.docker.com/search?badges=official&badges=open_source 305 | [3]: https://container-registry.oracle.com/ords/ocr/ba/database/free 306 | [4]: https://code.djangoproject.com/wiki/SupportedDatabaseVersions 307 | [5]: https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/coding-style/ 308 | [6]: https://docs.djangoproject.com/en/stable/internals/contributing/writing-documentation/ 309 | [7]: https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/unit-tests/#running-the-unit-tests 310 | [8]: https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/unit-tests/#running-the-selenium-tests 311 | [9]: https://docs.djangoproject.com/en/stable/ref/contrib/gis/testing/#geodjango-tests 312 | --------------------------------------------------------------------------------