182 lines
7.2 KiB
Python
182 lines
7.2 KiB
Python
"""The symbol server answers from a plain arduino-cli sketchbook too.
|
|
|
|
velxio.dev keeps its libraries in a content-addressed cache, where an entry
|
|
dir name IS the content hash. Velxio Desktop's sidecar has no such cache: its
|
|
libraries are whatever `arduino-cli lib install` put in the sketchbook, under
|
|
plain folder names, and they get rewritten in place on upgrade. Without this
|
|
path the desktop editor gets the engine's embedded catalog and nothing else —
|
|
`client.` on a library the user installed himself resolves to nothing.
|
|
|
|
The routes are called directly (no TestClient) so the suite keeps needing
|
|
nothing but pytest.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
_REPO = Path(__file__).resolve().parents[3]
|
|
sys.path.insert(0, str(_REPO / "backend"))
|
|
|
|
from app.api.routes import intellisense # noqa: E402
|
|
|
|
HEADER = """
|
|
class Widget : public Print {
|
|
public:
|
|
Widget(int pin);
|
|
void begin(void);
|
|
bool ready();
|
|
private:
|
|
int _pin;
|
|
};
|
|
"""
|
|
|
|
|
|
def _lib(libraries: Path, folder: str, name: str, version: str, header: str = HEADER) -> Path:
|
|
d = libraries / folder
|
|
(d / "src").mkdir(parents=True, exist_ok=True)
|
|
(d / "library.properties").write_text(
|
|
f"name={name}\nversion={version}\narchitectures=*\n", encoding="utf-8"
|
|
)
|
|
(d / "src" / f"{folder}.h").write_text(header, encoding="utf-8")
|
|
return d
|
|
|
|
|
|
@pytest.fixture
|
|
def sketchbook(tmp_path, monkeypatch):
|
|
"""A sketchbook with no content-addressed cache in sight. Both env vars are
|
|
set so the resolver never reaches arduino-cli or the home-dir guesses —
|
|
a unit test must not depend on what is installed on the machine."""
|
|
libraries = tmp_path / "sketchbook" / "libraries"
|
|
libraries.mkdir(parents=True)
|
|
monkeypatch.setenv("VELXIO_LIBCACHE_DIR", str(tmp_path / "no-cache-here"))
|
|
monkeypatch.setenv("VELXIO_SYMBOL_LIBRARIES_DIR", str(libraries))
|
|
return libraries
|
|
|
|
|
|
def _body(response) -> dict:
|
|
return json.loads(response.body)
|
|
|
|
|
|
def test_a_sketchbook_library_answers_by_display_name(sketchbook):
|
|
_lib(sketchbook, "MyWidget", "My Widget Library", "1.4.0")
|
|
|
|
payload = _body(intellisense.get_symbols("My Widget Library"))
|
|
|
|
assert payload["id"] == "My Widget Library"
|
|
assert "MyWidget.h" in payload["triggers"]
|
|
assert {"begin", "ready"} <= {s["name"] for s in payload["symbols"]}
|
|
|
|
|
|
def test_the_installed_version_answers_a_pin_for_another_one(sketchbook):
|
|
"""The project pins what velxio.dev cached; the desktop compiles against
|
|
what is on disk. Answering with the installed version beats a 404."""
|
|
_lib(sketchbook, "MyWidget", "My Widget Library", "1.4.0")
|
|
|
|
exact = _body(intellisense.get_symbols("My Widget Library@1.4.0"))
|
|
other = _body(intellisense.get_symbols("My Widget Library@9.9.9"))
|
|
|
|
assert exact["id"] == other["id"] == "My Widget Library"
|
|
|
|
|
|
def test_an_uninstalled_library_is_404(sketchbook):
|
|
_lib(sketchbook, "MyWidget", "My Widget Library", "1.4.0")
|
|
|
|
with pytest.raises(HTTPException) as err:
|
|
intellisense.get_symbols("Nothing Like This")
|
|
assert err.value.status_code == 404
|
|
|
|
|
|
def test_type_lookup_resolves_a_class_from_the_sketchbook(sketchbook):
|
|
_lib(sketchbook, "MyWidget", "My Widget Library", "1.4.0")
|
|
|
|
payload = _body(intellisense.get_type_members("Widget"))
|
|
|
|
assert payload["type"] == "Widget"
|
|
assert {"begin", "ready"} <= {s["name"] for s in payload["symbols"]}
|
|
# Print is not an installed library — reported as an unresolved base so the
|
|
# editor can supply its own members for it.
|
|
assert "Print" in payload["bases"]
|
|
|
|
|
|
def test_scans_are_cached_beside_libraries_not_inside_them(sketchbook):
|
|
"""arduino-cli scans `libraries/` for libraries. Nothing we write may land
|
|
there, or a stray dir shows up in `lib list`."""
|
|
_lib(sketchbook, "MyWidget", "My Widget Library", "1.4.0")
|
|
|
|
intellisense.get_symbols("My Widget Library")
|
|
|
|
assert (sketchbook.parent / ".velxio-symbols").is_dir()
|
|
assert [d.name for d in sketchbook.iterdir()] == ["MyWidget"]
|
|
|
|
|
|
def test_upgrading_a_library_in_place_invalidates_its_cached_scan(sketchbook):
|
|
"""A sketchbook folder keeps its name across `lib upgrade`, so the scan
|
|
cannot be keyed by name — it is keyed by a content stamp."""
|
|
lib = _lib(sketchbook, "MyWidget", "My Widget Library", "1.4.0")
|
|
first = _body(intellisense.get_symbols("My Widget Library"))
|
|
assert "shutdown" not in {s["name"] for s in first["symbols"]}
|
|
|
|
(lib / "src" / "MyWidget.h").write_text(
|
|
HEADER.replace("bool ready();", "bool ready();\n void shutdown();"), encoding="utf-8"
|
|
)
|
|
(lib / "library.properties").write_text(
|
|
"name=My Widget Library\nversion=1.5.0\narchitectures=*\n", encoding="utf-8"
|
|
)
|
|
|
|
second = _body(intellisense.get_symbols("My Widget Library"))
|
|
assert "shutdown" in {s["name"] for s in second["symbols"]}
|
|
|
|
|
|
def test_the_content_addressed_cache_wins_over_the_sketchbook(tmp_path, monkeypatch):
|
|
"""On velxio.dev both exist (the pro overlay points arduino-cli's
|
|
sketchbook at the cache). The immutable one answers."""
|
|
cache = tmp_path / "libcache"
|
|
cache.mkdir()
|
|
_lib(cache, "mywidgetlibrary@2.0.0-abcdef123456", "My Widget Library", "2.0.0")
|
|
libraries = tmp_path / "sketchbook" / "libraries"
|
|
libraries.mkdir(parents=True)
|
|
_lib(libraries, "MyWidget", "My Widget Library", "1.4.0")
|
|
monkeypatch.setenv("VELXIO_LIBCACHE_DIR", str(cache))
|
|
monkeypatch.setenv("VELXIO_SYMBOL_LIBRARIES_DIR", str(libraries))
|
|
|
|
payload = _body(intellisense.get_symbols("My Widget Library"))
|
|
|
|
# The cache entry's scan is keyed by its dir name; the sketchbook's is not.
|
|
assert (cache / ".symbols" / "mywidgetlibrary@2.0.0-abcdef123456.json").is_file()
|
|
assert not (libraries.parent / ".velxio-symbols").exists()
|
|
assert payload["id"] == "My Widget Library"
|
|
|
|
|
|
def test_a_sketchbook_that_resolves_to_the_cache_is_not_scanned_twice(tmp_path, monkeypatch):
|
|
"""The pro overlay's fallback sketchbook is a symlink to the cache root."""
|
|
cache = tmp_path / "libcache"
|
|
cache.mkdir()
|
|
_lib(cache, "mywidgetlibrary@2.0.0-abcdef123456", "My Widget Library", "2.0.0")
|
|
sketchbook = tmp_path / "cache-sketchbook"
|
|
sketchbook.mkdir()
|
|
(sketchbook / "libraries").symlink_to(cache, target_is_directory=True)
|
|
monkeypatch.setenv("VELXIO_LIBCACHE_DIR", str(cache))
|
|
monkeypatch.delenv("VELXIO_SYMBOL_LIBRARIES_DIR", raising=False)
|
|
monkeypatch.setenv("VELXIO_FALLBACK_SKETCHBOOK", str(sketchbook))
|
|
|
|
assert [s.content_addressed for s in intellisense._sources()] == [True]
|
|
|
|
|
|
def test_no_library_source_at_all_is_404(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("VELXIO_LIBCACHE_DIR", str(tmp_path / "nope"))
|
|
monkeypatch.setenv("VELXIO_SYMBOL_LIBRARIES_DIR", str(tmp_path / "also-nope"))
|
|
monkeypatch.delenv("ARDUINO_DIRECTORIES_USER", raising=False)
|
|
monkeypatch.delenv("VELXIO_FALLBACK_SKETCHBOOK", raising=False)
|
|
# Never probe arduino-cli or the home dir from a unit test.
|
|
monkeypatch.setattr(intellisense, "_cli_sketchbook", lambda: None)
|
|
monkeypatch.setattr(intellisense.Path, "home", staticmethod(lambda: tmp_path / "empty-home"))
|
|
|
|
with pytest.raises(HTTPException) as err:
|
|
intellisense.get_symbols("Anything")
|
|
assert err.value.status_code == 404
|