Skip to content

Commit

Permalink
chore(e2e): inline python assertions
Browse files Browse the repository at this point in the history
  • Loading branch information
Desdaemon committed May 20, 2024
1 parent d34fbf9 commit 8eaa51f
Show file tree
Hide file tree
Showing 13 changed files with 162 additions and 67 deletions.
4 changes: 0 additions & 4 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,6 @@ jobs:
sudo apt-get update
sudo apt-get install -y gcc-multilib
- name: Run unit tests
if: matrix.target == 'x86_64-unknown-linux-gnu'
run: cargo test

- name: Build
run: cargo build --release --target ${{ matrix.target }}

Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ jobs:
- name: Run tests
working-directory: testing
run: |
pip install uv
pip install -U uv
uv venv
. ${{ matrix.runs-on == 'windows-latest' && '.venv/Scripts/activate' || '.venv/bin/activate'}}
uv pip compile requirements.in -o requirements.txt
uv pip sync requirements.txt
pytest -vv --junitxml=.results.xml --color=yes
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ dist/*
syntaxes/*.json
.venv
__pycache__
.*_cache
.*_cache
testing/requirements.txt
8 changes: 7 additions & 1 deletion src/analyze.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Methods related to type analysis. The two most important methods are
//! [`Backend::model_of_range`] and [`Backend::type_of`].
use std::{borrow::Borrow, collections::HashMap, iter::FusedIterator, ops::ControlFlow};
use std::{borrow::Borrow, collections::HashMap, fmt::Debug, iter::FusedIterator, ops::ControlFlow};

use tracing::trace;
use tree_sitter::{Node, QueryCursor};
Expand Down Expand Up @@ -63,6 +63,12 @@ pub struct Scope {
pub super_: Option<ImStr>,
}

impl Debug for Scope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("Scope").field(&"..").finish()
}
}

pub fn normalize<'r, 'n>(node: &'r mut Node<'n>) -> &'r mut Node<'n> {
let mut cursor = node.walk();
while matches!(
Expand Down
5 changes: 5 additions & 0 deletions src/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1017,6 +1017,11 @@ impl Backend {
return ControlFlow::Continue(entered);
}

// HACK: fix this issue where the model name is just empty
if interner().resolve(&model_name).is_empty() {
return ControlFlow::Continue(entered);
}

diagnostics.push(Diagnostic {
range: ts_range_to_lsp_range(attribute.range()),
severity: Some(DiagnosticSeverity::ERROR),
Expand Down
4 changes: 3 additions & 1 deletion src/xml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use odoo_lsp::model::{Field, FieldKind};
use odoo_lsp::template::gather_templates;
use ropey::{Rope, RopeSlice};
use tower_lsp::lsp_types::*;
use tracing::{debug, warn};
use tracing::{debug, instrument, warn};
use tree_sitter::Parser;
use xmlparser::{ElementEnd, Error, StrSpan, StreamError, Token, Tokenizer};

Expand Down Expand Up @@ -610,6 +610,7 @@ impl Backend {
}
/// The main function that determines all the information needed
/// to resolve the symbol at the cursor.
#[instrument(level = "trace", skip_all, ret)]
fn gather_refs<'read>(
&self,
offset_at_cursor: ByteOffset,
Expand Down Expand Up @@ -998,6 +999,7 @@ impl Backend {
}
}

#[derive(Debug)]
struct XmlRefs<'a> {
ref_at_cursor: Option<(&'a str, core::ops::Range<usize>)>,
ref_kind: Option<RefKind<'a>>,
Expand Down
8 changes: 8 additions & 0 deletions testing/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
End-to-end testing for odoo-lsp.

```shell
pip install -U setuptools uv
uv venv
source .venv/bin/activate
uv pip compile requirements.in > requirements.txt
uv pip sync requirements.txt
```
2 changes: 1 addition & 1 deletion testing/fixtures/basic/bar/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ class DerivedBar(models.Model):
_inherit = "bar"

def test(self):
self.env["bar"]
pass
19 changes: 18 additions & 1 deletion testing/fixtures/basic/foo/models.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
class Foo(Model):
_name = "foo"

def test(self):
bar = fields.Char()

def completions(self):
self.env["bar"]
# ^complete bar derived.bar foo foob

def diagnostics(self):
self.foo
# ^diag Model `foo` has no field `foo`
self.env["foo"].foo
# ^diag Model `foo` has no field `foo`
self.mapped("foo")
# ^diag Model `foo` has no field `foo`
self.env["fo"]
# ^diag `fo` is not a valid model name


class Foob(Model):
_name = "foob"

foo_id = fields.Many2one("foo")
barb = fields.Char(related='foo_id.bar')
# ^complete bar

class NonModel:
__slots__ = ("foo", "bar")

def __init__(self):
...
6 changes: 6 additions & 0 deletions testing/fixtures/basic/foo/records.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<odoo>
<record id="foo.1" model="foo">
<field name="bar"/>
<!-- ^complete bar -->
</record>
</odoo>
146 changes: 103 additions & 43 deletions testing/fixtures/basic/test_main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import pytest
from collections import defaultdict
from pathlib import Path
from pytest_lsp import LanguageClient
from tree_sitter import Parser, Query, Tree
from deepdiff import DeepDiff # type: ignore
from tree_sitter import Language
import tree_sitter_python as tspython
from lsprotocol.types import (
DidOpenTextDocumentParams,
TextDocumentItem,
Expand All @@ -12,64 +17,119 @@
)


LANG_PY = Language(tspython.language())
QUERY_PY = Query(
LANG_PY,
"""
((comment) @diag
(#match? @diag "\\\\^diag "))
((comment) @complete
(#match? @complete "\\\\^complete "))
""",
)


class Expected:
__slots__ = ("diag", "complete")
diag: list[tuple[Position, str]]
complete: list[tuple[Position, list[str]]]

def __init__(self):
self.diag = []
self.complete = []


@pytest.mark.asyncio(scope="module")
async def test_completions(client: LanguageClient, rootdir: str):
"""Test various basic completions."""
async def test_python(client: LanguageClient, rootdir: str):
files = {file: file.read_text() for file in Path(rootdir).joinpath("foo").rglob("*.py")}
expected = defaultdict[Path, Expected](Expected)
asts = dict[Path, Tree]()
for file, text in files.items():
parser = Parser(LANG_PY)
ast = parser.parse(text.encode())
asts[file] = ast
for node, _ in QUERY_PY.captures(ast.root_node):
assert (text := node.text), "node has no text"
text = text.decode()
if (offset := text.find("^diag ")) != -1:
msg = text[offset + 6 :].strip()
pos = Position(node.start_point.row - 1, node.start_point.column + offset)
expected[file].diag.append((pos, msg))
elif (offset := text.find("^complete ")) != -1:
completions = text[offset + 10 :].strip().split(" ")
pos = Position(node.start_point.row - 1, node.start_point.column + offset)
expected[file].complete.append((pos, completions))

unexpected: list[str] = []
for file, text in files.items():
client.text_document_did_open(
DidOpenTextDocumentParams(
TextDocumentItem(
uri=file.as_uri(),
language_id="python",
version=1,
text=text,
)
)
)
await client.wait_for_notification("textDocument/publishDiagnostics")
actual_diagnostics = list(splay_diag(client.diagnostics[file.as_uri()]))
if diff := DeepDiff(expected[file].diag, actual_diagnostics):
for extra in diff.pop("iterable_item_added", {}).values(): # type: ignore
unexpected.append(f"diag: extra {extra}\n at {file}")
for missing in diff.pop("iterable_item_removed", {}).values(): # type: ignore
unexpected.append(f"diag: missing {missing}\n at {file}")
for mismatch in diff.pop("values_changed", {}).values(): # type: ignore
unexpected.append(f"diag: expected={mismatch['old_value']!r} actual={mismatch['new_value']!r}\n at {file}")
if diff:
unexpected.append(f"diag: unexpected {diff}\n at {file}")

for pos, expected_completion in expected[file].complete:
results = await client.text_document_completion_async(
CompletionParams(
TextDocumentIdentifier(uri=file.as_uri()),
pos,
)
)
assert isinstance(results, CompletionList)
actual = [e.label for e in results.items]
if actual != expected_completion:
node = asts[file].root_node.named_descendant_for_point_range((pos.line, pos.character), (pos.line, 9999))
assert node
if text := node.text:
node_text = text.decode()
else:
node_text = ""
unexpected.append(f"complete: actual={' '.join(actual)}\n at {file}:{pos}\n{' ' * node.start_point.column}{node_text}")
unexpected_len = len(unexpected)
assert not unexpected_len, "\n".join(unexpected)


@pytest.mark.asyncio(scope="module")
async def test_xml_completions(client: LanguageClient, rootdir: str):
client.text_document_did_open(
DidOpenTextDocumentParams(
TextDocumentItem(
uri=f"file://{rootdir}/foo/models.py",
language_id="python",
uri=f"file://{rootdir}/foo/records.xml",
language_id="xml",
version=1,
text=Path(f"{rootdir}/foo/models.py").read_text(),
text=Path(f"{rootdir}/foo/records.xml").read_text(),
)
)
)
await client.wait_for_notification("textDocument/publishDiagnostics")

results = await client.text_document_completion_async(
CompletionParams(
TextDocumentIdentifier(uri=f"file://{rootdir}/foo/models.py"),
Position(4, 18),
TextDocumentIdentifier(uri=f"file://{rootdir}/foo/records.xml"),
Position(2, 17),
)
)
if isinstance(results, CompletionList):
results = results.items
assert results
assert [e.label for e in results] == ["bar", "derived.bar", "foo", "foob"]
assert isinstance(results, CompletionList)
assert len(results.items) == 1
assert [e.label for e in results.items] == ["bar"]


def splay_diag(diags: list[Diagnostic]):
return (
(
d.range.start.line,
d.range.start.character,
d.range.end.line,
d.range.end.character,
d.message,
)
for d in diags
)


@pytest.mark.asyncio(scope="module")
async def test_diagnostics(client: LanguageClient, rootdir: str):
client.text_document_did_open(
DidOpenTextDocumentParams(
TextDocumentItem(
uri=f"file://{rootdir}/foo/models.py",
language_id="python",
version=1,
text=Path(f"{rootdir}/foo/models.py").read_text(),
)
)
)
await client.wait_for_notification("textDocument/publishDiagnostics")
diagnostics = client.diagnostics[Path(f"{rootdir}/foo/models.py").as_uri()]
assert list(splay_diag(diagnostics)) == [
(7, 13, 7, 16, "Model `foo` has no field `foo`"),
(8, 24, 8, 27, "Model `foo` has no field `foo`"),
(9, 21, 9, 24, "Model `foo` has no field `foo`"),
(10, 18, 10, 20, "`fo` is not a valid model name"),
]
return ((d.range.start, d.message) for d in diags)
7 changes: 7 additions & 0 deletions testing/requirements.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
deepdiff==7.0.1
pytest==8.2.0
pytest-lsp==0.4.1
pytest-timeout==2.3.1
tree-sitter==0.22.3
tree-sitter-python @ git+https://github.com/tree-sitter/tree-sitter-python@71778c2a472ed00a64abf4219544edbf8e4b86d7 ; platform_machine == 'arm64' and sys_platform == 'darwin'
tree-sitter-python==0.21.0
14 changes: 0 additions & 14 deletions testing/requirements.txt

This file was deleted.

0 comments on commit 8eaa51f

Please sign in to comment.