From 7e8472e1e2a0b39746ef86c041d56b92bab5a6b7 Mon Sep 17 00:00:00 2001 From: "Christopher R. Nelson" Date: Tue, 21 Jul 2026 11:18:56 -0400 Subject: Wire up cache, file watcher, and Unix socket daemon Adds a gen_server cache holding the rendered menu, an fs/inotify-based watcher that debounces changes and invalidates it, a Unix domain socket serving the cached binary, and the escript client Openbox invokes (with an inline render fallback if the daemon isn't running). Also documents local deployment (systemd user unit, Openbox menu.xml wiring) in the README. Co-Authored-By: Claude Sonnet 5 --- test/er_xdg_pipe_menu_cache_tests.erl | 130 ++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 test/er_xdg_pipe_menu_cache_tests.erl (limited to 'test/er_xdg_pipe_menu_cache_tests.erl') diff --git a/test/er_xdg_pipe_menu_cache_tests.erl b/test/er_xdg_pipe_menu_cache_tests.erl new file mode 100644 index 0000000..a1d1440 --- /dev/null +++ b/test/er_xdg_pipe_menu_cache_tests.erl @@ -0,0 +1,130 @@ +-module(er_xdg_pipe_menu_cache_tests). + +-include_lib("eunit/include/eunit.hrl"). + +build_renders_entries_from_dir_test() -> + Dir = mk_tmp_dir(), + write_desktop(Dir, "foo.desktop", "Foo", "foo"), + Menu = er_xdg_pipe_menu_cache:build([Dir]), + ok = rm_rf(Dir), + ?assert(binary:match(Menu, <<"Foo">>) =/= nomatch). + +build_groups_by_category_test() -> + Dir = mk_tmp_dir(), + write_desktop(Dir, "foo.desktop", "Foo", "foo", "Utility"), + Menu = er_xdg_pipe_menu_cache:build([Dir]), + ok = rm_rf(Dir), + ?assert(binary:match(Menu, <<"label=\"Utility\"">>) =/= nomatch). + +build_skips_hidden_entries_test() -> + Dir = mk_tmp_dir(), + write_desktop(Dir, "visible.desktop", "Visible", "visible"), + ok = file:write_file( + filename:join(Dir, "hidden.desktop"), + <<"[Desktop Entry]\nName=Hidden\nExec=hidden\nNoDisplay=true\n">> + ), + Menu = er_xdg_pipe_menu_cache:build([Dir]), + ok = rm_rf(Dir), + ?assert(binary:match(Menu, <<"Visible">>) =/= nomatch), + ?assertEqual(nomatch, binary:match(Menu, <<"Hidden">>)). + +build_skips_unparseable_files_without_failing_test() -> + Dir = mk_tmp_dir(), + write_desktop(Dir, "good.desktop", "Good", "good"), + ok = file:write_file(filename:join(Dir, "bad.desktop"), <<"[Desktop Entry]\nExec=bad\n">>), + Menu = er_xdg_pipe_menu_cache:build([Dir]), + ok = rm_rf(Dir), + ?assert(binary:match(Menu, <<"Good">>) =/= nomatch). + +build_skips_missing_dirs_test() -> + ?assertEqual( + <<"\n" + "\n" + "\n">>, + er_xdg_pipe_menu_cache:build(["/does-not-exist"]) + ). + +gen_server_serves_and_invalidates_test() -> + Root = mk_tmp_dir(), + AppDir = filename:join(Root, "applications"), + ok = file:make_dir(AppDir), + write_desktop(AppDir, "foo.desktop", "Foo", "foo"), + with_env( + [{"XDG_DATA_HOME", Root}, {"XDG_DATA_DIRS", "/does-not-exist-eunit"}], + fun() -> + {ok, Pid} = er_xdg_pipe_menu_cache:start_link(), + Menu1 = er_xdg_pipe_menu_cache:get_menu(), + ?assert(binary:match(Menu1, <<"Foo">>) =/= nomatch), + + write_desktop(AppDir, "bar.desktop", "Bar", "bar"), + ok = er_xdg_pipe_menu_cache:invalidate(), + Menu2 = er_xdg_pipe_menu_cache:get_menu(), + stop(Pid), + ?assert(binary:match(Menu2, <<"Bar">>) =/= nomatch) + end + ), + ok = rm_rf(Root). + +%% -- helpers ---------------------------------------------------------------- + +%% start_link/0 links the started process to us (the test process); +%% unlink before killing it so the kill signal doesn't propagate back +%% and take the test process down with it. +stop(Pid) -> + unlink(Pid), + true = erlang:exit(Pid, kill). + +with_env(Vars, Fun) -> + Saved = [{Name, os:getenv(Name)} || {Name, _} <- Vars], + try + lists:foreach(fun({Name, Value}) -> set_env(Name, Value) end, Vars), + Fun() + after + lists:foreach(fun({Name, Value}) -> set_env(Name, Value) end, Saved) + end. + +set_env(Name, false) -> os:unsetenv(Name); +set_env(Name, Value) -> os:putenv(Name, Value). + +write_desktop(Dir, FileName, Name, Exec) -> + ok = file:write_file( + filename:join(Dir, FileName), + iolist_to_binary(["[Desktop Entry]\nName=", Name, "\nExec=", Exec, "\n"]) + ). + +write_desktop(Dir, FileName, Name, Exec, Categories) -> + ok = file:write_file( + filename:join(Dir, FileName), + iolist_to_binary([ + "[Desktop Entry]\nName=", + Name, + "\nExec=", + Exec, + "\nCategories=", + Categories, + ";\n" + ]) + ). + +mk_tmp_dir() -> + Path = filename:join( + test_tmp_dir(), "eunit-cache-" ++ integer_to_list(erlang:unique_integer([positive])) + ), + ok = file:make_dir(Path), + Path. + +test_tmp_dir() -> + case os:getenv("TMPDIR") of + false -> "/tmp"; + Dir -> Dir + end. + +rm_rf(Path) -> + case filelib:is_dir(Path) of + true -> + {ok, Entries} = file:list_dir(Path), + lists:foreach(fun(E) -> rm_rf(filename:join(Path, E)) end, Entries), + file:del_dir(Path); + false -> + file:delete(Path) + end. -- cgit v1.2.3