| 1 | //go:build !darwin && !windows |
| 2 | |
| 3 | package main |
| 4 | |
| 5 | import ( |
| 6 | "errors" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "reflect" |
| 10 | "testing" |
| 11 | ) |
| 12 | |
| 13 | func writeLinuxDesktopFile(t *testing.T, dir, name, content string) string { |
| 14 | t.Helper() |
| 15 | path := filepath.Join(dir, name) |
| 16 | if err := os.WriteFile(path, []byte(content), 0o600); err != nil { |
| 17 | t.Fatal(err) |
| 18 | } |
| 19 | return path |
| 20 | } |
| 21 | |
| 22 | func TestParseLinuxDesktopEntryReadsDesktopEntrySectionOnly(t *testing.T) { |
| 23 | dir := t.TempDir() |
| 24 | path := writeLinuxDesktopFile(t, dir, "code.desktop", `[Desktop Entry] |
| 25 | Type=Application |
| 26 | Name=Visual Studio Code |
| 27 | Icon=vscode |
| 28 | |
| 29 | [Desktop Action new-window] |
| 30 | Name=New Window |
| 31 | Icon=new-window |
| 32 | `) |
| 33 | entry, ok := parseLinuxDesktopEntry(path) |
| 34 | if !ok { |
| 35 | t.Fatal("valid desktop entry was rejected") |
| 36 | } |
| 37 | want := linuxDesktopEntry{Path: path, ID: "code", Name: "Visual Studio Code", Icon: "vscode"} |
| 38 | if entry != want { |
| 39 | t.Fatalf("parseLinuxDesktopEntry = %+v, want %+v", entry, want) |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | func TestParseLinuxDesktopEntryRejectsHiddenAndNonApplications(t *testing.T) { |
| 44 | dir := t.TempDir() |
| 45 | for name, content := range map[string]string{ |
| 46 | "hidden.desktop": "[Desktop Entry]\nType=Application\nName=Ghost\nHidden=true\n", |
| 47 | "link.desktop": "[Desktop Entry]\nType=Link\nName=Bookmark\n", |
| 48 | "unnamed.desktop": "[Desktop Entry]\nType=Application\n", |
| 49 | "wrong-section.desktop": "[Desktop Action open]\nName=Open\n", |
| 50 | } { |
| 51 | path := writeLinuxDesktopFile(t, dir, name, content) |
| 52 | if entry, ok := parseLinuxDesktopEntry(path); ok { |
| 53 | t.Errorf("%s was accepted as %+v, want rejection", name, entry) |
| 54 | } |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | func TestFindLinuxDesktopEntryMatchesNormalizedAliasesThenSubstrings(t *testing.T) { |
| 59 | entries := []linuxDesktopEntry{ |
| 60 | {ID: "org.gnome.Terminal", Name: "Terminal"}, |
| 61 | {ID: "code", Name: "Visual Studio Code"}, |
| 62 | {ID: "jetbrains-idea", Name: "IntelliJ IDEA Ultimate Edition"}, |
| 63 | {ID: "kitty", Name: "kitty"}, |
| 64 | } |
| 65 | if entry, ok := findLinuxDesktopEntry(entries, "org.gnome.terminal"); !ok || entry.ID != "org.gnome.Terminal" { |
| 66 | t.Fatalf("normalized id lookup = (%+v, %v), want org.gnome.Terminal", entry, ok) |
| 67 | } |
| 68 | if entry, ok := findLinuxDesktopEntry(entries, "visual-studio-code"); !ok || entry.ID != "code" { |
| 69 | t.Fatalf("normalized name lookup = (%+v, %v), want code", entry, ok) |
| 70 | } |
| 71 | if entry, ok := findLinuxDesktopEntry(entries, "intellij-idea"); !ok || entry.ID != "jetbrains-idea" { |
| 72 | t.Fatalf("substring lookup = (%+v, %v), want jetbrains-idea", entry, ok) |
| 73 | } |
| 74 | if entry, ok := findLinuxDesktopEntry(entries, "kit"); ok { |
| 75 | t.Fatalf("short alias %q unexpectedly matched %+v via substring", "kit", entry) |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | func TestLinuxExternalOpenerCommandPerTerminalArguments(t *testing.T) { |
| 80 | const workdir = "/tmp/reasonix workspace" |
| 81 | cases := []struct { |
| 82 | mode string |
| 83 | target string |
| 84 | want []string |
| 85 | }{ |
| 86 | {"path", "/usr/bin/code", []string{"/usr/bin/code", workdir}}, |
| 87 | {"ghostty", "/usr/bin/ghostty", []string{"/usr/bin/ghostty", "--working-directory=" + workdir}}, |
| 88 | {"gnome-terminal", "/usr/bin/gnome-terminal", []string{"/usr/bin/gnome-terminal", "--working-directory=" + workdir}}, |
| 89 | {"konsole", "/usr/bin/konsole", []string{"/usr/bin/konsole", "--workdir", workdir}}, |
| 90 | {"kitty", "/usr/bin/kitty", []string{"/usr/bin/kitty", "--directory", workdir}}, |
| 91 | {"alacritty", "/usr/bin/alacritty", []string{"/usr/bin/alacritty", "--working-directory", workdir}}, |
| 92 | {"cwd", "/usr/bin/x-terminal-emulator", []string{"/usr/bin/x-terminal-emulator"}}, |
| 93 | } |
| 94 | for _, tc := range cases { |
| 95 | spec := externalOpenerSpec{Target: tc.target, LaunchMode: tc.mode} |
| 96 | cmd, err := linuxExternalOpenerCommand(spec, workdir) |
| 97 | if err != nil { |
| 98 | t.Errorf("%s: linuxExternalOpenerCommand error = %v", tc.mode, err) |
| 99 | continue |
| 100 | } |
| 101 | if !reflect.DeepEqual(cmd.Args, tc.want) { |
| 102 | t.Errorf("%s: args = %#v, want %#v", tc.mode, cmd.Args, tc.want) |
| 103 | } |
| 104 | if cmd.Dir != workdir { |
| 105 | t.Errorf("%s: dir = %q, want workspace directory", tc.mode, cmd.Dir) |
| 106 | } |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | func TestLinuxExternalOpenerCommandLaunchesDesktopEntriesViaGio(t *testing.T) { |
| 111 | binDir := t.TempDir() |
| 112 | gio := filepath.Join(binDir, "gio") |
| 113 | if err := os.WriteFile(gio, []byte("#!/bin/sh\n"), 0o755); err != nil { |
| 114 | t.Fatal(err) |
| 115 | } |
| 116 | t.Setenv("PATH", binDir) |
| 117 | |
| 118 | spec := externalOpenerSpec{Target: "/usr/share/applications/code.desktop", LaunchMode: "gio"} |
| 119 | cmd, err := linuxExternalOpenerCommand(spec, "/tmp/project") |
| 120 | if err != nil { |
| 121 | t.Fatalf("linuxExternalOpenerCommand error = %v", err) |
| 122 | } |
| 123 | want := []string{gio, "launch", "/usr/share/applications/code.desktop", "/tmp/project"} |
| 124 | if !reflect.DeepEqual(cmd.Args, want) { |
| 125 | t.Fatalf("gio args = %#v, want %#v", cmd.Args, want) |
| 126 | } |
| 127 | |
| 128 | t.Setenv("PATH", t.TempDir()) |
| 129 | if _, err := linuxExternalOpenerCommand(spec, "/tmp/project"); !errors.Is(err, os.ErrNotExist) { |
| 130 | t.Fatalf("missing gio error = %v, want os.ErrNotExist", err) |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | func TestResolveLinuxDesktopIconAcceptsOnlyExistingAbsoluteFiles(t *testing.T) { |
| 135 | dir := t.TempDir() |
| 136 | icon := filepath.Join(dir, "vscode.png") |
| 137 | if err := os.WriteFile(icon, []byte("png"), 0o600); err != nil { |
| 138 | t.Fatal(err) |
| 139 | } |
| 140 | if got := resolveLinuxDesktopIcon(icon); got != icon { |
| 141 | t.Fatalf("existing absolute icon = %q, want %q", got, icon) |
| 142 | } |
| 143 | if got := resolveLinuxDesktopIcon(filepath.Join(dir, "missing.png")); got != "" { |
| 144 | t.Fatalf("missing absolute icon = %q, want empty", got) |
| 145 | } |
| 146 | if got := resolveLinuxDesktopIcon(dir); got != "" { |
| 147 | t.Fatalf("directory icon = %q, want empty", got) |
| 148 | } |
| 149 | } |
| 150 |