Skip to content
Open
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
14 changes: 14 additions & 0 deletions scroll.lua
Original file line number Diff line number Diff line change
Expand Up @@ -653,4 +653,18 @@ 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.
---
--- @return boolean
---
function scroll.animating() end

---
--- Returns true if there are pending transactions that haven't been applied yet.
---
--- @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 @@ -13,6 +13,7 @@
#include "sway/desktop/animation.h"
#include "sway/desktop/launcher.h"
#include "sway/ipc-server.h"
#include "sway/server.h"

#if 0
static void print_table(lua_State *L, int index);
Expand Down Expand Up @@ -1725,7 +1726,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 @@ -1796,8 +1810,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
8 changes: 8 additions & 0 deletions sway/scroll.5.scd
Original file line number Diff line number Diff line change
Expand Up @@ -2402,6 +2402,14 @@ local process_data = scroll.exec_process("kitty")
Removes a callback set earlier using *add_callback*. _id_ is the unique
identifier returned by *add_callback*.

*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
Expand Down
52 changes: 44 additions & 8 deletions sway/tree/workspace.c
Original file line number Diff line number Diff line change
Expand Up @@ -795,8 +795,22 @@ struct workspace_switch_data {
list_t *from_containers;
list_t *to_containers;
struct sway_root_filters *root_filters;
struct wl_listener from_destroy;
struct wl_listener to_destroy;
};

static void workspace_switch_from_destroy(struct wl_listener *listener, void *data) {
struct workspace_switch_data *ws_data = wl_container_of(listener, ws_data, from_destroy);
ws_data->from = NULL;
wl_list_remove(&listener->link);
}

static void workspace_switch_to_destroy(struct wl_listener *listener, void *data) {
struct workspace_switch_data *ws_data = wl_container_of(listener, ws_data, to_destroy);
ws_data->to = NULL;
wl_list_remove(&listener->link);
}

static void workspace_switch_callback_end(void *callback_data) {
struct workspace_switch_data *data = callback_data;
root_filters_destroy(root, data->root_filters);
Expand All @@ -815,10 +829,17 @@ static void workspace_switch_callback_end(void *callback_data) {
if (data->from && data->from->output) {
node_set_dirty(&data->from->node);
}
if (data->to->output) {
if (data->to && data->to->output) {
node_set_dirty(&data->to->node);
}

if (data->from_destroy.link.next) {
wl_list_remove(&data->from_destroy.link);
}
if (data->to_destroy.link.next) {
wl_list_remove(&data->to_destroy.link);
}

list_free_items_and_destroy(data->from_containers);
list_free_items_and_destroy(data->to_containers);
free(data);
Expand All @@ -840,7 +861,7 @@ static bool switching_output(struct sway_workspace *workspace,
}
struct sway_output *output = workspace->output;
struct sway_output *from_output = data->from ? data->from->output : NULL;
struct sway_output *to_output = data->to->output;
struct sway_output *to_output = data->to ? data->to->output : NULL;
if (!output || !from_output || !to_output) {
return false;
}
Expand Down Expand Up @@ -1028,6 +1049,20 @@ static void animate_workspace_switch(struct sway_output *output,
data->from_containers = create_list();
data->to_containers = create_list();

data->from_destroy.notify = workspace_switch_from_destroy;
if (data->from) {
wl_signal_add(&data->from->node.events.destroy, &data->from_destroy);
} else {
wl_list_init(&data->from_destroy.link);
}

data->to_destroy.notify = workspace_switch_to_destroy;
if (data->to) {
wl_signal_add(&data->to->node.events.destroy, &data->to_destroy);
} else {
wl_list_init(&data->to_destroy.link);
}

animation_end();
animation_set_type(ANIMATION_WORKSPACE_SWITCH);

Expand Down Expand Up @@ -1102,12 +1137,13 @@ bool workspace_switch(struct sway_workspace *workspace) {
seat_set_focus(seat, next);

// old_ws may not have an output because it is being destroyed if empty
if (!layout_overview_workspaces_enabled() && old_ws != workspace && (
(old_ws->output && old_ws->output == workspace->output) ||
(old_ws->output && !workspace->output) ||
(!old_ws->output && workspace->output)) &&
workspace->split.sibling != old_ws &&
animation_enabled() && animation_path_enabled(ANIMATION_WORKSPACE_SWITCH)) {
if (!layout_overview_workspaces_enabled() && old_ws != workspace &&
((old_ws->output && old_ws->output == workspace->output) ||
(old_ws->output && !workspace->output) ||
(!old_ws->output && workspace->output)) &&
workspace->split.sibling != old_ws && animation_enabled() &&
animation_path_enabled(ANIMATION_WORKSPACE_SWITCH) && !workspace->node.destroying &&
!old_ws->node.destroying) {
struct sway_output *output = old_ws->output ? old_ws->output : workspace->output;
animate_workspace_switch(output, old_ws, workspace);
} else {
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