Skip to content

Commit

Permalink
lsp: Initial pytest-lsp integration, live preview test case
Browse files Browse the repository at this point in the history
  • Loading branch information
alcarney committed Sep 9, 2023
1 parent 717aec1 commit 22d347a
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 70 deletions.
169 changes: 128 additions & 41 deletions lib/esbonio/tests/sphinx-agent/test_sa_build.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,36 @@
import logging
import pathlib
import re
import sys

import pytest
import pytest_asyncio
import pytest_lsp
from lsprotocol.types import WorkspaceFolder
from pygls import IS_WIN
from pygls.workspace import Workspace
from pytest_lsp import ClientServerConfig

from esbonio.server import Uri
from esbonio.server.features.sphinx_manager.client_subprocess import (
SubprocessSphinxClient,
)
from esbonio.server.features.sphinx_manager.client_subprocess import (
make_test_sphinx_client,
)
from esbonio.server.features.sphinx_manager.config import SphinxConfig


@pytest_asyncio.fixture(scope="module")
async def sphinx_client(uri_for, tmp_path_factory):
# TODO: Integrate with `pytest_lsp`
logger = logging.getLogger("sphinx_client")
logger.setLevel(logging.INFO)

client = SubprocessSphinxClient()

@client.feature("window/logMessage")
def _(params):
logger.info("%s", params.message)

@pytest_lsp.fixture(
scope="module",
params=["html", "dirhtml"],
config=ClientServerConfig(
server_command=[sys.executable, "-m", "esbonio.sphinx_agent"],
client_factory=make_test_sphinx_client,
),
)
async def client(
request, sphinx_client: SubprocessSphinxClient, uri_for, tmp_path_factory
):
build_dir = tmp_path_factory.mktemp("build")
test_uri = uri_for("sphinx-default", "workspace", "index.rst")
sd_workspace = uri_for("sphinx-default", "workspace")
Expand All @@ -36,45 +42,126 @@ def _(params):
],
)
config = SphinxConfig(
build_command=["sphinx-build", "-M", "html", ".", str(build_dir)],
python_command=[sys.executable],
env_passthrough=["PYTHONPATH"],
build_command=[
"sphinx-build",
"-M",
request.param,
sd_workspace.fs_path,
str(build_dir),
],
)
resolved = config.resolve(test_uri, workspace, client.logger)
resolved = config.resolve(test_uri, workspace, sphinx_client.logger)
assert resolved is not None

info = await client.create_application(resolved)
info = await sphinx_client.create_application(resolved)
assert info is not None

yield client
yield


@pytest.mark.asyncio
async def test_build_includes_webview_js(client: SubprocessSphinxClient, uri_for):
"""Ensure that builds include the ``webview.js`` script."""

out = client.build_uri
src = client.src_uri
assert out is not None and src is not None

if not client.stopped:
await client.stop()
await client.build()

# Ensure the script is included in the build output
webview_js = pathlib.Path(out / "_static" / "webview.js")
assert webview_js.exists()
assert "editor/scroll" in webview_js.read_text()

# Ensure the script is included in the page
index_html = pathlib.Path(out / "index.html")
pattern = re.compile(r'<script src="_static/webview.js(\?v=[\w]+)?"></script>')
assert pattern.search(index_html.read_text()) is not None


@pytest.mark.asyncio
async def test_build_file_map(sphinx_client, uri_for):
async def test_build_file_map(client: SubprocessSphinxClient, uri_for):
"""Ensure that we can trigger a Sphinx build correctly and the returned
build_file_map is correct."""

src = sphinx_client.src_uri
src = client.src_uri
assert src is not None

build_file_map = {
(src / "index.rst").fs_path: "index.html",
(src / "definitions.rst").fs_path: "definitions.html",
(src / "directive_options.rst").fs_path: "directive_options.html",
(src / "glossary.rst").fs_path: "glossary.html",
(src / "math.rst").fs_path: "theorems/pythagoras.html",
(src / "code" / "cpp.rst").fs_path: "code/cpp.html",
(src / "theorems" / "index.rst").fs_path: "theorems/index.html",
(src / "theorems" / "pythagoras.rst").fs_path: "theorems/pythagoras.html",
# It looks like non-existant files show up in this mapping as well
(src / ".." / "badfile.rst").fs_path: "definitions.html",
}

result = await sphinx_client.build()
assert result.build_file_map == build_file_map

build_uri_map = {Uri.for_file(src): out for src, out in build_file_map.items()}
assert sphinx_client.build_file_map == build_uri_map
if client.builder == "dirhtml":
build_file_map = {
(src / "index.rst").fs_path: "",
(src / "definitions.rst").fs_path: "definitions/",
(src / "directive_options.rst").fs_path: "directive_options/",
(src / "glossary.rst").fs_path: "glossary/",
(src / "math.rst").fs_path: "theorems/pythagoras/",
(src / "code" / "cpp.rst").fs_path: "code/cpp/",
(src / "theorems" / "index.rst").fs_path: "theorems/",
(src / "theorems" / "pythagoras.rst").fs_path: "theorems/pythagoras/",
# It looks like non-existant files show up in this mapping as well
(src / ".." / "badfile.rst").fs_path: "definitions/",
}
else:
build_file_map = {
(src / "index.rst").fs_path: "index.html",
(src / "definitions.rst").fs_path: "definitions.html",
(src / "directive_options.rst").fs_path: "directive_options.html",
(src / "glossary.rst").fs_path: "glossary.html",
(src / "math.rst").fs_path: "theorems/pythagoras.html",
(src / "code" / "cpp.rst").fs_path: "code/cpp.html",
(src / "theorems" / "index.rst").fs_path: "theorems/index.html",
(src / "theorems" / "pythagoras.rst").fs_path: "theorems/pythagoras.html",
# It looks like non-existant files show up in this mapping as well
(src / ".." / "badfile.rst").fs_path: "definitions.html",
}

