Skip to content

Commit f1e756a

Browse files
test: add unit tests for headless file dialog mocks and stubs
1 parent f680f16 commit f1e756a

3 files changed

Lines changed: 286 additions & 152 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,7 @@ else() # NOT EMSCRIPTEN, ANDROID, or IOS
790790
tests/ui/test_screen.cpp
791791
tests/ui/test_pedal_board_chain.cpp
792792
tests/ui/test_pedal_board_menu.cpp
793+
tests/ui/test_file_dialog.cpp
793794
tests/ui/test_pedal_widget_knobs.cpp
794795
tests/unit/test_session_manager.cpp
795796
tests/unit/test_cli.cpp

src/gui/dialogs/file_dialog_native_open.cpp

Lines changed: 144 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -7,208 +7,200 @@
77

88
#include "gui/dialogs/file_dialog.h"
99

10-
#ifdef _WIN32
1110
// clang-format off
11+
#ifdef _WIN32
1212
#define WIN32_LEAN_AND_MEAN
1313
#include <windows.h>
1414
#include <commdlg.h>
1515
#endif
1616

1717
#ifdef __APPLE__
1818
#include <TargetConditionals.h>
19-
#include <cstdio>
2019
#include <fcntl.h>
2120
#include <sys/wait.h>
2221
#include <unistd.h>
22+
23+
#include <cstdio>
2324
#endif
2425

2526
#ifndef _WIN32
2627
#include <sys/wait.h>
2728
#endif
29+
// clang-format on
2830

