Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pytest]
addopts = -n 4
23 changes: 23 additions & 0 deletions scroll.lua
Original file line number Diff line number Diff line change
Expand Up @@ -653,4 +653,27 @@ function scroll.add_callback(event, cb_func, cb_data) end
--- @return integer
function scroll.remove_callback(id) end

---
--- Returns true if there is an active animation running.
---
--- This is primarily intended for use via the IPC interface for testing and
--- debugging. It is generally not useful when called synchronously within a
--- Lua script, as animations only start after the script execution completes.
---
--- @return boolean
---
function scroll.animating() end

---
--- Returns true if there are pending transactions that haven't been applied yet.
---
--- This is primarily intended for use via the IPC interface for testing and
--- debugging. It is generally not useful when called synchronously within a
--- Lua script, as transactions are not processed while the script is running
--- and blocking the main thread.
---
--- @return boolean
---
function scroll.pending_transactions() end

return scroll
17 changes: 17 additions & 0 deletions sway/lua.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "sway/desktop/launcher.h"
#include "sway/ipc-server.h"
#include "sway/desktop/transaction.h"
#include "sway/server.h"

#if 0
static void print_table(lua_State *L, int index);
Expand Down Expand Up @@ -1727,7 +1728,20 @@ static int scroll_remove_callback(lua_State *L) {
return 0;
}

static int scroll_animating(lua_State *L) {
lua_pushboolean(L, animation_animating());
return 1;
}

static int scroll_pending_transactions(lua_State *L) {
bool pending = server.queued_transaction != NULL || server.pending_transaction != NULL ||
server.dirty_nodes->length > 0;
lua_pushboolean(L, pending);
return 1;
}

