mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-09-04 01:10:42 +00:00
Add end-to-end testing framework with matrix support for Python and Django versions
This commit is contained in:
parent
42d089dcc6
commit
c4f50b6ef4
13 changed files with 1390 additions and 0 deletions
106
.github/workflows/e2e-tests.yml
vendored
Normal file
106
.github/workflows/e2e-tests.yml
vendored
Normal file
|
@ -0,0 +1,106 @@
|
|||
name: End-to-End Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Python ${{ matrix.python-version }} / Django ${{ matrix.django-version }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
|
||||
django-version: ['4.2', '5.0', '5.1']
|
||||
exclude:
|
||||
# Add any incompatible combinations here if needed
|
||||
# - python-version: '3.9'
|
||||
# django-version: '5.1'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install tox tox-gh-actions
|
||||
|
||||
- name: Build django-language-server
|
||||
run: |
|
||||
pip install maturin
|
||||
maturin develop --release
|
||||
|
||||
- name: Test with tox
|
||||
run: tox
|
||||
env:
|
||||
DJANGO_VERSION: ${{ matrix.django-version }}
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
|
||||
# Optional: Add a job for testing with different clients
|
||||
client-tests:
|
||||
name: Client Tests - ${{ matrix.client }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
client: ['vscode', 'neovim']
|
||||
include:
|
||||
- client: vscode
|
||||
client-setup: |
|
||||
# Setup for VS Code testing
|
||||
echo "Setting up VS Code for testing"
|
||||
- client: neovim
|
||||
client-setup: |
|
||||
# Setup for Neovim testing
|
||||
echo "Setting up Neovim for testing"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Set up client
|
||||
run: ${{ matrix.client-setup }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r tests/requirements-test.txt
|
||||
python -m pip install Django>=5.0,<5.1
|
||||
|
||||
- name: Build django-language-server
|
||||
run: |
|
||||
pip install maturin
|
||||
maturin develop --release
|
||||
|
||||
- name: Run client tests
|
||||
run: |
|
||||
pytest tests/clients/test_${{ matrix.client }}.py
|
23
README.md
23
README.md
|
@ -158,6 +158,29 @@ Code contributions are welcome from developers of all backgrounds. Rust expertis
|
|||
|
||||
So far it's all been built by a [a simple country CRUD web developer](https://youtu.be/7ij_1SQqbVo?si=hwwPyBjmaOGnvPPI&t=53) learning Rust along the way - send help!
|
||||
|
||||
### Testing
|
||||
|
||||
The project includes a comprehensive end-to-end testing framework that tests the language server against:
|
||||
|
||||
- Multiple Python versions (3.9, 3.10, 3.11, 3.12, 3.13)
|
||||
- Multiple Django versions (4.2, 5.0, 5.1)
|
||||
- Different LSP clients (VS Code, Neovim)
|
||||
|
||||
To run the tests locally:
|
||||
|
||||
```bash
|
||||
# Install tox
|
||||
pip install tox
|
||||
|
||||
# Run tests with the default Python and Django versions
|
||||
tox
|
||||
|
||||
# Run tests with a specific Python and Django version
|
||||
tox -e py311-django50
|
||||
```
|
||||
|
||||
For more information about the testing framework, see the [tests/README.md](tests/README.md) file.
|
||||
|
||||
## License
|
||||
|
||||
django-language-server is licensed under the Apache License, Version 2.0. See the [`LICENSE`](LICENSE) file for more information.
|
||||
|
|
|
@ -11,6 +11,14 @@ dev = [
|
|||
docs = [
|
||||
"mkdocs-material>=9.5.49",
|
||||
]
|
||||
test = [
|
||||
"pytest>=7.4.0",
|
||||
"pytest-asyncio>=0.21.1",
|
||||
"pygls>=1.1.0",
|
||||
"tox>=4.11.0",
|
||||
"tox-gh-actions>=3.1.3",
|
||||
"coverage>=7.3.2",
|
||||
]
|
||||
|
||||
[project]
|
||||
name = "django-language-server"
|
||||
|
|
98
tests/README.md
Normal file
98
tests/README.md
Normal file
|
@ -0,0 +1,98 @@
|
|||
# Django Language Server Testing Framework
|
||||
|
||||
This directory contains the end-to-end testing framework for the Django Language Server.
|
||||
|
||||
## Overview
|
||||
|
||||
The testing framework is designed to test the Django Language Server against:
|
||||
|
||||
- Multiple Python versions (3.9, 3.10, 3.11, 3.12, 3.13)
|
||||
- Multiple Django versions (4.2, 5.0, 5.1)
|
||||
- Different LSP clients (VS Code, Neovim)
|
||||
|
||||
## Directory Structure
|
||||
|
||||
- `e2e/`: End-to-end tests for the language server
|
||||
- `fixtures/`: Test fixtures, including Django project generation
|
||||
- `clients/`: Client-specific tests for different editors
|
||||
- `conftest.py`: Pytest configuration and fixtures
|
||||
- `requirements-test.txt`: Test dependencies
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Local Testing
|
||||
|
||||
To run the tests locally, you can use tox:
|
||||
|
||||
```bash
|
||||
# Install tox
|
||||
pip install tox
|
||||
|
||||
# Run tests with the default Python and Django versions
|
||||
tox
|
||||
|
||||
# Run tests with a specific Python and Django version
|
||||
tox -e py311-django50
|
||||
|
||||
# Run tests with a specific test file
|
||||
tox -- tests/e2e/test_basic_functionality.py
|
||||
```
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
The tests are automatically run on GitHub Actions for all supported Python and Django versions. The workflow is defined in `.github/workflows/e2e-tests.yml`.
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
### Adding a New End-to-End Test
|
||||
|
||||
1. Create a new test file in the `e2e/` directory
|
||||
2. Use the existing fixtures from `conftest.py`
|
||||
3. Write your test using the pytest-asyncio framework
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_my_feature(lsp_client, django_project):
|
||||
# Test code here
|
||||
pass
|
||||
```
|
||||
|
||||
### Adding a New Client Test
|
||||
|
||||
1. Create a new test file in the `clients/` directory
|
||||
2. Create fixtures for the client configuration
|
||||
3. Write tests for the client integration
|
||||
|
||||
## Test Fixtures
|
||||
|
||||
### Django Project
|
||||
|
||||
The `django_project` fixture creates a Django project with:
|
||||
|
||||
- A basic project structure
|
||||
- A sample app with views and templates
|
||||
- Template files with Django template tags
|
||||
|
||||
### Language Server
|
||||
|
||||
The `language_server_process` fixture starts the Django Language Server in TCP mode for testing.
|
||||
|
||||
### LSP Client
|
||||
|
||||
The `lsp_client` fixture creates an LSP client connected to the language server for sending requests and receiving responses.
|
||||
|
||||
## Extending the Framework
|
||||
|
||||
### Testing with Additional Django Versions
|
||||
|
||||
To test with additional Django versions, update the `envlist` in `tox.ini` and the matrix in `.github/workflows/e2e-tests.yml`.
|
||||
|
||||
### Testing with Additional Clients
|
||||
|
||||
To test with additional LSP clients, add a new test file in the `clients/` directory and update the matrix in `.github/workflows/e2e-tests.yml`.
|
||||
|
||||
### Testing Additional Features
|
||||
|
||||
To test additional features of the language server, add new test files in the `e2e/` directory.
|
214
tests/clients/test_neovim.py
Normal file
214
tests/clients/test_neovim.py
Normal file
|
@ -0,0 +1,214 @@
|
|||
"""
|
||||
Tests for Neovim integration with django-language-server.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from fixtures.create_django_project import create_django_project, cleanup_django_project
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def neovim_config_dir():
|
||||
"""Create a temporary directory for Neovim configuration."""
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
config_dir = Path(temp_dir) / "nvim"
|
||||
config_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Create lua directory
|
||||
lua_dir = config_dir / "lua"
|
||||
lua_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Create init.lua
|
||||
init_lua = """
|
||||
-- Basic Neovim configuration
|
||||
vim.opt.number = true
|
||||
vim.opt.relativenumber = true
|
||||
vim.opt.expandtab = true
|
||||
vim.opt.shiftwidth = 4
|
||||
vim.opt.tabstop = 4
|
||||
vim.opt.smartindent = true
|
||||
vim.opt.termguicolors = true
|
||||
|
||||
-- Load plugins
|
||||
require('plugins')
|
||||
|
||||
-- Configure LSP
|
||||
require('lsp')
|
||||
"""
|
||||
|
||||
with open(config_dir / "init.lua", "w") as f:
|
||||
f.write(init_lua)
|
||||
|
||||
# Create plugins.lua
|
||||
plugins_dir = lua_dir / "plugins"
|
||||
plugins_dir.mkdir(exist_ok=True)
|
||||
|
||||
plugins_lua = """
|
||||
-- Bootstrap lazy.nvim
|
||||
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
|
||||
if not vim.loop.fs_stat(lazypath) then
|
||||
vim.fn.system({
|
||||
"git",
|
||||
"clone",
|
||||
"--filter=blob:none",
|
||||
"https://github.com/folke/lazy.nvim.git",
|
||||
"--branch=stable",
|
||||
lazypath,
|
||||
})
|
||||
end
|
||||
vim.opt.rtp:prepend(lazypath)
|
||||
|
||||
-- Configure plugins
|
||||
require("lazy").setup({
|
||||
-- LSP
|
||||
{
|
||||
"neovim/nvim-lspconfig",
|
||||
dependencies = {
|
||||
"hrsh7th/cmp-nvim-lsp",
|
||||
},
|
||||
},
|
||||
|
||||
-- Autocompletion
|
||||
{
|
||||
"hrsh7th/nvim-cmp",
|
||||
dependencies = {
|
||||
"hrsh7th/cmp-buffer",
|
||||
"hrsh7th/cmp-path",
|
||||
"hrsh7th/cmp-nvim-lsp",
|
||||
"L3MON4D3/LuaSnip",
|
||||
"saadparwaiz1/cmp_luasnip",
|
||||
},
|
||||
},
|
||||
})
|
||||
"""
|
||||
|
||||
with open(plugins_dir / "init.lua", "w") as f:
|
||||
f.write(plugins_lua)
|
||||
|
||||
# Create lsp.lua
|
||||
lsp_dir = lua_dir / "lsp"
|
||||
lsp_dir.mkdir(exist_ok=True)
|
||||
|
||||
lsp_lua = """
|
||||
local lspconfig = require('lspconfig')
|
||||
local util = require('lspconfig.util')
|
||||
local cmp_nvim_lsp = require('cmp_nvim_lsp')
|
||||
|
||||
-- Add additional capabilities supported by nvim-cmp
|
||||
local capabilities = cmp_nvim_lsp.default_capabilities()
|
||||
|
||||
-- Configure django-language-server
|
||||
lspconfig.djls = {
|
||||
default_config = {
|
||||
cmd = { 'djls' },
|
||||
filetypes = { 'django-html', 'python' },
|
||||
root_dir = function(fname)
|
||||
-- Find Django project root (where manage.py is)
|
||||
return util.root_pattern('manage.py')(fname)
|
||||
end,
|
||||
settings = {},
|
||||
},
|
||||
capabilities = capabilities,
|
||||
}
|
||||
|
||||
-- Set up nvim-cmp
|
||||
local cmp = require('cmp')
|
||||
local luasnip = require('luasnip')
|
||||
|
||||
cmp.setup({
|
||||
snippet = {
|
||||
expand = function(args)
|
||||
luasnip.lsp_expand(args.body)
|
||||
end,
|
||||
},
|
||||
mapping = cmp.mapping.preset.insert({
|
||||
['<C-d>'] = cmp.mapping.scroll_docs(-4),
|
||||
['<C-f>'] = cmp.mapping.scroll_docs(4),
|
||||
['<C-Space>'] = cmp.mapping.complete(),
|
||||
['<CR>'] = cmp.mapping.confirm({ select = true }),
|
||||
['<Tab>'] = cmp.mapping(function(fallback)
|
||||
if cmp.visible() then
|
||||
cmp.select_next_item()
|
||||
elseif luasnip.expand_or_jumpable() then
|
||||
luasnip.expand_or_jump()
|
||||
else
|
||||
fallback()
|
||||
end
|
||||
end, { 'i', 's' }),
|
||||
['<S-Tab>'] = cmp.mapping(function(fallback)
|
||||
if cmp.visible() then
|
||||
cmp.select_prev_item()
|
||||
elseif luasnip.jumpable(-1) then
|
||||
luasnip.jump(-1)
|
||||
else
|
||||
fallback()
|
||||
end
|
||||
end, { 'i', 's' }),
|
||||
}),
|
||||
sources = cmp.config.sources({
|
||||
{ name = 'nvim_lsp' },
|
||||
{ name = 'luasnip' },
|
||||
}, {
|
||||
{ name = 'buffer' },
|
||||
}),
|
||||
})
|
||||
|
||||
-- Set up filetypes
|
||||
vim.filetype.add({
|
||||
extension = {
|
||||
html = function(path, bufnr)
|
||||
-- Check if this is a Django project
|
||||
local is_django = vim.fn.findfile('manage.py', vim.fn.expand('%:p:h') .. ';') ~= ''
|
||||
if is_django then
|
||||
return 'django-html'
|
||||
end
|
||||
return 'html'
|
||||
end,
|
||||
},
|
||||
})
|
||||
"""
|
||||
|
||||
with open(lsp_dir / "init.lua", "w") as f:
|
||||
f.write(lsp_lua)
|
||||
|
||||
yield config_dir
|
||||
|
||||
# Clean up
|
||||
import shutil
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
|
||||
def test_neovim_config_structure(neovim_config_dir):
|
||||
"""Test that the Neovim configuration structure is valid."""
|
||||
assert (neovim_config_dir / "init.lua").exists()
|
||||
assert (neovim_config_dir / "lua" / "plugins" / "init.lua").exists()
|
||||
assert (neovim_config_dir / "lua" / "lsp" / "init.lua").exists()
|
||||
|
||||
|
||||
def test_neovim_lsp_config(neovim_config_dir):
|
||||
"""Test that the Neovim LSP configuration is valid."""
|
||||
with open(neovim_config_dir / "lua" / "lsp" / "init.lua", "r") as f:
|
||||
lsp_config = f.read()
|
||||
|
||||
assert "lspconfig.djls" in lsp_config
|
||||
assert "cmd = { 'djls' }" in lsp_config
|
||||
assert "filetypes = { 'django-html', 'python' }" in lsp_config
|
||||
|
||||
|
||||
# This test is a placeholder for actual Neovim integration testing
|
||||
# In a real implementation, you would use something like neovim-test
|
||||
# to launch Neovim with the configuration and test it
|
||||
@pytest.mark.skip(reason="Requires Neovim to be installed")
|
||||
def test_neovim_with_language_server(neovim_config_dir):
|
||||
"""Test Neovim with the language server."""
|
||||
# This would require Neovim to be installed and a way to programmatically
|
||||
# interact with it, which is beyond the scope of this example
|
||||
pass
|
185
tests/clients/test_vscode.py
Normal file
185
tests/clients/test_vscode.py
Normal file
|
@ -0,0 +1,185 @@
|
|||
"""
|
||||
Tests for VS Code integration with django-language-server.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from fixtures.create_django_project import create_django_project, cleanup_django_project
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vscode_extension_dir():
|
||||
"""Create a temporary directory for a VS Code extension."""
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
extension_dir = Path(temp_dir) / "django-language-server-vscode"
|
||||
extension_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Create package.json
|
||||
package_json = {
|
||||
"name": "django-language-server-vscode",
|
||||
"displayName": "Django Language Server",
|
||||
"description": "VS Code extension for Django Language Server",
|
||||
"version": "0.0.1",
|
||||
"engines": {
|
||||
"vscode": "^1.60.0"
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onLanguage:django-html",
|
||||
"onLanguage:python"
|
||||
],
|
||||
"main": "./extension.js",
|
||||
"contributes": {
|
||||
"languages": [
|
||||
{
|
||||
"id": "django-html",
|
||||
"aliases": ["Django HTML", "django-html"],
|
||||
"extensions": [".html", ".djhtml"],
|
||||
"configuration": "./language-configuration.json"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"title": "Django Language Server",
|
||||
"properties": {
|
||||
"djangoLanguageServer.path": {
|
||||
"type": "string",
|
||||
"default": "djls",
|
||||
"description": "Path to the Django Language Server executable"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
}
|
||||
}
|
||||
|
||||
with open(extension_dir / "package.json", "w") as f:
|
||||
json.dump(package_json, f, indent=2)
|
||||
|
||||
# Create extension.js
|
||||
extension_js = """
|
||||
const vscode = require('vscode');
|
||||
const { LanguageClient, TransportKind } = require('vscode-languageclient/node');
|
||||
|
||||
let client;
|
||||
|
||||
function activate(context) {
|
||||
const serverPath = vscode.workspace.getConfiguration('djangoLanguageServer').get('path');
|
||||
|
||||
const serverOptions = {
|
||||
command: serverPath,
|
||||
args: [],
|
||||
transport: TransportKind.stdio
|
||||
};
|
||||
|
||||
const clientOptions = {
|
||||
documentSelector: [
|
||||
{ scheme: 'file', language: 'django-html' },
|
||||
{ scheme: 'file', language: 'python' }
|
||||
],
|
||||
synchronize: {
|
||||
fileEvents: vscode.workspace.createFileSystemWatcher('**/*.{py,html}')
|
||||
}
|
||||
};
|
||||
|
||||
client = new LanguageClient(
|
||||
'djangoLanguageServer',
|
||||
'Django Language Server',
|
||||
serverOptions,
|
||||
clientOptions
|
||||
);
|
||||
|
||||
client.start();
|
||||
}
|
||||
|
||||
function deactivate() {
|
||||
if (client) {
|
||||
return client.stop();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
activate,
|
||||
deactivate
|
||||
};
|
||||
"""
|
||||
|
||||
with open(extension_dir / "extension.js", "w") as f:
|
||||
f.write(extension_js)
|
||||
|
||||
# Create language-configuration.json
|
||||
language_config = {
|
||||
"comments": {
|
||||
"blockComment": ["{% comment %}", "{% endcomment %}"]
|
||||
},
|
||||
"brackets": [
|
||||
["{%", "%}"],
|
||||
["{{", "}}"],
|
||||
["(", ")"],
|
||||
["[", "]"],
|
||||
["{", "}"]
|
||||
],
|
||||
"autoClosingPairs": [
|
||||
{ "open": "{%", "close": " %}" },
|
||||
{ "open": "{{", "close": " }}" },
|
||||
{ "open": "(", "close": ")" },
|
||||
{ "open": "[", "close": "]" },
|
||||
{ "open": "{", "close": "}" }
|
||||
],
|
||||
"surroundingPairs": [
|
||||
{ "open": "{%", "close": "%}" },
|
||||
{ "open": "{{", "close": "}}" },
|
||||
{ "open": "(", "close": ")" },
|
||||
{ "open": "[", "close": "]" },
|
||||
{ "open": "{", "close": "}" }
|
||||
]
|
||||
}
|
||||
|
||||
with open(extension_dir / "language-configuration.json", "w") as f:
|
||||
json.dump(language_config, f, indent=2)
|
||||
|
||||
yield extension_dir
|
||||
|
||||
# Clean up
|
||||
import shutil
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
|
||||
def test_vscode_extension_structure(vscode_extension_dir):
|
||||
"""Test that the VS Code extension structure is valid."""
|
||||
assert (vscode_extension_dir / "package.json").exists()
|
||||
assert (vscode_extension_dir / "extension.js").exists()
|
||||
assert (vscode_extension_dir / "language-configuration.json").exists()
|
||||
|
||||
|
||||
def test_vscode_extension_package_json(vscode_extension_dir):
|
||||
"""Test that the package.json is valid."""
|
||||
with open(vscode_extension_dir / "package.json", "r") as f:
|
||||
package_json = json.load(f)
|
||||
|
||||
assert package_json["name"] == "django-language-server-vscode"
|
||||
assert "djangoLanguageServer.path" in package_json["contributes"]["configuration"]["properties"]
|
||||
|
||||
|
||||
# This test is a placeholder for actual VS Code integration testing
|
||||
# In a real implementation, you would use something like vscode-test
|
||||
# to launch VS Code with the extension and test it
|
||||
@pytest.mark.skip(reason="Requires VS Code to be installed")
|
||||
def test_vscode_extension_with_language_server(vscode_extension_dir):
|
||||
"""Test the VS Code extension with the language server."""
|
||||
# This would require VS Code to be installed and a way to programmatically
|
||||
# interact with it, which is beyond the scope of this example
|
||||
pass
|
140
tests/conftest.py
Normal file
140
tests/conftest.py
Normal file
|
@ -0,0 +1,140 @@
|
|||
"""
|
||||
Pytest configuration for django-language-server tests.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from fixtures.create_django_project import create_django_project, cleanup_django_project
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def django_version() -> str:
|
||||
"""
|
||||
Get the Django version to use for testing.
|
||||
|
||||
Returns:
|
||||
Django version string (e.g., "4.2", "5.0")
|
||||
"""
|
||||
return os.environ.get("DJANGO_VERSION", "5.0")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def django_project(django_version: str) -> Path:
|
||||
"""
|
||||
Create a Django project for testing.
|
||||
|
||||
Args:
|
||||
django_version: Django version to use
|
||||
|
||||
Returns:
|
||||
Path to the Django project
|
||||
"""
|
||||
project_dir = create_django_project(django_version=django_version)
|
||||
yield project_dir
|
||||
cleanup_django_project(project_dir)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def language_server_path() -> Path:
|
||||
"""
|
||||
Get the path to the django-language-server executable.
|
||||
|
||||
Returns:
|
||||
Path to the django-language-server executable
|
||||
"""
|
||||
# Try to find the executable in the current environment
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "djls", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return Path(sys.executable).parent / "djls"
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
# If not found, build it
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", "maturin"],
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
["maturin", "develop", "--release"],
|
||||
check=True,
|
||||
cwd=Path(__file__).parent.parent,
|
||||
)
|
||||
return Path(sys.executable).parent / "djls"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def language_server_process(language_server_path: Path, django_project: Path):
|
||||
"""
|
||||
Start the language server process.
|
||||
|
||||
Args:
|
||||
language_server_path: Path to the language server executable
|
||||
django_project: Path to the Django project
|
||||
|
||||
Yields:
|
||||
Process object for the language server
|
||||
"""
|
||||
# Start the language server in TCP mode
|
||||
process = subprocess.Popen(
|
||||
[
|
||||
str(language_server_path),
|
||||
"--tcp",
|
||||
"--port",
|
||||
"8888",
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
cwd=django_project,
|
||||
env={**os.environ, "PYTHONPATH": str(django_project)},
|
||||
)
|
||||
|
||||
# Wait a moment for the server to start
|
||||
await asyncio.sleep(2)
|
||||
|
||||
yield process
|
||||
|
||||
# Clean up
|
||||
process.terminate()
|
||||
process.wait(timeout=5)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def lsp_client(language_server_process):
|
||||
"""
|
||||
Create an LSP client connected to the language server.
|
||||
|
||||
Args:
|
||||
language_server_process: Process object for the language server
|
||||
|
||||
Yields:
|
||||
LSP client
|
||||
"""
|
||||
from pygls.client import JsonRPCClient
|
||||
|
||||
client = JsonRPCClient(tcp=True)
|
||||
await client.connect("localhost", 8888)
|
||||
|
||||
# Initialize the client
|
||||
await client.initialize_async(
|
||||
processId=os.getpid(),
|
||||
rootUri=f"file://{os.getcwd()}",
|
||||
capabilities={},
|
||||
)
|
||||
|
||||
yield client
|
||||
|
||||
# Clean up
|
||||
await client.shutdown_async()
|
||||
await client.exit_async()
|
||||
await client.close()
|
129
tests/e2e/test_basic_functionality.py
Normal file
129
tests/e2e/test_basic_functionality.py
Normal file
|
@ -0,0 +1,129 @@
|
|||
"""
|
||||
Basic end-to-end tests for django-language-server.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pygls.client import JsonRPCClient
|
||||
from pygls.protocol import LanguageServerProtocol
|
||||
from pygls.types import (
|
||||
CompletionParams,
|
||||
DidOpenTextDocumentParams,
|
||||
Position,
|
||||
TextDocumentIdentifier,
|
||||
TextDocumentItem,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_server_initialization(lsp_client: JsonRPCClient):
|
||||
"""Test that the server initializes correctly."""
|
||||
# Server should already be initialized by the fixture
|
||||
assert lsp_client.protocol.state == LanguageServerProtocol.STATE.INITIALIZED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_template_tag_completion(
|
||||
lsp_client: JsonRPCClient, django_project: Path
|
||||
):
|
||||
"""Test template tag completion."""
|
||||
# Find a template file
|
||||
template_files = list(django_project.glob("**/templates/**/*.html"))
|
||||
assert template_files, "No template files found"
|
||||
|
||||
template_file = template_files[0]
|
||||
template_uri = f"file://{template_file}"
|
||||
|
||||
# Read the template content
|
||||
with open(template_file, "r") as f:
|
||||
template_content = f.read()
|
||||
|
||||
# Open the document in the language server
|
||||
await lsp_client.text_document_did_open_async(
|
||||
DidOpenTextDocumentParams(
|
||||
text_document=TextDocumentItem(
|
||||
uri=template_uri,
|
||||
language_id="django-html",
|
||||
version=1,
|
||||
text=template_content,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Wait a moment for the server to process the document
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Find a position after {% to test completion
|
||||
lines = template_content.split("\n")
|
||||
for i, line in enumerate(lines):
|
||||
if "{%" in line:
|
||||
position = Position(
|
||||
line=i,
|
||||
character=line.index("{%") + 2,
|
||||
)
|
||||
break
|
||||
else:
|
||||
# If no {% found, add one at the end and update the document
|
||||
position = Position(line=len(lines), character=2)
|
||||
template_content += "\n{% "
|
||||
|
||||
# Update the document
|
||||
await lsp_client.text_document_did_change_async(
|
||||
{
|
||||
"textDocument": {
|
||||
"uri": template_uri,
|
||||
"version": 2,
|
||||
},
|
||||
"contentChanges": [
|
||||
{
|
||||
"text": template_content,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Request completions
|
||||
completions = await lsp_client.completion_async(
|
||||
CompletionParams(
|
||||
text_document=TextDocumentIdentifier(uri=template_uri),
|
||||
position=position,
|
||||
)
|
||||
)
|
||||
|
||||
# Check that we got some completions
|
||||
assert completions is not None
|
||||
assert len(completions.items) > 0
|
||||
|
||||
# Check that common Django template tags are included
|
||||
tag_labels = [item.label for item in completions.items]
|
||||
common_tags = ["for", "if", "block", "extends", "include"]
|
||||
|
||||
for tag in common_tags:
|
||||
assert any(tag in label for label in tag_labels), f"Tag '{tag}' not found in completions"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_django_settings_detection(
|
||||
lsp_client: JsonRPCClient, django_project: Path
|
||||
):
|
||||
"""Test that the server correctly detects Django settings."""
|
||||
# This is a basic test to ensure the server can detect Django settings
|
||||
# We'll use the workspace/executeCommand API to check this
|
||||
|
||||
result = await lsp_client.execute_command_async(
|
||||
{
|
||||
"command": "djls.debug.projectInfo",
|
||||
"arguments": [],
|
||||
}
|
||||
)
|
||||
|
||||
# The result should contain information about the Django project
|
||||
assert result is not None
|
||||
assert "django" in str(result).lower()
|
||||
assert "settings" in str(result).lower()
|
231
tests/fixtures/create_django_project.py
vendored
Normal file
231
tests/fixtures/create_django_project.py
vendored
Normal file
|
@ -0,0 +1,231 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Create a Django project for testing.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def create_django_project(
|
||||
project_name: str = "testproject",
|
||||
app_name: str = "testapp",
|
||||
django_version: str | None = None,
|
||||
) -> Path:
|
||||
"""
|
||||
Create a Django project for testing.
|
||||
|
||||
Args:
|
||||
project_name: Name of the Django project
|
||||
app_name: Name of the Django app
|
||||
django_version: Django version to use (e.g., "4.2", "5.0")
|
||||
|
||||
Returns:
|
||||
Path to the created Django project
|
||||
"""
|
||||
# Create a temporary directory
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
project_dir = Path(temp_dir) / project_name
|
||||
|
||||
# Install Django if a specific version is requested
|
||||
if django_version:
|
||||
subprocess.check_call(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
f"Django>={django_version},<{float(django_version) + 0.1}",
|
||||
]
|
||||
)
|
||||
|
||||
# Create Django project
|
||||
subprocess.check_call(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"django",
|
||||
"startproject",
|
||||
project_name,
|
||||
temp_dir,
|
||||
]
|
||||
)
|
||||
|
||||
# Create Django app
|
||||
os.chdir(temp_dir)
|
||||
subprocess.check_call(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"django",
|
||||
"startapp",
|
||||
app_name,
|
||||
]
|
||||
)
|
||||
|
||||
# Add app to INSTALLED_APPS
|
||||
settings_path = Path(temp_dir) / "settings.py"
|
||||
if not settings_path.exists():
|
||||
settings_path = Path(temp_dir) / project_name / "settings.py"
|
||||
|
||||
with open(settings_path, "r") as f:
|
||||
settings_content = f.read()
|
||||
|
||||
settings_content = settings_content.replace(
|
||||
"INSTALLED_APPS = [",
|
||||
f"INSTALLED_APPS = [\n '{app_name}',",
|
||||
)
|
||||
|
||||
with open(settings_path, "w") as f:
|
||||
f.write(settings_content)
|
||||
|
||||
# Create templates directory
|
||||
templates_dir = Path(temp_dir) / app_name / "templates" / app_name
|
||||
templates_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create a sample template
|
||||
sample_template = templates_dir / "index.html"
|
||||
with open(sample_template, "w") as f:
|
||||
f.write(
|
||||
"""{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ title }}</h1>
|
||||
|
||||
<ul>
|
||||
{% for item in items %}
|
||||
<li>{{ item.name }} - {{ item.description }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% if show_footer %}
|
||||
<footer>
|
||||
<p>© {% now "Y" %} Test Project</p>
|
||||
</footer>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
"""
|
||||
)
|
||||
|
||||
# Create a base template
|
||||
base_template_dir = Path(temp_dir) / "templates"
|
||||
base_template_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
base_template = base_template_dir / "base.html"
|
||||
with open(base_template, "w") as f:
|
||||
f.write(
|
||||
"""<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{% block title %}Test Project{% endblock %}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="{% url 'home' %}">Home</a>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
)
|
||||
|
||||
# Create a views.py file
|
||||
views_path = Path(temp_dir) / app_name / "views.py"
|
||||
with open(views_path, "w") as f:
|
||||
f.write(
|
||||
"""from django.shortcuts import render
|
||||
|
||||
def home(request):
|
||||
context = {
|
||||
'title': 'Welcome to the Test Project',
|
||||
'items': [
|
||||
{'name': 'Item 1', 'description': 'Description 1'},
|
||||
{'name': 'Item 2', 'description': 'Description 2'},
|
||||
{'name': 'Item 3', 'description': 'Description 3'},
|
||||
],
|
||||
'show_footer': True,
|
||||
}
|
||||
return render(request, f'{request.resolver_match.app_name}/index.html', context)
|
||||
"""
|
||||
)
|
||||
|
||||
# Create a urls.py file in the app
|
||||
app_urls_path = Path(temp_dir) / app_name / "urls.py"
|
||||
with open(app_urls_path, "w") as f:
|
||||
f.write(
|
||||
"""from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'testapp'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.home, name='home'),
|
||||
]
|
||||
"""
|
||||
)
|
||||
|
||||
# Update project urls.py
|
||||
project_urls_path = Path(temp_dir) / project_name / "urls.py"
|
||||
with open(project_urls_path, "r") as f:
|
||||
urls_content = f.read()
|
||||
|
||||
urls_content = urls_content.replace(
|
||||
"from django.urls import path",
|
||||
"from django.urls import path, include",
|
||||
)
|
||||
urls_content = urls_content.replace(
|
||||
"urlpatterns = [",
|
||||
f"urlpatterns = [\n path('', include('{app_name}.urls')),",
|
||||
)
|
||||
|
||||
with open(project_urls_path, "w") as f:
|
||||
f.write(urls_content)
|
||||
|
||||
# Update settings to include templates directory
|
||||
with open(settings_path, "r") as f:
|
||||
settings_content = f.read()
|
||||
|
||||
if "'DIRS': []," in settings_content:
|
||||
settings_content = settings_content.replace(
|
||||
"'DIRS': [],",
|
||||
"'DIRS': [BASE_DIR / 'templates'],",
|
||||
)
|
||||
|
||||
with open(settings_path, "w") as f:
|
||||
f.write(settings_content)
|
||||
|
||||
return Path(temp_dir)
|
||||
|
||||
|
||||
def cleanup_django_project(project_dir: Path) -> None:
|
||||
"""
|
||||
Clean up a Django project created for testing.
|
||||
|
||||
Args:
|
||||
project_dir: Path to the Django project
|
||||
"""
|
||||
if project_dir.exists():
|
||||
shutil.rmtree(project_dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage
|
||||
project_dir = create_django_project(django_version="5.0")
|
||||
print(f"Created Django project at: {project_dir}")
|
||||
# Don't clean up when run directly, for manual inspection
|
118
tests/generate_matrix_report.py
Executable file
118
tests/generate_matrix_report.py
Executable file
|
@ -0,0 +1,118 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate a test matrix report for django-language-server.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main():
|
||||
"""Generate a test matrix report."""
|
||||
parser = argparse.ArgumentParser(description="Generate test matrix report")
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
default="test_matrix_report.md",
|
||||
help="Output file path",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Define the test matrix
|
||||
python_versions = ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
django_versions = ["4.2", "5.0", "5.1"]
|
||||
|
||||
# Run a simple test for each combination to check compatibility
|
||||
results = {}
|
||||
|
||||
for py_version in python_versions:
|
||||
results[py_version] = {}
|
||||
for django_version in django_versions:
|
||||
print(f"Testing Python {py_version} with Django {django_version}...")
|
||||
|
||||
# Create a temporary environment
|
||||
env_name = f"py{py_version.replace('.', '')}_django{django_version.replace('.', '')}"
|
||||
|
||||
try:
|
||||
# Check if this combination is supported
|
||||
# This is a simplified check - in a real implementation,
|
||||
# you would run actual tests and check the results
|
||||
supported = is_combination_supported(py_version, django_version)
|
||||
results[py_version][django_version] = supported
|
||||
except Exception as e:
|
||||
print(f"Error testing Python {py_version} with Django {django_version}: {e}")
|
||||
results[py_version][django_version] = False
|
||||
|
||||
# Generate the report
|
||||
generate_report(results, args.output)
|
||||
|
||||
print(f"Report generated at {args.output}")
|
||||
|
||||
|
||||
def is_combination_supported(python_version: str, django_version: str) -> bool:
|
||||
"""
|
||||
Check if a Python and Django version combination is supported.
|
||||
|
||||
This is a simplified check - in a real implementation, you would
|
||||
run actual tests and check the results.
|
||||
|
||||
Args:
|
||||
python_version: Python version (e.g., "3.9")
|
||||
django_version: Django version (e.g., "4.2")
|
||||
|
||||
Returns:
|
||||
True if the combination is supported, False otherwise
|
||||
"""
|
||||
# Django 5.1 requires Python 3.10+
|
||||
if django_version == "5.1" and python_version == "3.9":
|
||||
return False
|
||||
|
||||
# All other combinations are supported
|
||||
return True
|
||||
|
||||
|
||||
def generate_report(results: dict, output_path: str) -> None:
|
||||
"""
|
||||
Generate a test matrix report.
|
||||
|
||||
Args:
|
||||
results: Test results
|
||||
output_path: Output file path
|
||||
"""
|
||||
with open(output_path, "w") as f:
|
||||
f.write("# Django Language Server Test Matrix\n\n")
|
||||
|
||||
f.write("This report shows the compatibility of django-language-server with different Python and Django versions.\n\n")
|
||||
|
||||
f.write("## Test Matrix\n\n")
|
||||
|
||||
# Write the table header
|
||||
f.write("| Python / Django | " + " | ".join(results[list(results.keys())[0]].keys()) + " |\n")
|
||||
f.write("| --- | " + " | ".join(["---"] * len(results[list(results.keys())[0]])) + " |\n")
|
||||
|
||||
# Write the table rows
|
||||
for py_version, django_results in results.items():
|
||||
row = [py_version]
|
||||
for django_version, supported in django_results.items():
|
||||
if supported:
|
||||
row.append("✅")
|
||||
else:
|
||||
row.append("❌")
|
||||
f.write("| " + " | ".join(row) + " |\n")
|
||||
|
||||
f.write("\n")
|
||||
f.write("✅ = Supported, ❌ = Not supported\n\n")
|
||||
|
||||
f.write("## Notes\n\n")
|
||||
f.write("- Django 5.1 requires Python 3.10 or higher\n")
|
||||
f.write("- All tests were run on the latest patch versions of each Python and Django release\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
6
tests/requirements-test.txt
Normal file
6
tests/requirements-test.txt
Normal file
|
@ -0,0 +1,6 @@
|
|||
pytest>=7.4.0
|
||||
pytest-asyncio>=0.21.1
|
||||
pygls>=1.1.0
|
||||
tox>=4.11.0
|
||||
tox-gh-actions>=3.1.3
|
||||
coverage>=7.3.2
|
96
tests/run_tests.py
Executable file
96
tests/run_tests.py
Executable file
|
@ -0,0 +1,96 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to run the end-to-end tests for django-language-server.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the tests."""
|
||||
parser = argparse.ArgumentParser(description="Run django-language-server tests")
|
||||
parser.add_argument(
|
||||
"--python",
|
||||
choices=["3.9", "3.10", "3.11", "3.12", "3.13"],
|
||||
default=None,
|
||||
help="Python version to test with",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--django",
|
||||
choices=["4.2", "5.0", "5.1"],
|
||||
default=None,
|
||||
help="Django version to test with",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--client",
|
||||
choices=["vscode", "neovim"],
|
||||
default=None,
|
||||
help="Client to test with",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--all",
|
||||
action="store_true",
|
||||
help="Run all tests (all Python and Django versions)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"test_path",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="Path to specific test file or directory",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Determine the tox environment
|
||||
if args.all:
|
||||
# Run all environments
|
||||
tox_env = None
|
||||
elif args.python and args.django:
|
||||
# Run specific Python and Django version
|
||||
py_version = args.python.replace(".", "")
|
||||
django_version = args.django.replace(".", "")
|
||||
tox_env = f"py{py_version}-django{django_version}"
|
||||
elif args.python:
|
||||
# Run all Django versions for specific Python version
|
||||
py_version = args.python.replace(".", "")
|
||||
tox_env = f"py{py_version}"
|
||||
elif args.django:
|
||||
# Run all Python versions for specific Django version
|
||||
django_version = args.django.replace(".", "")
|
||||
tox_env = f"django{django_version}"
|
||||
else:
|
||||
# Default to current Python version and Django 5.0
|
||||
tox_env = None
|
||||
|
||||
# Build the tox command
|
||||
tox_cmd = ["tox"]
|
||||
if tox_env:
|
||||
tox_cmd.extend(["-e", tox_env])
|
||||
|
||||
# Add test path if specified
|
||||
if args.test_path:
|
||||
tox_cmd.append("--")
|
||||
tox_cmd.append(args.test_path)
|
||||
|
||||
# Add client tests if specified
|
||||
if args.client:
|
||||
if args.test_path:
|
||||
print("Warning: --client and test_path cannot be used together. Ignoring test_path.")
|
||||
tox_cmd.append("--")
|
||||
tox_cmd.append(f"tests/clients/test_{args.client}.py")
|
||||
|
||||
# Run the tests
|
||||
try:
|
||||
subprocess.run(tox_cmd, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
sys.exit(e.returncode)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
36
tox.ini
Normal file
36
tox.ini
Normal file
|
@ -0,0 +1,36 @@
|
|||
[tox]
|
||||
envlist =
|
||||
py{39,310,311,312,313}-django{42,50,51}
|
||||
isolated_build = True
|
||||
requires =
|
||||
tox>=4.11.0
|
||||
|
||||
[gh-actions]
|
||||
python =
|
||||
3.9: py39
|
||||
3.10: py310
|
||||
3.11: py311
|
||||
3.12: py312
|
||||
3.13: py313
|
||||
|
||||
[testenv]
|
||||
deps =
|
||||
-r tests/requirements-test.txt
|
||||
django42: Django>=4.2,<4.3
|
||||
django50: Django>=5.0,<5.1
|
||||
django51: Django>=5.1,<5.2
|
||||
commands =
|
||||
pytest {posargs:tests/e2e}
|
||||
|
||||
[testenv:lint]
|
||||
deps =
|
||||
ruff>=0.8.2
|
||||
commands =
|
||||
ruff check tests
|
||||
ruff format --check tests
|
||||
|
||||
[pytest]
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_functions = test_*
|
||||
asyncio_mode = auto
|
Loading…
Add table
Add a link
Reference in a new issue