2931
namespace Amplitron {
3032

3133
#ifdef AMPLITRON_HEADLESS
3234
static std::string s_mock_open_path = "";
3335

34-
void set_mock_open_dialog_path(const std::string &path) {
35-
s_mock_open_path = path;
36-
}
36+
void set_mock_open_dialog_path(const std::string &path) { s_mock_open_path = path; }
3737

38-
std::string show_open_dialog(const std::string &, const std::string &,
39-
const std::string &) {
40-
return s_mock_open_path;
38+
std::string show_open_dialog(const std::string &, const std::string &, const std::string &) {
39+
return s_mock_open_path;
4140
}
4241
#else
4342

43+
// clang-format off
4444
#ifdef _WIN32
45-
std::string show_open_dialog(const std::string &title,
46-
const std::string &filter_desc,
47-
const std::string &filter_ext) {
48-
char filename[MAX_PATH] = "";
49-
50-
char filter[256];
51-
std::memset(filter, 0, sizeof(filter));
52-
int pos = 0;
53-
pos += snprintf(filter + pos, 256 - pos, "%s (*.%s)", filter_desc.c_str(),
54-
filter_ext.c_str());
55-
pos++;
56-
pos += snprintf(filter + pos, 256 - pos, "*.%s", filter_ext.c_str());
57-
pos++;
58-
pos += snprintf(filter + pos, 256 - pos, "All Files (*.*)");
59-
pos++;
60-
pos += snprintf(filter + pos, 256 - pos, "*.*");
61-
62-
OPENFILENAMEA ofn;
63-
std::memset(&ofn, 0, sizeof(ofn));
64-
ofn.lStructSize = sizeof(ofn);
65-
ofn.hwndOwner = NULL;
66-
ofn.lpstrFilter = filter;
67-
ofn.lpstrFile = filename;
68-
ofn.nMaxFile = MAX_PATH;
69-
ofn.lpstrTitle = title.c_str();
70-
ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
71-
72-
if (GetOpenFileNameA(&ofn)) {
73-
return std::string(filename);
74-
}
75-
return "";
45+
std::string show_open_dialog(const std::string& title, const std::string& filter_desc,
46+
const std::string& filter_ext) {
47+
char filename[MAX_PATH] = "";
48+
49+
char filter[256];
50+
std::memset(filter, 0, sizeof(filter));
51+
int pos = 0;
52+
pos += snprintf(filter + pos, 256 - pos, "%s (*.%s)", filter_desc.c_str(), filter_ext.c_str());
53+
pos++;
54+
pos += snprintf(filter + pos, 256 - pos, "*.%s", filter_ext.c_str());
55+
pos++;
56+
pos += snprintf(filter + pos, 256 - pos, "All Files (*.*)");
57+
pos++;
58+
pos += snprintf(filter + pos, 256 - pos, "*.*");
59+
60+
OPENFILENAMEA ofn;
61+
std::memset(&ofn, 0, sizeof(ofn));
62+
ofn.lStructSize = sizeof(ofn);
63+
ofn.hwndOwner = NULL;
64+
ofn.lpstrFilter = filter;
65+
ofn.lpstrFile = filename;
66+
ofn.nMaxFile = MAX_PATH;
67+
ofn.lpstrTitle = title.c_str();
68+
ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
69+
70+
if (GetOpenFileNameA(&ofn)) {
71+
return std::string(filename);
72+
}
73+
return "";
7674
}
7775

7876
#elif defined(__APPLE__) && !TARGET_OS_IOS
79-
std::string show_open_dialog(const std::string &title,
80-
const std::string & /*filter_desc*/,
81-
const std::string &filter_ext) {
82-
// Sanitize title and filter_ext for AppleScript
83-
std::string safe_title;
84-
for (char c : title) {
85-
if (c == '\\') {
86-
safe_title += "\\\\";
87-
} else if (c == '"') {
88-
safe_title += "\\\"";
89-
} else {
90-
safe_title += c;
77+
std::string show_open_dialog(const std::string& title, const std::string& /*filter_desc*/,
78+
const std::string& filter_ext) {
79+
// Sanitize title and filter_ext for AppleScript
80+
std::string safe_title;
81+
for (char c : title) {
82+
if (c == '\\') {
83+
safe_title += "\\\\";
84+
} else if (c == '"') {
85+
safe_title += "\\\"";
86+
} else {
87+
safe_title += c;
88+
}
9189
}
92-
}
93-
94-
std::string safe_ext;
95-
for (char c : filter_ext) {
96-
if (c == '\\') {
97-
safe_ext += "\\\\";
98-
} else if (c == '"') {
99-
safe_ext += "\\\"";
100-
} else {
101-
safe_ext += c;
90+
91+
std::string safe_ext;
92+
for (char c : filter_ext) {
93+
if (c == '\\') {
94+
safe_ext += "\\\\";
95+
} else if (c == '"') {
96+
safe_ext += "\\\"";
97+
} else {
98+
safe_ext += c;
99+
}
102100
}
103-
}
104101

105-
std::string script = "POSIX path of (choose file of type {\"" + safe_ext +
106-
"\"} with prompt \"" + safe_title + "\")";
102+
std::string script = "POSIX path of (choose file of type {\"" + safe_ext +
103+
"\"} with prompt \"" + safe_title + "\")";
107104

108-
// Use fork+exec to invoke osascript directly
109-
int pipefd[2];
110-
if (pipe(pipefd) != 0)
111-
return "";
105+
// Use fork+exec to invoke osascript directly
106+
int pipefd[2];
107+
if (pipe(pipefd) != 0) return "";
112108

113-
pid_t pid = fork();
114-
if (pid < 0) {
115-
close(pipefd[0]);
116-
close(pipefd[1]);
117-
return "";
118-
}
109+
pid_t pid = fork();
110+
if (pid < 0) {
111+
close(pipefd[0]);
112+
close(pipefd[1]);
113+
return "";
114+
}
119115

120-
if (pid == 0) {
121-
close(pipefd[0]);
122-
dup2(pipefd[1], STDOUT_FILENO);
123-
close(pipefd[1]);
124-
int devnull = open("/dev/null", O_WRONLY);
125-
if (devnull >= 0) {
126-
dup2(devnull, STDERR_FILENO);
127-
close(devnull);
116+
if (pid == 0) {
117+
close(pipefd[0]);
118+
dup2(pipefd[1], STDOUT_FILENO);
119+
close(pipefd[1]);
120+
int devnull = open("/dev/null", O_WRONLY);
121+
if (devnull >= 0) {
122+
dup2(devnull, STDERR_FILENO);
123+
close(devnull);
124+
}
125+
execl("/usr/bin/osascript", "osascript", "-e", script.c_str(), nullptr);
126+
_exit(1);
128127
}
129-
execl("/usr/bin/osascript", "osascript", "-e", script.c_str(), nullptr);
130-
_exit(1);
131-
}
132128

133-
close(pipefd[1]);
134-
char buf[1024];
135-
std::string result;
136-
ssize_t n;
137-
while ((n = read(pipefd[0], buf, sizeof(buf))) > 0)
138-
result.append(buf, static_cast<size_t>(n));
139-
close(pipefd[0]);
129+
close(pipefd[1]);
130+
char buf[1024];
131+
std::string result;
132+
ssize_t n;
133+
while ((n = read(pipefd[0], buf, sizeof(buf))) > 0) result.append(buf, static_cast<size_t>(n));
134+
close(pipefd[0]);
140135

141-
int status = 0;
142-
waitpid(pid, &status, 0);
136+
int status = 0;
137+
waitpid(pid, &status, 0);
143138

144-
while (!result.empty() && (result.back() == '\n' || result.back() == '\r'))
145-
result.pop_back();
139+
while (!result.empty() && (result.back() == '\n' || result.back() == '\r')) result.pop_back();
146140

147-
return result;
141+
return result;
148142
}
149143

150-
#else // Linux
151-
std::string show_open_dialog(const std::string &title,
152-
const std::string &filter_desc,
153-
const std::string &filter_ext) {
154-
// Escape single quotes for shell
155-
auto escape_single_quotes = [](const std::string &s) {
144+
#else // Linux
145+
std::string show_open_dialog(const std::string& title, const std::string& filter_desc,
146+
const std::string& filter_ext) {
147+
// Escape single quotes for shell
148+
auto escape_single_quotes = [](const std::string& s) {
149+
std::string result;
150+
for (char c : s) {
151+
if (c == '\'') {
152+
result += "'\\''";
153+
} else {
154+
result += c;
155+
}
156+
}
157+
return result;
158+
};
159+
160+
std::string safe_title = escape_single_quotes(title);
161+
std::string safe_desc = escape_single_quotes(filter_desc);
162+
std::string safe_ext = escape_single_quotes(filter_ext);
163+
164+
std::string cmd =
165+
"zenity --file-selection "
166+
"--title='" +
167+
safe_title +
168+
"' "
169+
"--file-filter='" +
170+
safe_desc + " (*." + safe_ext + ")|*." + safe_ext +
171+
"' "
172+
"--file-filter='All Files (*)|*' 2>/dev/null";
173+
174+
FILE* pipe = popen(cmd.c_str(), "r");
175+
if (!pipe) return "";
176+
177+
char buf[1024];
156178
std::string result;
157-
for (char c : s) {
158-
if (c == '\'') {
159-
result += "'\\''";
160-
} else {
161-
result += c;
162-
}
163-
}
164-
return result;
165-
};
166-
167-
std::string safe_title = escape_single_quotes(title);
168-
std::string safe_desc = escape_single_quotes(filter_desc);
169-
std::string safe_ext = escape_single_quotes(filter_ext);
170-
171-
std::string cmd = "zenity --file-selection "
172-
"--title='" +
173-
safe_title +
174-
"' "
175-
"--file-filter='" +
176-
safe_desc + " (*." + safe_ext + ")|*." + safe_ext +
177-
"' "
178-
"--file-filter='All Files (*)|*' 2>/dev/null";
179-
180-
FILE *pipe = popen(cmd.c_str(), "r");
181-
if (!pipe)
182-
return "";
183-
184-
char buf[1024];
185-
std::string result;
186-
while (fgets(buf, sizeof(buf), pipe)) {
187-
result += buf;
188-
}
189-
int wait_status = pclose(pipe);
190-
191-
if (WIFEXITED(wait_status) && WEXITSTATUS(wait_status) != 0) {
192-
cmd = "kdialog --getopenfilename ~/ '*." + safe_ext + "|" + safe_desc +
193-
"' "
194-
"--title '" +
195-
safe_title + "' 2>/dev/null";
196-
pipe = popen(cmd.c_str(), "r");
197-
if (!pipe)
198-
return "";
199-
result.clear();
200179
while (fgets(buf, sizeof(buf), pipe)) {
201-
result += buf;
180+
result += buf;
181+
}
182+
int wait_status = pclose(pipe);
183+
184+
if (WIFEXITED(wait_status) && WEXITSTATUS(wait_status) != 0) {
185+
cmd = "kdialog --getopenfilename ~/ '*." + safe_ext + "|" + safe_desc +
186+
"' "
187+
"--title '" +
188+
safe_title + "' 2>/dev/null";
189+
pipe = popen(cmd.c_str(), "r");
190+
if (!pipe) return "";
191+
result.clear();
192+
while (fgets(buf, sizeof(buf), pipe)) {
193+
result += buf;
194+
}
195+
pclose(pipe);
202196
}
203-
pclose(pipe);
204-
}
205197

206-
while (!result.empty() && (result.back() == '\n' || result.back() == '\r'))
207-
result.pop_back();
198+
while (!result.empty() && (result.back() == '\n' || result.back() == '\r')) result.pop_back();
208199

209-
return result;
200+
return result;
210201
}
211202
#endif
203+
// clang-format on
212204

213205
#endif // AMPLITRON_HEADLESS
214206

0 commit comments

Comments
 (0)