// Module functions
/* clang-format off */
static luaL_Reg const scroll_lib[] = {
{ "log", scroll_log },
{ "state_set_value", scroll_state_set_value },
Expand Down Expand Up @@ -1798,8 +1812,11 @@ static luaL_Reg const scroll_lib[] = {
{ "scratchpad_hide", scroll_scratchpad_hide },
{ "add_callback", scroll_add_callback },
{ "remove_callback", scroll_remove_callback },
{ "animating", scroll_animating },
{ "pending_transactions", scroll_pending_transactions },
{ NULL, NULL }
};
/* clang-format on */

// Module Loader
int luaopen_scroll(lua_State *L) {
Expand Down
22 changes: 21 additions & 1 deletion sway/scroll.5.scd
Original file line number Diff line number Diff line change
Expand Up @@ -2402,7 +2402,27 @@ local process_data = scroll.exec_process("kitty")
Removes a callback set earlier using *add_callback*. _id_ is the unique
identifier returned by *add_callback*.

Examples:
## TESTING AND DEBUGGING API

These functions are primarily intended for use via the IPC interface (see
*scroll-ipc*(7)) for testing and debugging purposes, and are generally not
useful when called synchronously from standard Lua scripts or event callbacks.

Because scroll's Lua integration executes scripts synchronously on the main
compositor thread, the event loop is blocked during execution. This means that
any compositor actions triggered by the script (such as applying window layouts
or starting animations) will only occur after the script completes.
Consequently, calling these functions synchronously within a script will not
reflect the state changes of commands run immediately before them.

*animating()*
Returns _true_ if there is an active animation running.

*pending_transactions()*
Returns _true_ if there are pending transactions that haven't been applied
yet.

## EXAMPLES

Calling this script from the configuration file, you will get focus on every
window that gets the *urgent* attribute:
Expand Down
172 changes: 137 additions & 35 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,69 +3,171 @@
import pytest
from typing import Generator
from pathlib import Path
from test_utils import ScrollInstance, run_compositor
from test_utils import ScrollInstance, run_compositor, ScrollCompositorFactory


def pytest_addoption(parser: pytest.Parser) -> None:
parser.addoption("--scroll", help="the scroll binary to test", default=None)


@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(
item: pytest.Item, call: pytest.CallInfo
) -> Generator[None, None, None]:
outcome = yield
rep = outcome.get_result()
setattr(item, "result_" + rep.when, rep)


def _build_scroll() -> str:
# Auto-build using Meson/Ninja
print("\nBuilding scroll with Meson/Ninja...")
build_dir = os.path.abspath("./build")
if not os.path.exists(build_dir):
res = subprocess.run(
[
"meson",
"setup",
"build",
"-Dwerror=false",
"-Db_sanitize=address",
"-Dbuildtype=debugoptimized",
],
capture_output=True,
text=True,
)
if res.returncode != 0:
pytest.exit(
f"Failed to setup build:\nStdout: {res.stdout}\nStderr: {res.stderr}"
)
else:
# Ensure ASan is enabled
res = subprocess.run(
[
"meson",
"configure",
"build",
"-Db_sanitize=address",
"-Dbuildtype=debugoptimized",
],
capture_output=True,
text=True,
)
if res.returncode != 0:
pytest.exit(
"Failed to configure build with ASan:\nStdout:"
f" {res.stdout}\nStderr: {res.stderr}"
)

# Run ninja to compile (incremental build)
res = subprocess.run(["ninja", "-C", "build"], capture_output=True, text=True)
if res.returncode != 0:
pytest.exit(
f"Failed to build scroll:\nStdout: {res.stdout}\nStderr: {res.stderr}"
)

return os.path.join(build_dir, "sway", "scroll")


@pytest.fixture(scope="session")
def scroll_compositor_binary(request: pytest.FixtureRequest) -> str:
binary_path: str = request.config.getoption("scroll")
if not binary_path:
# Auto-build using Meson/Ninja
print("\nBuilding scroll with Meson/Ninja...")
build_dir = os.path.abspath("./build")
if not os.path.exists(build_dir):
res = subprocess.run(
["meson", "setup", "build", "-Dwerror=false", "-Db_sanitize=address"],
capture_output=True,
text=True,
)
if res.returncode != 0:
pytest.exit(
f"Failed to setup build:\nStdout: {res.stdout}\nStderr: {res.stderr}"
)
# Check if we are running under xdist
try:
worker_id = request.getfixturevalue("worker_id")
except Exception:
worker_id = "master"

if worker_id == "master":
binary_path = _build_scroll()
else:
# Ensure ASan is enabled
res = subprocess.run(
["meson", "configure", "build", "-Db_sanitize=address"],
capture_output=True,
text=True,
)
if res.returncode != 0:
pytest.exit(
f"Failed to configure build with ASan:\nStdout: {res.stdout}\nStderr: {res.stderr}"
)
tmp_path_factory = request.getfixturevalue("tmp_path_factory")
shared_dir = tmp_path_factory.getbasetemp().parent
lock_path = shared_dir / "scroll_build.lock"
status_path = shared_dir / "scroll_build.status"

# Run ninja to compile (incremental build)
res = subprocess.run(["ninja", "-C", "build"], capture_output=True, text=True)
if res.returncode != 0:
pytest.exit(
f"Failed to build scroll:\nStdout: {res.stdout}\nStderr: {res.stderr}"
)
import fcntl

binary_path = os.path.join(build_dir, "sway", "scroll")
# Open with 'a' to avoid truncating while another process might have it locked
with open(lock_path, "a") as lock_file:
fcntl.flock(lock_file, fcntl.LOCK_EX)
try:
if status_path.exists():
binary_path = status_path.read_text().strip()
else:
binary_path = _build_scroll()
status_path.write_text(binary_path)
finally:
fcntl.flock(lock_file, fcntl.LOCK_UN)
else:
binary_path = os.path.abspath(binary_path)

assert os.path.exists(binary_path), f"Binary not found at {binary_path}"

# Set up PATH to include build directories so that the compositor can find
# our newly built scrollbar, swaymsg, swaynag, etc.
build_dir = Path(binary_path).parent.parent
old_path = os.environ.get("PATH", "")
build_paths = [
str(build_dir / "sway"),
str(build_dir / "swaymsg"),
str(build_dir / "swaybar"),
str(build_dir / "swaynag"),
]
os.environ["PATH"] = ":".join(build_paths) + ":" + old_path

return binary_path


@pytest.fixture(scope="session")
def scroll_compositor(
def scroll_compositor_factory(
scroll_compositor_binary: str, tmp_path_factory: pytest.TempPathFactory
) -> Generator[ScrollInstance, None, None]:
) -> Generator[ScrollCompositorFactory, None, None]:
temp_dir: Path = tmp_path_factory.mktemp("scroll")
with run_compositor(scroll_compositor_binary, temp_dir) as inst:
yield ScrollCompositorFactory(inst)


@pytest.fixture(scope="function")
def scroll_compositor(
request: pytest.FixtureRequest,
scroll_compositor_factory: ScrollCompositorFactory,
) -> Generator[ScrollInstance, None, None]:
ctx = scroll_compositor_factory()
inst = ctx.__enter__()
try:
yield inst
finally:
failed = (
hasattr(request.node, "result_call") and request.node.result_call.failed
)
if failed:
scroll_compositor_factory.print_log()
ctx.__exit__(None, None, None)


@pytest.fixture(scope="function")
def fresh_compositor(
def fresh_compositor_factory(
scroll_compositor_binary: str, tmp_path: Path
) -> Generator[ScrollInstance, None, None]:
) -> Generator[ScrollCompositorFactory, None, None]:
with run_compositor(scroll_compositor_binary, tmp_path) as inst:
yield ScrollCompositorFactory(inst)


@pytest.fixture(scope="function")
def fresh_compositor(
request: pytest.FixtureRequest,
fresh_compositor_factory: ScrollCompositorFactory,
) -> Generator[ScrollInstance, None, None]:
ctx = fresh_compositor_factory()
inst = ctx.__enter__()
try:
yield inst
finally:
failed = (
hasattr(request.node, "result_call") and request.node.result_call.failed
)
if failed:
fresh_compositor_factory.print_log()
ctx.__exit__(None, None, None)
4 changes: 2 additions & 2 deletions tests/interactive.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ if [ "$has_config" = "false" ]; then
set -- -c "$CONFIG" "$@"
fi

# Enable leak detection but use suppressions for known system library leaks.
export ASAN_OPTIONS="detect_leaks=1${ASAN_OPTIONS:+:$ASAN_OPTIONS}"
# Disable leak detection to suppress leak errors in the interactive test.
export ASAN_OPTIONS="detect_leaks=0${ASAN_OPTIONS:+:$ASAN_OPTIONS}"
export LSAN_OPTIONS="suppressions=$PROJECT_ROOT/tests/lsan.supp${LSAN_OPTIONS:+:$LSAN_OPTIONS}"

echo "Starting scroll..."
Expand Down
3 changes: 3 additions & 0 deletions tests/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ if xcb_dep.found()
install: false,
)
endif



Loading