From 5ff1fbe175af17fbb808c5ea942f465a932da3af Mon Sep 17 00:00:00 2001 From: "Christopher R. Nelson" Date: Tue, 21 Jul 2026 11:44:41 -0400 Subject: Rename app from er_xdg_pipe_menu to xdg_pipe_menu Drop the "er-" prefix throughout: module/atom names, filenames, socket paths, and docs. It was an implementation detail, not part of the app's identity. Co-Authored-By: Claude Sonnet 5 --- test/er_xdg_pipe_menu_cache_tests.erl | 130 ------------------ test/er_xdg_pipe_menu_desktop_entry_tests.erl | 183 -------------------------- test/er_xdg_pipe_menu_renderer_tests.erl | 167 ----------------------- test/er_xdg_pipe_menu_scanner_tests.erl | 144 -------------------- test/er_xdg_pipe_menu_socket_tests.erl | 93 ------------- test/er_xdg_pipe_menu_watcher_tests.erl | 86 ------------ test/xdg_pipe_menu_cache_tests.erl | 130 ++++++++++++++++++ test/xdg_pipe_menu_desktop_entry_tests.erl | 183 ++++++++++++++++++++++++++ test/xdg_pipe_menu_renderer_tests.erl | 167 +++++++++++++++++++++++ test/xdg_pipe_menu_scanner_tests.erl | 144 ++++++++++++++++++++ test/xdg_pipe_menu_socket_tests.erl | 93 +++++++++++++ test/xdg_pipe_menu_watcher_tests.erl | 86 ++++++++++++ 12 files changed, 803 insertions(+), 803 deletions(-) delete mode 100644 test/er_xdg_pipe_menu_cache_tests.erl delete mode 100644 test/er_xdg_pipe_menu_desktop_entry_tests.erl delete mode 100644 test/er_xdg_pipe_menu_renderer_tests.erl delete mode 100644 test/er_xdg_pipe_menu_scanner_tests.erl delete mode 100644 test/er_xdg_pipe_menu_socket_tests.erl delete mode 100644 test/er_xdg_pipe_menu_watcher_tests.erl create mode 100644 test/xdg_pipe_menu_cache_tests.erl create mode 100644 test/xdg_pipe_menu_desktop_entry_tests.erl create mode 100644 test/xdg_pipe_menu_renderer_tests.erl create mode 100644 test/xdg_pipe_menu_scanner_tests.erl create mode 100644 test/xdg_pipe_menu_socket_tests.erl create mode 100644 test/xdg_pipe_menu_watcher_tests.erl (limited to 'test') diff --git a/test/er_xdg_pipe_menu_cache_tests.erl b/test/er_xdg_pipe_menu_cache_tests.erl deleted file mode 100644 index a1d1440..0000000 --- a/test/er_xdg_pipe_menu_cache_tests.erl +++ /dev/null @@ -1,130 +0,0 @@ --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. diff --git a/test/er_xdg_pipe_menu_desktop_entry_tests.erl b/test/er_xdg_pipe_menu_desktop_entry_tests.erl deleted file mode 100644 index bcea976..0000000 --- a/test/er_xdg_pipe_menu_desktop_entry_tests.erl +++ /dev/null @@ -1,183 +0,0 @@ --module(er_xdg_pipe_menu_desktop_entry_tests). - --include_lib("eunit/include/eunit.hrl"). --include("er_xdg_pipe_menu.hrl"). - -basic_entry_test() -> - Bin = << - "[Desktop Entry]\n" - "Type=Application\n" - "Name=Firefox\n" - "Exec=firefox %u\n" - "Icon=firefox\n" - >>, - ?assertEqual( - {ok, #desktop_entry{ - name = <<"Firefox">>, exec = <<"firefox">>, icon = <<"firefox">>, categories = [], hidden = false - }}, - er_xdg_pipe_menu_desktop_entry:parse(Bin) - ). - -icon_is_optional_test() -> - Bin = << - "[Desktop Entry]\n" - "Name=Foo\n" - "Exec=foo\n" - >>, - ?assertEqual( - {ok, #desktop_entry{name = <<"Foo">>, exec = <<"foo">>, icon = undefined, categories = [], hidden = false}}, - er_xdg_pipe_menu_desktop_entry:parse(Bin) - ). - -categories_are_split_on_semicolon_test() -> - Bin = << - "[Desktop Entry]\n" - "Name=Foo\n" - "Exec=foo\n" - "Categories=Utility;Development;\n" - >>, - {ok, Entry} = er_xdg_pipe_menu_desktop_entry:parse(Bin), - ?assertEqual([<<"Utility">>, <<"Development">>], Entry#desktop_entry.categories). - -categories_is_empty_list_when_absent_test() -> - Bin = <<"[Desktop Entry]\nName=Foo\nExec=foo\n">>, - {ok, Entry} = er_xdg_pipe_menu_desktop_entry:parse(Bin), - ?assertEqual([], Entry#desktop_entry.categories). - -repeated_category_separators_are_ignored_test() -> - Bin = << - "[Desktop Entry]\n" - "Name=Foo\n" - "Exec=foo\n" - "Categories=Utility;;Development\n" - >>, - {ok, Entry} = er_xdg_pipe_menu_desktop_entry:parse(Bin), - ?assertEqual([<<"Utility">>, <<"Development">>], Entry#desktop_entry.categories). - -missing_name_test() -> - Bin = <<"[Desktop Entry]\nExec=foo\n">>, - ?assertEqual({error, missing_name}, er_xdg_pipe_menu_desktop_entry:parse(Bin)). - -missing_exec_test() -> - Bin = <<"[Desktop Entry]\nName=Foo\n">>, - ?assertEqual({error, missing_exec}, er_xdg_pipe_menu_desktop_entry:parse(Bin)). - -ignores_other_groups_test() -> - Bin = << - "[Desktop Entry]\n" - "Name=Foo\n" - "Exec=foo\n" - "\n" - "[Desktop Action new-window]\n" - "Name=New Window\n" - "Exec=foo --new-window\n" - >>, - ?assertEqual( - {ok, #desktop_entry{name = <<"Foo">>, exec = <<"foo">>, icon = undefined, categories = [], hidden = false}}, - er_xdg_pipe_menu_desktop_entry:parse(Bin) - ). - -ignores_comments_and_blank_lines_test() -> - Bin = << - "# a comment\n" - "\n" - "[Desktop Entry]\n" - "# another comment\n" - "Name=Foo\n" - "\n" - "Exec=foo\n" - >>, - ?assertEqual( - {ok, #desktop_entry{name = <<"Foo">>, exec = <<"foo">>, icon = undefined, categories = [], hidden = false}}, - er_xdg_pipe_menu_desktop_entry:parse(Bin) - ). - -strips_multiple_field_codes_test() -> - Bin = << - "[Desktop Entry]\n" - "Name=Foo\n" - "Exec=foo %f --flag %i %c\n" - >>, - ?assertEqual( - {ok, #desktop_entry{name = <<"Foo">>, exec = <<"foo --flag">>, icon = undefined, categories = [], hidden = false}}, - er_xdg_pipe_menu_desktop_entry:parse(Bin) - ). - -literal_percent_is_unescaped_test() -> - Bin = << - "[Desktop Entry]\n" - "Name=Foo\n" - "Exec=foo --progress=%%\n" - >>, - ?assertEqual( - {ok, #desktop_entry{ - name = <<"Foo">>, exec = <<"foo --progress=%">>, icon = undefined, categories = [], hidden = false - }}, - er_xdg_pipe_menu_desktop_entry:parse(Bin) - ). - -unescapes_value_test() -> - Bin = <<"[Desktop Entry]\nName=Foo\\sBar\nExec=foo\n">>, - ?assertEqual( - {ok, #desktop_entry{name = <<"Foo Bar">>, exec = <<"foo">>, icon = undefined, categories = [], hidden = false}}, - er_xdg_pipe_menu_desktop_entry:parse(Bin) - ). - -no_display_is_hidden_test() -> - Bin = <<"[Desktop Entry]\nName=Foo\nExec=foo\nNoDisplay=true\n">>, - {ok, Entry} = er_xdg_pipe_menu_desktop_entry:parse(Bin), - ?assertEqual(true, Entry#desktop_entry.hidden). - -hidden_key_is_hidden_test() -> - Bin = <<"[Desktop Entry]\nName=Foo\nExec=foo\nHidden=true\n">>, - {ok, Entry} = er_xdg_pipe_menu_desktop_entry:parse(Bin), - ?assertEqual(true, Entry#desktop_entry.hidden). - -non_application_type_is_hidden_test() -> - Bin = <<"[Desktop Entry]\nName=Foo\nExec=foo\nType=Link\n">>, - {ok, Entry} = er_xdg_pipe_menu_desktop_entry:parse(Bin), - ?assertEqual(true, Entry#desktop_entry.hidden). - -application_type_is_not_hidden_test() -> - Bin = <<"[Desktop Entry]\nName=Foo\nExec=foo\nType=Application\n">>, - {ok, Entry} = er_xdg_pipe_menu_desktop_entry:parse(Bin), - ?assertEqual(false, Entry#desktop_entry.hidden). - -missing_type_is_not_hidden_test() -> - Bin = <<"[Desktop Entry]\nName=Foo\nExec=foo\n">>, - {ok, Entry} = er_xdg_pipe_menu_desktop_entry:parse(Bin), - ?assertEqual(false, Entry#desktop_entry.hidden). - -false_values_are_not_hidden_test() -> - Bin = << - "[Desktop Entry]\n" - "Name=Foo\n" - "Exec=foo\n" - "NoDisplay=false\n" - "Hidden=false\n" - "Type=Application\n" - >>, - {ok, Entry} = er_xdg_pipe_menu_desktop_entry:parse(Bin), - ?assertEqual(false, Entry#desktop_entry.hidden). - -parse_file_test() -> - Path = filename:join(test_tmp_dir(), "eunit-desktop-entry-test.desktop"), - ok = file:write_file(Path, <<"[Desktop Entry]\nName=Foo\nExec=foo\n">>), - Result = er_xdg_pipe_menu_desktop_entry:parse_file(Path), - ok = file:delete(Path), - ?assertEqual( - {ok, #desktop_entry{name = <<"Foo">>, exec = <<"foo">>, icon = undefined, categories = [], hidden = false}}, - Result - ). - -parse_file_missing_test() -> - ?assertEqual( - {error, enoent}, - er_xdg_pipe_menu_desktop_entry:parse_file("/nonexistent/does-not-exist.desktop") - ). - -test_tmp_dir() -> - case os:getenv("TMPDIR") of - false -> "/tmp"; - Dir -> Dir - end. diff --git a/test/er_xdg_pipe_menu_renderer_tests.erl b/test/er_xdg_pipe_menu_renderer_tests.erl deleted file mode 100644 index 7273625..0000000 --- a/test/er_xdg_pipe_menu_renderer_tests.erl +++ /dev/null @@ -1,167 +0,0 @@ --module(er_xdg_pipe_menu_renderer_tests). - --include_lib("eunit/include/eunit.hrl"). --include_lib("xmerl/include/xmerl.hrl"). --include("er_xdg_pipe_menu.hrl"). - -entry(Name, Exec, Icon) -> - entry(Name, Exec, Icon, []). - -entry(Name, Exec, Icon, Categories) -> - entry(Name, Exec, Icon, Categories, false). - -entry(Name, Exec, Icon, Categories, Hidden) -> - #desktop_entry{ - name = Name, exec = Exec, icon = Icon, categories = Categories, hidden = Hidden - }. - -empty_list_renders_empty_menu_test() -> - ?assertEqual( - <<"\n" - "\n" - "\n">>, - er_xdg_pipe_menu_renderer:render([]) - ). - -single_entry_with_icon_test() -> - Rendered = er_xdg_pipe_menu_renderer:render([ - entry(<<"Firefox">>, <<"firefox">>, <<"firefox">>, [<<"Network">>]) - ]), - ?assertEqual( - <<"\n" - "\n" - " \n" - " \n" - " \n" - " firefox\n" - " \n" - " \n" - " \n" - "\n">>, - Rendered - ). - -entry_without_icon_omits_icon_attr_test() -> - Rendered = er_xdg_pipe_menu_renderer:render([ - entry(<<"Foo">>, <<"foo">>, undefined, [<<"Utility">>]) - ]), - ?assertEqual( - <<"\n" - "\n" - " \n" - " \n" - " \n" - " foo\n" - " \n" - " \n" - " \n" - "\n">>, - Rendered - ). - -entries_without_categories_go_in_other_submenu_test() -> - Rendered = er_xdg_pipe_menu_renderer:render([entry(<<"Foo">>, <<"foo">>, undefined)]), - ?assertEqual( - <<"\n" - "\n" - " \n" - " \n" - " \n" - " foo\n" - " \n" - " \n" - " \n" - "\n">>, - Rendered - ). - -hidden_entries_are_dropped_test() -> - Entries = [ - entry(<<"Visible">>, <<"visible">>, undefined, [<<"Utility">>]), - entry(<<"Invisible">>, <<"invisible">>, undefined, [<<"Utility">>], true) - ], - Rendered = er_xdg_pipe_menu_renderer:render(Entries), - ?assertNot(binary:match(Rendered, <<"Invisible">>) =/= nomatch), - ?assert(binary:match(Rendered, <<"Visible">>) =/= nomatch). - -hidden_entry_leaves_no_empty_submenu_test() -> - Entries = [entry(<<"Invisible">>, <<"invisible">>, undefined, [<<"Utility">>], true)], - Rendered = er_xdg_pipe_menu_renderer:render(Entries), - ?assertNot(binary:match(Rendered, <<"Utility">>) =/= nomatch). - -entries_are_sorted_case_insensitively_within_a_submenu_test() -> - Entries = [ - entry(<<"zeta">>, <<"zeta">>, undefined, [<<"Utility">>]), - entry(<<"Alpha">>, <<"alpha">>, undefined, [<<"Utility">>]), - entry(<<"beta">>, <<"beta">>, undefined, [<<"Utility">>]) - ], - Rendered = er_xdg_pipe_menu_renderer:render(Entries), - Positions = [ - element(1, binary:match(Rendered, <<"label=\"Alpha\"">>)), - element(1, binary:match(Rendered, <<"label=\"beta\"">>)), - element(1, binary:match(Rendered, <<"label=\"zeta\"">>)) - ], - ?assertEqual(Positions, lists:sort(Positions)). - -submenus_are_sorted_case_insensitively_test() -> - Entries = [ - entry(<<"Zed App">>, <<"zed">>, undefined, [<<"zeta">>]), - entry(<<"Alpha App">>, <<"alpha">>, undefined, [<<"Alpha">>]), - entry(<<"Beta App">>, <<"beta">>, undefined, [<<"beta">>]) - ], - Rendered = er_xdg_pipe_menu_renderer:render(Entries), - Positions = [ - element(1, binary:match(Rendered, <<"label=\"Alpha\"">>)), - element(1, binary:match(Rendered, <<"label=\"beta\"">>)), - element(1, binary:match(Rendered, <<"label=\"zeta\"">>)) - ], - ?assertEqual(Positions, lists:sort(Positions)). - -entry_with_multiple_categories_appears_in_each_submenu_test() -> - Entries = [entry(<<"Foo">>, <<"foo">>, undefined, [<<"Utility">>, <<"Development">>])], - Rendered = er_xdg_pipe_menu_renderer:render(Entries), - {Doc, _Rest} = xmerl_scan:string(binary_to_list(Rendered)), - Menus = [E || E = #xmlElement{name = menu} <- Doc#xmlElement.content], - ?assertEqual([<<"Development">>, <<"Utility">>], lists:sort([menu_id(M) || M <- Menus])), - Items = [I || M <- Menus, I = #xmlElement{name = item} <- M#xmlElement.content], - ?assertEqual(2, length(Items)). - -escapes_special_characters_test() -> - Entry = entry( - <<"Foo & \"Baz\"">>, - <<"foo --title=\"&y\"">>, - <<"icon\"&<>">>, - [<<"Foo & ">>] - ), - Rendered = er_xdg_pipe_menu_renderer:render([Entry]), - ?assertEqual( - <<"\n" - "\n" - " \" label=\"Foo & <Bar>\">\n" - " "Baz"\" icon=\"icon"&<>\">\n" - " \n" - " foo --title=\"<x>&y\"\n" - " \n" - " \n" - " \n" - "\n">>, - Rendered - ). - -output_is_well_formed_xml_test() -> - Entries = [ - entry(<<"Firefox">>, <<"firefox">>, <<"firefox">>, [<<"Network">>]), - entry(<<"Foo & Bar">>, <<"foo">>, undefined, [<<"Network">>]) - ], - Rendered = er_xdg_pipe_menu_renderer:render(Entries), - {Doc, _Rest} = xmerl_scan:string(binary_to_list(Rendered)), - ?assertEqual(openbox_pipe_menu, Doc#xmlElement.name), - Menus = [E || E = #xmlElement{name = menu} <- Doc#xmlElement.content], - ?assertEqual(1, length(Menus)), - [Menu] = Menus, - Items = [E || E = #xmlElement{name = item} <- Menu#xmlElement.content], - ?assertEqual(2, length(Items)). - -menu_id(#xmlElement{attributes = Attrs}) -> - [Value] = [V || #xmlAttribute{name = id, value = V} <- Attrs], - list_to_binary(Value). diff --git a/test/er_xdg_pipe_menu_scanner_tests.erl b/test/er_xdg_pipe_menu_scanner_tests.erl deleted file mode 100644 index 3f93496..0000000 --- a/test/er_xdg_pipe_menu_scanner_tests.erl +++ /dev/null @@ -1,144 +0,0 @@ --module(er_xdg_pipe_menu_scanner_tests). - --include_lib("eunit/include/eunit.hrl"). - -data_dirs_defaults_test() -> - with_env( - [{"XDG_DATA_HOME", false}, {"XDG_DATA_DIRS", false}, {"HOME", "/home/testuser"}], - fun() -> - ?assertEqual( - [ - "/home/testuser/.local/share/applications", - "/usr/local/share/applications", - "/usr/share/applications" - ], - er_xdg_pipe_menu_scanner:data_dirs() - ) - end - ). - -data_dirs_honors_xdg_data_home_test() -> - with_env( - [{"XDG_DATA_HOME", "/custom/data"}, {"XDG_DATA_DIRS", false}, {"HOME", "/home/testuser"}], - fun() -> - ?assertEqual( - [ - "/custom/data/applications", - "/usr/local/share/applications", - "/usr/share/applications" - ], - er_xdg_pipe_menu_scanner:data_dirs() - ) - end - ). - -data_dirs_honors_xdg_data_dirs_test() -> - with_env( - [ - {"XDG_DATA_HOME", false}, - {"XDG_DATA_DIRS", "/opt/flatpak/share:/var/lib/flatpak/exports/share"}, - {"HOME", "/home/testuser"} - ], - fun() -> - ?assertEqual( - [ - "/home/testuser/.local/share/applications", - "/opt/flatpak/share/applications", - "/var/lib/flatpak/exports/share/applications" - ], - er_xdg_pipe_menu_scanner:data_dirs() - ) - end - ). - -data_dirs_treats_empty_as_unset_test() -> - with_env( - [{"XDG_DATA_HOME", ""}, {"XDG_DATA_DIRS", ""}, {"HOME", "/home/testuser"}], - fun() -> - ?assertEqual( - [ - "/home/testuser/.local/share/applications", - "/usr/local/share/applications", - "/usr/share/applications" - ], - er_xdg_pipe_menu_scanner:data_dirs() - ) - end - ). - -data_dirs_ignores_empty_segments_in_xdg_data_dirs_test() -> - with_env( - [{"XDG_DATA_HOME", false}, {"XDG_DATA_DIRS", "/a::/b:"}, {"HOME", "/home/testuser"}], - fun() -> - ?assertEqual( - ["/home/testuser/.local/share/applications", "/a/applications", "/b/applications"], - er_xdg_pipe_menu_scanner:data_dirs() - ) - end - ). - -data_dirs_skips_data_home_without_xdg_data_home_or_home_test() -> - with_env( - [{"XDG_DATA_HOME", false}, {"XDG_DATA_DIRS", false}, {"HOME", false}], - fun() -> - ?assertEqual( - ["/usr/local/share/applications", "/usr/share/applications"], - er_xdg_pipe_menu_scanner:data_dirs() - ) - end - ). - -find_desktop_files_recurses_and_skips_missing_dirs_test() -> - Root = mk_tmp_dir(), - SubDir = filename:join(Root, "kde4"), - ok = file:make_dir(SubDir), - ok = file:write_file(filename:join(Root, "top.desktop"), <<"[Desktop Entry]\n">>), - ok = file:write_file(filename:join(SubDir, "nested.desktop"), <<"[Desktop Entry]\n">>), - ok = file:write_file(filename:join(Root, "not-a-desktop-file.txt"), <<"ignore me">>), - Missing = filename:join(Root, "does-not-exist"), - Found = er_xdg_pipe_menu_scanner:find_desktop_files([Root, Missing]), - ok = rm_rf(Root), - ?assertEqual( - lists:sort([filename:join(Root, "top.desktop"), filename:join(SubDir, "nested.desktop")]), - lists:sort(Found) - ). - -find_desktop_files_empty_for_no_dirs_test() -> - ?assertEqual([], er_xdg_pipe_menu_scanner:find_desktop_files([])). - -%% -- helpers ---------------------------------------------------------------- - -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). - -mk_tmp_dir() -> - Path = filename:join( - test_tmp_dir(), "eunit-scanner-" ++ 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. diff --git a/test/er_xdg_pipe_menu_socket_tests.erl b/test/er_xdg_pipe_menu_socket_tests.erl deleted file mode 100644 index 08a4b56..0000000 --- a/test/er_xdg_pipe_menu_socket_tests.erl +++ /dev/null @@ -1,93 +0,0 @@ --module(er_xdg_pipe_menu_socket_tests). - --include_lib("eunit/include/eunit.hrl"). - -serves_the_cached_menu_test() -> - Root = mk_tmp_dir(), - AppDir = filename:join(Root, "applications"), - ok = file:make_dir(AppDir), - write_desktop(AppDir, "foo.desktop", "Foo", "foo"), - SockPath = filename:join( - test_tmp_dir(), "eunit-socket-" ++ integer_to_list(erlang:unique_integer([positive])) - ), - PrevSockPath = application:get_env(er_xdg_pipe_menu, socket_path), - application:set_env(er_xdg_pipe_menu, socket_path, SockPath), - with_env( - [{"XDG_DATA_HOME", Root}, {"XDG_DATA_DIRS", "/does-not-exist-eunit"}], - fun() -> - {ok, CachePid} = er_xdg_pipe_menu_cache:start_link(), - {ok, SocketPid} = er_xdg_pipe_menu_socket:start_link(), - - {ok, Conn} = gen_tcp:connect({local, SockPath}, 0, [binary, {active, false}], 1000), - Received = recv_all(Conn, []), - gen_tcp:close(Conn), - - Expected = er_xdg_pipe_menu_cache:get_menu(), - stop(SocketPid), - stop(CachePid), - ?assertEqual(Expected, Received), - ?assert(binary:match(Received, <<"Foo">>) =/= nomatch) - end - ), - _ = file:delete(SockPath), - ok = rm_rf(Root), - restore_env(er_xdg_pipe_menu, socket_path, PrevSockPath). - -%% -- 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). - -recv_all(Socket, Acc) -> - case gen_tcp:recv(Socket, 0, 1000) of - {ok, Data} -> recv_all(Socket, [Data | Acc]); - {error, closed} -> iolist_to_binary(lists:reverse(Acc)) - end. - -restore_env(App, Key, undefined) -> application:unset_env(App, Key); -restore_env(App, Key, {ok, Value}) -> application:set_env(App, Key, Value). - -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"]) - ). - -mk_tmp_dir() -> - Path = filename:join( - test_tmp_dir(), "eunit-socket-dir-" ++ 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. diff --git a/test/er_xdg_pipe_menu_watcher_tests.erl b/test/er_xdg_pipe_menu_watcher_tests.erl deleted file mode 100644 index 6754f24..0000000 --- a/test/er_xdg_pipe_menu_watcher_tests.erl +++ /dev/null @@ -1,86 +0,0 @@ --module(er_xdg_pipe_menu_watcher_tests). - --include_lib("eunit/include/eunit.hrl"). - -%% Exercises the real debounce-timer-reset-then-invalidate wiring by -%% sending the watcher a synthetic message of the exact shape `fs' -%% delivers, rather than waiting on a real inotify event -- `fs' -%% itself is treated as out of scope for this test. -debounced_event_invalidates_cache_test() -> - Root = mk_tmp_dir(), - AppDir = filename:join(Root, "applications"), - ok = file:make_dir(AppDir), - write_desktop(AppDir, "foo.desktop", "Foo", "foo"), - PrevDebounce = application:get_env(er_xdg_pipe_menu, debounce_ms), - application:set_env(er_xdg_pipe_menu, debounce_ms, 20), - with_env( - [{"XDG_DATA_HOME", Root}, {"XDG_DATA_DIRS", "/does-not-exist-eunit"}], - fun() -> - {ok, CachePid} = er_xdg_pipe_menu_cache:start_link(), - {ok, WatcherPid} = er_xdg_pipe_menu_watcher:start_link(), - - write_desktop(AppDir, "bar.desktop", "Bar", "bar"), - WatcherPid ! {self(), {fs, file_event}, {"dummy", [modified]}}, - timer:sleep(100), - - Menu = er_xdg_pipe_menu_cache:get_menu(), - stop(WatcherPid), - stop(CachePid), - ?assert(binary:match(Menu, <<"Bar">>) =/= nomatch) - end - ), - ok = rm_rf(Root), - restore_env(er_xdg_pipe_menu, debounce_ms, PrevDebounce). - -%% -- 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). - -restore_env(App, Key, undefined) -> application:unset_env(App, Key); -restore_env(App, Key, {ok, Value}) -> application:set_env(App, Key, Value). - -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"]) - ). - -mk_tmp_dir() -> - Path = filename:join( - test_tmp_dir(), "eunit-watcher-" ++ 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. diff --git a/test/xdg_pipe_menu_cache_tests.erl b/test/xdg_pipe_menu_cache_tests.erl new file mode 100644 index 0000000..4b9dfbb --- /dev/null +++ b/test/xdg_pipe_menu_cache_tests.erl @@ -0,0 +1,130 @@ +-module(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 = 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 = 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 = 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 = 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">>, + 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} = xdg_pipe_menu_cache:start_link(), + Menu1 = xdg_pipe_menu_cache:get_menu(), + ?assert(binary:match(Menu1, <<"Foo">>) =/= nomatch), + + write_desktop(AppDir, "bar.desktop", "Bar", "bar"), + ok = xdg_pipe_menu_cache:invalidate(), + Menu2 = 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. diff --git a/test/xdg_pipe_menu_desktop_entry_tests.erl b/test/xdg_pipe_menu_desktop_entry_tests.erl new file mode 100644 index 0000000..a55cd67 --- /dev/null +++ b/test/xdg_pipe_menu_desktop_entry_tests.erl @@ -0,0 +1,183 @@ +-module(xdg_pipe_menu_desktop_entry_tests). + +-include_lib("eunit/include/eunit.hrl"). +-include("xdg_pipe_menu.hrl"). + +basic_entry_test() -> + Bin = << + "[Desktop Entry]\n" + "Type=Application\n" + "Name=Firefox\n" + "Exec=firefox %u\n" + "Icon=firefox\n" + >>, + ?assertEqual( + {ok, #desktop_entry{ + name = <<"Firefox">>, exec = <<"firefox">>, icon = <<"firefox">>, categories = [], hidden = false + }}, + xdg_pipe_menu_desktop_entry:parse(Bin) + ). + +icon_is_optional_test() -> + Bin = << + "[Desktop Entry]\n" + "Name=Foo\n" + "Exec=foo\n" + >>, + ?assertEqual( + {ok, #desktop_entry{name = <<"Foo">>, exec = <<"foo">>, icon = undefined, categories = [], hidden = false}}, + xdg_pipe_menu_desktop_entry:parse(Bin) + ). + +categories_are_split_on_semicolon_test() -> + Bin = << + "[Desktop Entry]\n" + "Name=Foo\n" + "Exec=foo\n" + "Categories=Utility;Development;\n" + >>, + {ok, Entry} = xdg_pipe_menu_desktop_entry:parse(Bin), + ?assertEqual([<<"Utility">>, <<"Development">>], Entry#desktop_entry.categories). + +categories_is_empty_list_when_absent_test() -> + Bin = <<"[Desktop Entry]\nName=Foo\nExec=foo\n">>, + {ok, Entry} = xdg_pipe_menu_desktop_entry:parse(Bin), + ?assertEqual([], Entry#desktop_entry.categories). + +repeated_category_separators_are_ignored_test() -> + Bin = << + "[Desktop Entry]\n" + "Name=Foo\n" + "Exec=foo\n" + "Categories=Utility;;Development\n" + >>, + {ok, Entry} = xdg_pipe_menu_desktop_entry:parse(Bin), + ?assertEqual([<<"Utility">>, <<"Development">>], Entry#desktop_entry.categories). + +missing_name_test() -> + Bin = <<"[Desktop Entry]\nExec=foo\n">>, + ?assertEqual({error, missing_name}, xdg_pipe_menu_desktop_entry:parse(Bin)). + +missing_exec_test() -> + Bin = <<"[Desktop Entry]\nName=Foo\n">>, + ?assertEqual({error, missing_exec}, xdg_pipe_menu_desktop_entry:parse(Bin)). + +ignores_other_groups_test() -> + Bin = << + "[Desktop Entry]\n" + "Name=Foo\n" + "Exec=foo\n" + "\n" + "[Desktop Action new-window]\n" + "Name=New Window\n" + "Exec=foo --new-window\n" + >>, + ?assertEqual( + {ok, #desktop_entry{name = <<"Foo">>, exec = <<"foo">>, icon = undefined, categories = [], hidden = false}}, + xdg_pipe_menu_desktop_entry:parse(Bin) + ). + +ignores_comments_and_blank_lines_test() -> + Bin = << + "# a comment\n" + "\n" + "[Desktop Entry]\n" + "# another comment\n" + "Name=Foo\n" + "\n" + "Exec=foo\n" + >>, + ?assertEqual( + {ok, #desktop_entry{name = <<"Foo">>, exec = <<"foo">>, icon = undefined, categories = [], hidden = false}}, + xdg_pipe_menu_desktop_entry:parse(Bin) + ). + +strips_multiple_field_codes_test() -> + Bin = << + "[Desktop Entry]\n" + "Name=Foo\n" + "Exec=foo %f --flag %i %c\n" + >>, + ?assertEqual( + {ok, #desktop_entry{name = <<"Foo">>, exec = <<"foo --flag">>, icon = undefined, categories = [], hidden = false}}, + xdg_pipe_menu_desktop_entry:parse(Bin) + ). + +literal_percent_is_unescaped_test() -> + Bin = << + "[Desktop Entry]\n" + "Name=Foo\n" + "Exec=foo --progress=%%\n" + >>, + ?assertEqual( + {ok, #desktop_entry{ + name = <<"Foo">>, exec = <<"foo --progress=%">>, icon = undefined, categories = [], hidden = false + }}, + xdg_pipe_menu_desktop_entry:parse(Bin) + ). + +unescapes_value_test() -> + Bin = <<"[Desktop Entry]\nName=Foo\\sBar\nExec=foo\n">>, + ?assertEqual( + {ok, #desktop_entry{name = <<"Foo Bar">>, exec = <<"foo">>, icon = undefined, categories = [], hidden = false}}, + xdg_pipe_menu_desktop_entry:parse(Bin) + ). + +no_display_is_hidden_test() -> + Bin = <<"[Desktop Entry]\nName=Foo\nExec=foo\nNoDisplay=true\n">>, + {ok, Entry} = xdg_pipe_menu_desktop_entry:parse(Bin), + ?assertEqual(true, Entry#desktop_entry.hidden). + +hidden_key_is_hidden_test() -> + Bin = <<"[Desktop Entry]\nName=Foo\nExec=foo\nHidden=true\n">>, + {ok, Entry} = xdg_pipe_menu_desktop_entry:parse(Bin), + ?assertEqual(true, Entry#desktop_entry.hidden). + +non_application_type_is_hidden_test() -> + Bin = <<"[Desktop Entry]\nName=Foo\nExec=foo\nType=Link\n">>, + {ok, Entry} = xdg_pipe_menu_desktop_entry:parse(Bin), + ?assertEqual(true, Entry#desktop_entry.hidden). + +application_type_is_not_hidden_test() -> + Bin = <<"[Desktop Entry]\nName=Foo\nExec=foo\nType=Application\n">>, + {ok, Entry} = xdg_pipe_menu_desktop_entry:parse(Bin), + ?assertEqual(false, Entry#desktop_entry.hidden). + +missing_type_is_not_hidden_test() -> + Bin = <<"[Desktop Entry]\nName=Foo\nExec=foo\n">>, + {ok, Entry} = xdg_pipe_menu_desktop_entry:parse(Bin), + ?assertEqual(false, Entry#desktop_entry.hidden). + +false_values_are_not_hidden_test() -> + Bin = << + "[Desktop Entry]\n" + "Name=Foo\n" + "Exec=foo\n" + "NoDisplay=false\n" + "Hidden=false\n" + "Type=Application\n" + >>, + {ok, Entry} = xdg_pipe_menu_desktop_entry:parse(Bin), + ?assertEqual(false, Entry#desktop_entry.hidden). + +parse_file_test() -> + Path = filename:join(test_tmp_dir(), "eunit-desktop-entry-test.desktop"), + ok = file:write_file(Path, <<"[Desktop Entry]\nName=Foo\nExec=foo\n">>), + Result = xdg_pipe_menu_desktop_entry:parse_file(Path), + ok = file:delete(Path), + ?assertEqual( + {ok, #desktop_entry{name = <<"Foo">>, exec = <<"foo">>, icon = undefined, categories = [], hidden = false}}, + Result + ). + +parse_file_missing_test() -> + ?assertEqual( + {error, enoent}, + xdg_pipe_menu_desktop_entry:parse_file("/nonexistent/does-not-exist.desktop") + ). + +test_tmp_dir() -> + case os:getenv("TMPDIR") of + false -> "/tmp"; + Dir -> Dir + end. diff --git a/test/xdg_pipe_menu_renderer_tests.erl b/test/xdg_pipe_menu_renderer_tests.erl new file mode 100644 index 0000000..6d632cf --- /dev/null +++ b/test/xdg_pipe_menu_renderer_tests.erl @@ -0,0 +1,167 @@ +-module(xdg_pipe_menu_renderer_tests). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("xmerl/include/xmerl.hrl"). +-include("xdg_pipe_menu.hrl"). + +entry(Name, Exec, Icon) -> + entry(Name, Exec, Icon, []). + +entry(Name, Exec, Icon, Categories) -> + entry(Name, Exec, Icon, Categories, false). + +entry(Name, Exec, Icon, Categories, Hidden) -> + #desktop_entry{ + name = Name, exec = Exec, icon = Icon, categories = Categories, hidden = Hidden + }. + +empty_list_renders_empty_menu_test() -> + ?assertEqual( + <<"\n" + "\n" + "\n">>, + xdg_pipe_menu_renderer:render([]) + ). + +single_entry_with_icon_test() -> + Rendered = xdg_pipe_menu_renderer:render([ + entry(<<"Firefox">>, <<"firefox">>, <<"firefox">>, [<<"Network">>]) + ]), + ?assertEqual( + <<"\n" + "\n" + " \n" + " \n" + " \n" + " firefox\n" + " \n" + " \n" + " \n" + "\n">>, + Rendered + ). + +entry_without_icon_omits_icon_attr_test() -> + Rendered = xdg_pipe_menu_renderer:render([ + entry(<<"Foo">>, <<"foo">>, undefined, [<<"Utility">>]) + ]), + ?assertEqual( + <<"\n" + "\n" + " \n" + " \n" + " \n" + " foo\n" + " \n" + " \n" + " \n" + "\n">>, + Rendered + ). + +entries_without_categories_go_in_other_submenu_test() -> + Rendered = xdg_pipe_menu_renderer:render([entry(<<"Foo">>, <<"foo">>, undefined)]), + ?assertEqual( + <<"\n" + "\n" + " \n" + " \n" + " \n" + " foo\n" + " \n" + " \n" + " \n" + "\n">>, + Rendered + ). + +hidden_entries_are_dropped_test() -> + Entries = [ + entry(<<"Visible">>, <<"visible">>, undefined, [<<"Utility">>]), + entry(<<"Invisible">>, <<"invisible">>, undefined, [<<"Utility">>], true) + ], + Rendered = xdg_pipe_menu_renderer:render(Entries), + ?assertNot(binary:match(Rendered, <<"Invisible">>) =/= nomatch), + ?assert(binary:match(Rendered, <<"Visible">>) =/= nomatch). + +hidden_entry_leaves_no_empty_submenu_test() -> + Entries = [entry(<<"Invisible">>, <<"invisible">>, undefined, [<<"Utility">>], true)], + Rendered = xdg_pipe_menu_renderer:render(Entries), + ?assertNot(binary:match(Rendered, <<"Utility">>) =/= nomatch). + +entries_are_sorted_case_insensitively_within_a_submenu_test() -> + Entries = [ + entry(<<"zeta">>, <<"zeta">>, undefined, [<<"Utility">>]), + entry(<<"Alpha">>, <<"alpha">>, undefined, [<<"Utility">>]), + entry(<<"beta">>, <<"beta">>, undefined, [<<"Utility">>]) + ], + Rendered = xdg_pipe_menu_renderer:render(Entries), + Positions = [ + element(1, binary:match(Rendered, <<"label=\"Alpha\"">>)), + element(1, binary:match(Rendered, <<"label=\"beta\"">>)), + element(1, binary:match(Rendered, <<"label=\"zeta\"">>)) + ], + ?assertEqual(Positions, lists:sort(Positions)). + +submenus_are_sorted_case_insensitively_test() -> + Entries = [ + entry(<<"Zed App">>, <<"zed">>, undefined, [<<"zeta">>]), + entry(<<"Alpha App">>, <<"alpha">>, undefined, [<<"Alpha">>]), + entry(<<"Beta App">>, <<"beta">>, undefined, [<<"beta">>]) + ], + Rendered = xdg_pipe_menu_renderer:render(Entries), + Positions = [ + element(1, binary:match(Rendered, <<"label=\"Alpha\"">>)), + element(1, binary:match(Rendered, <<"label=\"beta\"">>)), + element(1, binary:match(Rendered, <<"label=\"zeta\"">>)) + ], + ?assertEqual(Positions, lists:sort(Positions)). + +entry_with_multiple_categories_appears_in_each_submenu_test() -> + Entries = [entry(<<"Foo">>, <<"foo">>, undefined, [<<"Utility">>, <<"Development">>])], + Rendered = xdg_pipe_menu_renderer:render(Entries), + {Doc, _Rest} = xmerl_scan:string(binary_to_list(Rendered)), + Menus = [E || E = #xmlElement{name = menu} <- Doc#xmlElement.content], + ?assertEqual([<<"Development">>, <<"Utility">>], lists:sort([menu_id(M) || M <- Menus])), + Items = [I || M <- Menus, I = #xmlElement{name = item} <- M#xmlElement.content], + ?assertEqual(2, length(Items)). + +escapes_special_characters_test() -> + Entry = entry( + <<"Foo & \"Baz\"">>, + <<"foo --title=\"&y\"">>, + <<"icon\"&<>">>, + [<<"Foo & ">>] + ), + Rendered = xdg_pipe_menu_renderer:render([Entry]), + ?assertEqual( + <<"\n" + "\n" + " \" label=\"Foo & <Bar>\">\n" + " "Baz"\" icon=\"icon"&<>\">\n" + " \n" + " foo --title=\"<x>&y\"\n" + " \n" + " \n" + " \n" + "\n">>, + Rendered + ). + +output_is_well_formed_xml_test() -> + Entries = [ + entry(<<"Firefox">>, <<"firefox">>, <<"firefox">>, [<<"Network">>]), + entry(<<"Foo & Bar">>, <<"foo">>, undefined, [<<"Network">>]) + ], + Rendered = xdg_pipe_menu_renderer:render(Entries), + {Doc, _Rest} = xmerl_scan:string(binary_to_list(Rendered)), + ?assertEqual(openbox_pipe_menu, Doc#xmlElement.name), + Menus = [E || E = #xmlElement{name = menu} <- Doc#xmlElement.content], + ?assertEqual(1, length(Menus)), + [Menu] = Menus, + Items = [E || E = #xmlElement{name = item} <- Menu#xmlElement.content], + ?assertEqual(2, length(Items)). + +menu_id(#xmlElement{attributes = Attrs}) -> + [Value] = [V || #xmlAttribute{name = id, value = V} <- Attrs], + list_to_binary(Value). diff --git a/test/xdg_pipe_menu_scanner_tests.erl b/test/xdg_pipe_menu_scanner_tests.erl new file mode 100644 index 0000000..fa321ad --- /dev/null +++ b/test/xdg_pipe_menu_scanner_tests.erl @@ -0,0 +1,144 @@ +-module(xdg_pipe_menu_scanner_tests). + +-include_lib("eunit/include/eunit.hrl"). + +data_dirs_defaults_test() -> + with_env( + [{"XDG_DATA_HOME", false}, {"XDG_DATA_DIRS", false}, {"HOME", "/home/testuser"}], + fun() -> + ?assertEqual( + [ + "/home/testuser/.local/share/applications", + "/usr/local/share/applications", + "/usr/share/applications" + ], + xdg_pipe_menu_scanner:data_dirs() + ) + end + ). + +data_dirs_honors_xdg_data_home_test() -> + with_env( + [{"XDG_DATA_HOME", "/custom/data"}, {"XDG_DATA_DIRS", false}, {"HOME", "/home/testuser"}], + fun() -> + ?assertEqual( + [ + "/custom/data/applications", + "/usr/local/share/applications", + "/usr/share/applications" + ], + xdg_pipe_menu_scanner:data_dirs() + ) + end + ). + +data_dirs_honors_xdg_data_dirs_test() -> + with_env( + [ + {"XDG_DATA_HOME", false}, + {"XDG_DATA_DIRS", "/opt/flatpak/share:/var/lib/flatpak/exports/share"}, + {"HOME", "/home/testuser"} + ], + fun() -> + ?assertEqual( + [ + "/home/testuser/.local/share/applications", + "/opt/flatpak/share/applications", + "/var/lib/flatpak/exports/share/applications" + ], + xdg_pipe_menu_scanner:data_dirs() + ) + end + ). + +data_dirs_treats_empty_as_unset_test() -> + with_env( + [{"XDG_DATA_HOME", ""}, {"XDG_DATA_DIRS", ""}, {"HOME", "/home/testuser"}], + fun() -> + ?assertEqual( + [ + "/home/testuser/.local/share/applications", + "/usr/local/share/applications", + "/usr/share/applications" + ], + xdg_pipe_menu_scanner:data_dirs() + ) + end + ). + +data_dirs_ignores_empty_segments_in_xdg_data_dirs_test() -> + with_env( + [{"XDG_DATA_HOME", false}, {"XDG_DATA_DIRS", "/a::/b:"}, {"HOME", "/home/testuser"}], + fun() -> + ?assertEqual( + ["/home/testuser/.local/share/applications", "/a/applications", "/b/applications"], + xdg_pipe_menu_scanner:data_dirs() + ) + end + ). + +data_dirs_skips_data_home_without_xdg_data_home_or_home_test() -> + with_env( + [{"XDG_DATA_HOME", false}, {"XDG_DATA_DIRS", false}, {"HOME", false}], + fun() -> + ?assertEqual( + ["/usr/local/share/applications", "/usr/share/applications"], + xdg_pipe_menu_scanner:data_dirs() + ) + end + ). + +find_desktop_files_recurses_and_skips_missing_dirs_test() -> + Root = mk_tmp_dir(), + SubDir = filename:join(Root, "kde4"), + ok = file:make_dir(SubDir), + ok = file:write_file(filename:join(Root, "top.desktop"), <<"[Desktop Entry]\n">>), + ok = file:write_file(filename:join(SubDir, "nested.desktop"), <<"[Desktop Entry]\n">>), + ok = file:write_file(filename:join(Root, "not-a-desktop-file.txt"), <<"ignore me">>), + Missing = filename:join(Root, "does-not-exist"), + Found = xdg_pipe_menu_scanner:find_desktop_files([Root, Missing]), + ok = rm_rf(Root), + ?assertEqual( + lists:sort([filename:join(Root, "top.desktop"), filename:join(SubDir, "nested.desktop")]), + lists:sort(Found) + ). + +find_desktop_files_empty_for_no_dirs_test() -> + ?assertEqual([], xdg_pipe_menu_scanner:find_desktop_files([])). + +%% -- helpers ---------------------------------------------------------------- + +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). + +mk_tmp_dir() -> + Path = filename:join( + test_tmp_dir(), "eunit-scanner-" ++ 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. diff --git a/test/xdg_pipe_menu_socket_tests.erl b/test/xdg_pipe_menu_socket_tests.erl new file mode 100644 index 0000000..91cd3e4 --- /dev/null +++ b/test/xdg_pipe_menu_socket_tests.erl @@ -0,0 +1,93 @@ +-module(xdg_pipe_menu_socket_tests). + +-include_lib("eunit/include/eunit.hrl"). + +serves_the_cached_menu_test() -> + Root = mk_tmp_dir(), + AppDir = filename:join(Root, "applications"), + ok = file:make_dir(AppDir), + write_desktop(AppDir, "foo.desktop", "Foo", "foo"), + SockPath = filename:join( + test_tmp_dir(), "eunit-socket-" ++ integer_to_list(erlang:unique_integer([positive])) + ), + PrevSockPath = application:get_env(xdg_pipe_menu, socket_path), + application:set_env(xdg_pipe_menu, socket_path, SockPath), + with_env( + [{"XDG_DATA_HOME", Root}, {"XDG_DATA_DIRS", "/does-not-exist-eunit"}], + fun() -> + {ok, CachePid} = xdg_pipe_menu_cache:start_link(), + {ok, SocketPid} = xdg_pipe_menu_socket:start_link(), + + {ok, Conn} = gen_tcp:connect({local, SockPath}, 0, [binary, {active, false}], 1000), + Received = recv_all(Conn, []), + gen_tcp:close(Conn), + + Expected = xdg_pipe_menu_cache:get_menu(), + stop(SocketPid), + stop(CachePid), + ?assertEqual(Expected, Received), + ?assert(binary:match(Received, <<"Foo">>) =/= nomatch) + end + ), + _ = file:delete(SockPath), + ok = rm_rf(Root), + restore_env(xdg_pipe_menu, socket_path, PrevSockPath). + +%% -- 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). + +recv_all(Socket, Acc) -> + case gen_tcp:recv(Socket, 0, 1000) of + {ok, Data} -> recv_all(Socket, [Data | Acc]); + {error, closed} -> iolist_to_binary(lists:reverse(Acc)) + end. + +restore_env(App, Key, undefined) -> application:unset_env(App, Key); +restore_env(App, Key, {ok, Value}) -> application:set_env(App, Key, Value). + +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"]) + ). + +mk_tmp_dir() -> + Path = filename:join( + test_tmp_dir(), "eunit-socket-dir-" ++ 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. diff --git a/test/xdg_pipe_menu_watcher_tests.erl b/test/xdg_pipe_menu_watcher_tests.erl new file mode 100644 index 0000000..cca5cc2 --- /dev/null +++ b/test/xdg_pipe_menu_watcher_tests.erl @@ -0,0 +1,86 @@ +-module(xdg_pipe_menu_watcher_tests). + +-include_lib("eunit/include/eunit.hrl"). + +%% Exercises the real debounce-timer-reset-then-invalidate wiring by +%% sending the watcher a synthetic message of the exact shape `fs' +%% delivers, rather than waiting on a real inotify event -- `fs' +%% itself is treated as out of scope for this test. +debounced_event_invalidates_cache_test() -> + Root = mk_tmp_dir(), + AppDir = filename:join(Root, "applications"), + ok = file:make_dir(AppDir), + write_desktop(AppDir, "foo.desktop", "Foo", "foo"), + PrevDebounce = application:get_env(xdg_pipe_menu, debounce_ms), + application:set_env(xdg_pipe_menu, debounce_ms, 20), + with_env( + [{"XDG_DATA_HOME", Root}, {"XDG_DATA_DIRS", "/does-not-exist-eunit"}], + fun() -> + {ok, CachePid} = xdg_pipe_menu_cache:start_link(), + {ok, WatcherPid} = xdg_pipe_menu_watcher:start_link(), + + write_desktop(AppDir, "bar.desktop", "Bar", "bar"), + WatcherPid ! {self(), {fs, file_event}, {"dummy", [modified]}}, + timer:sleep(100), + + Menu = xdg_pipe_menu_cache:get_menu(), + stop(WatcherPid), + stop(CachePid), + ?assert(binary:match(Menu, <<"Bar">>) =/= nomatch) + end + ), + ok = rm_rf(Root), + restore_env(xdg_pipe_menu, debounce_ms, PrevDebounce). + +%% -- 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). + +restore_env(App, Key, undefined) -> application:unset_env(App, Key); +restore_env(App, Key, {ok, Value}) -> application:set_env(App, Key, Value). + +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"]) + ). + +mk_tmp_dir() -> + Path = filename:join( + test_tmp_dir(), "eunit-watcher-" ++ 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