result = await client.build()

# Paths are case insensitive on windows.
if IS_WIN:
actual_map = {f.lower(): uri for f, uri in result.build_file_map.items()}
expected_map = {f.lower(): uri for f, uri in build_file_map.items()}
assert actual_map == expected_map

actual_uri_map = {
Uri.parse(str(src).lower()): out
for src, out in client.build_file_map.items()
}
expected_uri_map = {
Uri.for_file(src.lower()): out for src, out in build_file_map.items()
}
assert actual_uri_map == expected_uri_map

else:
assert result.build_file_map == build_file_map

build_uri_map = {Uri.for_file(src): out for src, out in build_file_map.items()}
assert client.build_file_map == build_uri_map


@pytest.mark.asyncio
async def test_build_content_override(client: SubprocessSphinxClient, uri_for):
"""Ensure that we can override the contents of a given src file when
required."""

out = client.build_uri
src = client.src_uri
assert out is not None and src is not None

await client.build()

# Before
expected = "Welcome to the documentation!"
index_html = pathlib.Path(out / "index.html")
assert expected in index_html.read_text()

await client.build(
content_overrides={
(src / "index.rst").fs_path: "My Custom Title\n==============="
}
)

# Ensure the override was applied
expected = "My Custom Title"
print(index_html.read_text())
assert expected in index_html.read_text()
55 changes: 28 additions & 27 deletions lib/esbonio/tests/sphinx-agent/test_sa_create_app.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,35 @@
import asyncio
import logging
import sys

import pytest
import pytest_lsp
from lsprotocol.types import WorkspaceFolder
from pygls import IS_WIN
from pygls.workspace import Workspace
from pytest_lsp import ClientServerConfig

from esbonio.server.features.sphinx_manager.client_subprocess import (
SubprocessSphinxClient,
)
from esbonio.server.features.sphinx_manager.client_subprocess import (
make_test_sphinx_client,
)
from esbonio.server.features.sphinx_manager.config import SphinxConfig


@pytest.fixture()
def sphinx_client():
# TODO: Integrate with `pytest_lsp`
logger = logging.getLogger("sphinx_client")
logger.setLevel(logging.INFO)

client = SubprocessSphinxClient()

@client.feature("window/logMessage")
def _(params):
logger.info("%s", params.message)

return client
@pytest_lsp.fixture(
config=ClientServerConfig(
server_command=[sys.executable, "-m", "esbonio.sphinx_agent"],
client_factory=make_test_sphinx_client,
)
)
async def client(sphinx_client: SubprocessSphinxClient):
yield


@pytest.mark.asyncio
async def test_create_application(sphinx_client, uri_for):
async def test_create_application(client: SubprocessSphinxClient, uri_for):
"""Ensure that we can create a Sphinx application instance correctly."""

test_uri = uri_for("sphinx-default", "workspace", "index.rst")
Expand All @@ -41,21 +43,20 @@ async def test_create_application(sphinx_client, uri_for):
WorkspaceFolder(uri=str(sd_workspace), name="sphinx-default"),
],
)
config = SphinxConfig(
python_command=[sys.executable],
env_passthrough=["PYTHONPATH"],
)
resolved = config.resolve(test_uri, workspace, sphinx_client.logger)
config = SphinxConfig()
resolved = config.resolve(test_uri, workspace, client.logger)
assert resolved is not None

try:
info = await sphinx_client.create_application(resolved)
assert info is not None
info = await client.create_application(resolved)
assert info is not None
assert info.builder_name == "dirhtml"

assert info.builder_name == "dirhtml"
# Paths are case insensitive on Windows
if IS_WIN:
assert info.src_dir.lower() == sd_workspace.fs_path.lower()
assert info.conf_dir.lower() == sd_workspace.fs_path.lower()
assert "cache" in info.build_dir.lower()
else:
assert info.src_dir == sd_workspace.fs_path
assert info.conf_dir == sd_workspace.fs_path
assert "cache" in info.build_dir.lower()
finally:
if not sphinx_client.stopped:
await sphinx_client.stop()
assert "cache" in info.build_dir
4 changes: 4 additions & 0 deletions lib/esbonio/tests/sphinx-agent/test_sa_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ def application_args(**kwargs) -> Dict[str, Any]:
"warningiserror": False,
}

for arg in {"srcdir", "outdir", "confdir", "doctreedir"}:
if arg in kwargs:
kwargs[arg] = str(pathlib.Path(kwargs[arg]).resolve())

# Order matters, kwargs will override any keys found in defaults.
return {**defaults, **kwargs}

Expand Down
4 changes: 2 additions & 2 deletions lib/esbonio/tests/sphinx-default/workspace/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
.. _welcome:

Welcome to Defaults's documentation!
====================================
Welcome to the documentation!
=============================

.. toctree::
:maxdepth: 2
Expand Down

0 comments on commit 22d347a

Please sign in to comment.