mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-09-07 02:40:38 +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!
|
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
|
## License
|
||||||
|
|
||||||
django-language-server is licensed under the Apache License, Version 2.0. See the [`LICENSE`](LICENSE) file for more information.
|
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 = [
|
docs = [
|
||||||
"mkdocs-material>=9.5.49",
|
"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]
|
[project]
|
||||||
name = "django-language-server"
|
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