dte test coverage


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 50.0% high: ≥ 85.0%
Coverage Exec / Excl / Total
Lines: 41.9% 103 / 2 / 248
Functions: 61.5% 8 / 0 / 13
Branches: 31.2% 43 / 18 / 156

src/exec.c
Line Branch Exec Source
1 #include <stdint.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <unistd.h>
5 #include "exec.h"
6 #include "block-iter.h"
7 #include "buffer.h"
8 #include "change.h"
9 #include "command/macro.h"
10 #include "commands.h"
11 #include "ctags.h"
12 #include "editor.h"
13 #include "move.h"
14 #include "msg.h"
15 #include "selection.h"
16 #include "spawn.h"
17 #include "tag.h"
18 #include "terminal/mode.h"
19 #include "util/bsearch.h"
20 #include "util/debug.h"
21 #include "util/str-util.h"
22 #include "util/string.h"
23 #include "util/strtonum.h"
24 #include "util/xsnprintf.h"
25 #include "view.h"
26 #include "window.h"
27
28 enum {
29 IN = 1 << 0,
30 OUT = 1 << 1,
31 ERR = 1 << 2,
32 ALL = IN | OUT | ERR,
33 };
34
35 static const struct {
36 char name[11];
37 uint8_t flags;
38 } exec_map[] = {
39 [EXEC_BUFFER] = {"buffer", IN | OUT},
40 [EXEC_COMMAND] = {"command", IN},
41 [EXEC_ECHO] = {"echo", OUT},
42 [EXEC_ERRMSG] = {"errmsg", ERR},
43 [EXEC_EVAL] = {"eval", OUT},
44 [EXEC_LINE] = {"line", IN},
45 [EXEC_MSG] = {"msg", IN | OUT},
46 [EXEC_MSG_A] = {"msgA", IN | OUT},
47 [EXEC_MSG_B] = {"msgB", IN | OUT},
48 [EXEC_MSG_C] = {"msgC", IN | OUT},
49 [EXEC_NULL] = {"null", ALL},
50 [EXEC_OPEN] = {"open", IN | OUT},
51 [EXEC_OPEN_REL] = {"open-rel", IN},
52 [EXEC_SEARCH] = {"search", IN},
53 [EXEC_TAG] = {"tag", OUT},
54 [EXEC_TAG_A] = {"tagA", OUT},
55 [EXEC_TAG_B] = {"tagB", OUT},
56 [EXEC_TAG_C] = {"tagC", OUT},
57 [EXEC_TTY] = {"tty", ALL},
58 [EXEC_WORD] = {"word", IN},
59 };
60
61 24 UNITTEST {
62 24 CHECK_BSEARCH_ARRAY(exec_map, name);
63 24 }
64
65 18 ExecAction lookup_exec_action(const char *name, int fd)
66 {
67 18 BUG_ON(fd < 0 || fd > 2);
68 18 ssize_t i = BSEARCH_IDX(name, exec_map, vstrcmp);
69
3/4
✓ Branch 5 → 6 taken 15 times.
✓ Branch 5 → 8 taken 3 times.
✓ Branch 6 → 7 taken 15 times.
✗ Branch 6 → 8 not taken.
18 return (i >= 0 && (exec_map[i].flags & 1u << fd)) ? i : EXEC_INVALID;
70 }
71
72 1 void collect_exec_actions(PointerArray *a, StringView prefix, int fd)
73 {
74
1/2
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 10 not taken.
1 if (unlikely(fd < 0 || fd > 2)) {
75 return;
76 }
77
78 1 unsigned int flag = 1u << fd;
79
2/2
✓ Branch 9 → 4 taken 20 times.
✓ Branch 9 → 10 taken 1 time.
21 for (size_t i = 0; i < ARRAYLEN(exec_map); i++) {
80 20 const char *action = exec_map[i].name;
81
3/4
✓ Branch 4 → 5 taken 14 times.
✓ Branch 4 → 8 taken 6 times.
✓ Branch 5 → 6 taken 14 times.
✗ Branch 5 → 8 not taken.
20 if ((exec_map[i].flags & flag) && str_has_sv_prefix(action, prefix)) {
82 14 ptr_array_append(a, xstrdup(action));
83 }
84 }
85 }
86
87 static void open_files_from_string(EditorState *e, const String *str)
88 {
89 PointerArray filenames = PTR_ARRAY_INIT;
90 for (size_t pos = 0, size = str->len; pos < size; ) {
91 char *filename = buf_next_line(str->buffer, &pos, size); // Mutates `str->buffer`
92 if (filename[0] != '\0') {
93 ptr_array_append(&filenames, filename);
94 }
95 }
96
97 if (filenames.count == 0) {
98 return;
99 }
100
101 ptr_array_append(&filenames, NULL);
102 window_open_files(e->window, (char**)filenames.ptrs, NULL);
103
104 // TODO: re-enable this when the todo in allow_macro_recording() is done
105 // macro_command_hook(&e->macro, "open", (char**)filenames.ptrs);
106
107 ptr_array_free_array(&filenames);
108 }
109
110 static void parse_and_activate_message(EditorState *e, StringView str, ExecAction action)
111 {
112 if (unlikely(str.length == 0)) {
113 error_msg(&e->err, "child produced no output");
114 return;
115 }
116
117 size_t msgs_idx = (action == EXEC_MSG) ? 0 : action - EXEC_MSG_A;
118 BUG_ON(msgs_idx >= ARRAYLEN(e->messages));
119 MessageList *msgs = &e->messages[msgs_idx];
120 size_t count = msgs->array.count;
121 size_t x;
122
123 if (!count || !buf_parse_size(str, &x) || !x) {
124 return;
125 }
126
127 msgs->pos = MIN(x - 1, count - 1);
128 activate_current_message(msgs, e->window, &e->err);
129 }
130
131 static void parse_and_activate_tags(EditorState *e, StringView str, ExecAction action)
132 {
133 ErrorBuffer *ebuf = &e->err;
134 if (unlikely(str.length == 0)) {
135 error_msg(ebuf, "child produced no output");
136 return;
137 }
138
139 char cwd[8192];
140 if (unlikely(!getcwd(cwd, sizeof cwd))) {
141 error_msg_errno(ebuf, "getcwd() failed");
142 return;
143 }
144
145 const StringView dir = strview(cwd);
146 const char *buffer_filename = e->buffer->abs_filename;
147 TagFile *tf = &e->tagfile;
148 enum {NOT_LOADED, LOADED, FAILED} tf_status = NOT_LOADED;
149
150 size_t msgs_idx = (action == EXEC_TAG) ? e->options.msg_tag : action - EXEC_TAG_A;
151 BUG_ON(msgs_idx >= ARRAYLEN(e->messages));
152 MessageList *msgs = &e->messages[msgs_idx];
153 clear_messages(msgs);
154
155 for (size_t pos = 0; pos < str.length; ) {
156 Tag tag;
157 StringView line = buf_slice_next_line(str.data, &pos, str.length);
158 if (line.length == 0) {
159 continue;
160 }
161
162 bool parsed = parse_ctags_line(&tag, line);
163 if (parsed) {
164 // `line` is a valid tags(5) file entry; handle it directly
165 add_message_for_tag(msgs, &tag, dir);
166 continue;
167 }
168
169 // Treat `line` as a simple tag name (look it up in the tags(5) file)
170 switch (tf_status) {
171 case NOT_LOADED:
172 tf_status = load_tag_file(tf, ebuf) ? LOADED : FAILED;
173 if (tf_status == FAILED) {
174 continue;
175 }
176 // Fallthrough
177 case LOADED:
178 tag_lookup(tf, msgs, ebuf, line, buffer_filename);
179 continue;
180 case FAILED:
181 continue;
182 }
183 }
184
185 activate_current_message_save(msgs, &e->bookmarks, e->view, ebuf);
186 }
187
188 static void insert_to_selection (
189 View *view,
190 StringView text,
191 const SelectionInfo *info
192 ) {
193 size_t del_count = info->eo - info->so;
194 buffer_replace_bytes(view, del_count, text);
195
196 if (text.length == 0) {
197 // If the selection was replaced with 0 bytes then there's nothing
198 // new to select, so just unselect instead
199 unselect(view);
200 return;
201 }
202
203 // Keep the selection and adjust the size to the newly inserted text
204 size_t so = info->so;
205 size_t eo = so + (text.length - 1);
206 block_iter_goto_offset(&view->cursor, info->swapped ? so : eo);
207 view->sel_so = info->swapped ? eo : so;
208 view->sel_eo = SEL_EO_RECALC;
209 }
210
211 4 static void show_spawn_error_msg(ErrorBuffer *ebuf, StringView errstr, int err)
212 {
213
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 3 times.
4 if (err <= 0) {
214 // spawn() returned an error code instead of an exit code, which
215 // indicates that something failed before (or during) the child
216 // process exec(3p), or that an error occurred in wait_child().
217 // In this case, error_msg() has already been called.
218 1 return;
219 }
220
221 3 char msg[512];
222 3 msg[0] = '\0';
223
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 10 taken 2 times.
3 if (errstr.length) {
224 1 size_t pos = 0;
225 1 StringView line = buf_slice_next_line(errstr.data, &pos, errstr.length);
226 1 BUG_ON(pos == 0);
227 1 size_t len = MIN(line.length, sizeof(msg) - 8);
228 1 xsnprintf(msg, sizeof(msg), ": \"%.*s\"", (int)len, line.data);
229 }
230
231
2/2
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 15 taken 2 times.
3 if (err >= 256) {
232 1 int sig = err >> 8;
233 1 const char *str = strsignal(sig);
234
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 1 time.
1 error_msg(ebuf, "Child received signal %d (%s)%s", sig, str ? str : "??", msg);
235 2 } else if (err) {
236 2 error_msg(ebuf, "Child returned %d%s", err, msg);
237 }
238 }
239
240 static size_t msgs_idx_from_exec_action(ExecAction action)
241 {
242 BUG_ON(action < EXEC_MSG || action > EXEC_MSG_C);
243 return (action == EXEC_MSG) ? 0 : action - EXEC_MSG_A;
244 }
245
246 45 static SpawnAction spawn_action_from_exec_action(ExecAction action)
247 {
248 45 BUG_ON(action == EXEC_INVALID);
249
1/2
✓ Branch 4 → 5 taken 45 times.
✗ Branch 4 → 7 not taken.
45 if (action == EXEC_NULL) {
250 return SPAWN_NULL;
251
2/2
✓ Branch 5 → 6 taken 15 times.
✓ Branch 5 → 7 taken 30 times.
45 } else if (action == EXEC_TTY) {
252 return SPAWN_TTY;
253 } else {
254 15 return SPAWN_PIPE;
255 }
256 }
257
258 // NOLINTNEXTLINE(readability-function-size)
259 15 ssize_t handle_exec (
260 EditorState *e,
261 const char **argv,
262 ExecAction actions[3],
263 ExecFlags exec_flags
264 ) {
265 15 View *view = e->view;
266 15 const BlockIter saved_cursor = view->cursor;
267 15 const ssize_t saved_sel_so = view->sel_so;
268 15 const ssize_t saved_sel_eo = view->sel_eo;
269 15 String input = STRING_INIT;
270 15 size_t input_from_buffer_length = 0;
271 15 bool input_from_buffer = false;
272 15 bool output_to_buffer = (actions[STDOUT_FILENO] == EXEC_BUFFER);
273 15 bool quiet = (exec_flags & EXECFLAG_QUIET);
274
275 80 SpawnContext ctx = {
276 .argv = argv,
277 .outputs = {STRING_INIT, STRING_INIT},
278 .quiet = quiet,
279 15 .ebuf = &e->err,
280
2/2
✓ Branch 5 → 6 taken 5 times.
✓ Branch 5 → 8 taken 10 times.
15 .lines = output_to_buffer ? view->window->edit_h : 0,
281
1/2
✓ Branch 6 → 7 taken 5 times.
✗ Branch 6 → 8 not taken.
5 .columns = output_to_buffer ? view->window->edit_w : 0,
282 .actions = {
283 15 spawn_action_from_exec_action(actions[0]),
284 15 spawn_action_from_exec_action(actions[1]),
285 15 spawn_action_from_exec_action(actions[2]),
286 },
287 };
288
289 15 ExecAction in_action = actions[STDIN_FILENO];
290
2/10
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 13 taken 3 times.
✗ Branch 8 → 18 not taken.
✗ Branch 8 → 26 not taken.
✗ Branch 8 → 28 not taken.
✗ Branch 8 → 30 not taken.
✗ Branch 8 → 32 not taken.
✗ Branch 8 → 34 not taken.
✗ Branch 8 → 36 not taken.
✓ Branch 8 → 38 taken 12 times.
15 switch (in_action) {
291 case EXEC_LINE:
292 input_from_buffer = true;
293 if (!view->selection) {
294 move_bol(view, BOL_SIMPLE);
295 input_from_buffer_length = block_iter_get_line(&view->cursor).length;
296 }
297 break;
298 3 case EXEC_BUFFER:
299 3 input_from_buffer = true;
300
1/2
✓ Branch 13 → 14 taken 3 times.
✗ Branch 13 → 38 not taken.
3 if (!view->selection) {
301 3 const Block *blk;
302
2/2
✓ Branch 16 → 15 taken 3 times.
✓ Branch 16 → 17 taken 3 times.
6 block_for_each(blk, &view->buffer->blocks) {
303 3 input_from_buffer_length += blk->size;
304 }
305 3 move_bof(view);
306 }
307 break;
308 case EXEC_WORD:
309 input_from_buffer = true;
310 if (!view->selection) {
311 CurrentLineRef lr = get_current_line_and_offset(view->cursor);
312 WordBounds wb = get_bounds_for_word_under_cursor(lr);
313 if (wb.end == 0) {
314 break;
315 }
316
317 // If `wb.start` is less than `lr.cursor_offset` here, the
318 // subtraction wraps but nevertheless works as intended.
319 // view->cursor.offset -= (lr.cursor_offset - wb.start) ==
320 view->cursor.offset += wb.start - lr.cursor_offset;
321
322 input_from_buffer_length = wb.end - wb.start;
323 BUG_ON(view->cursor.offset >= view->cursor.blk->size);
324 }
325 break;
326 case EXEC_MSG:
327 case EXEC_MSG_A:
328 case EXEC_MSG_B:
329 case EXEC_MSG_C:
330 input = dump_messages(&e->messages[msgs_idx_from_exec_action(in_action)]);
331 break;
332 case EXEC_COMMAND:
333 input = history_dump(&e->command_history);
334 break;
335 case EXEC_SEARCH:
336 input = history_dump(&e->search_history);
337 break;
338 case EXEC_OPEN:
339 input = file_history_dump(&e->file_history);
340 break;
341 case EXEC_OPEN_REL:
342 input = file_history_dump_relative(&e->file_history);
343 break;
344 case EXEC_NULL:
345 case EXEC_TTY:
346 break;
347 // These can't be used as input actions and should be prevented by
348 // the validity checks in cmd_exec():
349 case EXEC_TAG:
350 case EXEC_TAG_A:
351 case EXEC_TAG_B:
352 case EXEC_TAG_C:
353 case EXEC_ECHO:
354 case EXEC_EVAL:
355 case EXEC_ERRMSG:
356 case EXEC_INVALID:
357 default:
358 BUG("unhandled action");
359 return -1;
360 }
361
362 // This could be left uninitialized, but doing so makes some old compilers
363 // produce false-positive "-Wmaybe-uninitialized" warnings
364 15 SelectionInfo info = {.si = view->cursor};
365
366
1/4
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 43 taken 15 times.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 45 not taken.
15 if (view->selection && (input_from_buffer || output_to_buffer)) {
367 info = init_selection(view);
368 view->cursor = info.si;
369 if (input_from_buffer) {
370 input_from_buffer_length = info.eo - info.so;
371 }
372 }
373
374
2/2
✓ Branch 43 → 44 taken 3 times.
✓ Branch 43 → 45 taken 12 times.
15 if (input_from_buffer) {
375 3 input = block_iter_get_bytes(view->cursor, input_from_buffer_length);
376 } else {
377 12 BUG_ON(input_from_buffer_length);
378 }
379
380 15 ctx.input = strview_from_string(&input);
381 15 yield_terminal(e, quiet);
382 15 int err = spawn(&ctx);
383
3/4
✓ Branch 49 → 50 taken 14 times.
✓ Branch 49 → 51 taken 1 time.
✓ Branch 50 → 51 taken 14 times.
✗ Branch 50 → 52 not taken.
15 bool prompt = (err >= 0) && (exec_flags & EXECFLAG_PROMPT);
384 15 resume_terminal(e, quiet, prompt);
385 15 string_free(&input);
386 15 ctx.input.length = 0;
387
388
2/2
✓ Branch 54 → 55 taken 4 times.
✓ Branch 54 → 59 taken 11 times.
15 if (err != 0) {
389 4 show_spawn_error_msg(&e->err, strview_from_string(&ctx.outputs[1]), err);
390 4 string_free(&ctx.outputs[0]);
391 4 string_free(&ctx.outputs[1]);
392 4 view->cursor = saved_cursor;
393 4 return -1;
394 }
395
396 11 string_free(&ctx.outputs[1]);
397 11 String *output = &ctx.outputs[0];
398 11 bool strip_trailing_newline = (exec_flags & EXECFLAG_STRIP_NL);
399 11 if (
400 strip_trailing_newline
401
1/2
✗ Branch 60 → 61 not taken.
✓ Branch 60 → 66 taken 11 times.
11 && output_to_buffer
402 && output->len > 0
403 && output->buffer[output->len - 1] == '\n'
404 ) {
405 output->len--;
406 if (output->len > 0 && output->buffer[output->len - 1] == '\r') {
407 output->len--;
408 }
409 }
410
411
2/2
✓ Branch 66 → 67 taken 6 times.
✓ Branch 66 → 68 taken 5 times.
11 if (!output_to_buffer) {
412 6 view->cursor = saved_cursor;
413 6 view->sel_so = saved_sel_so;
414 6 view->sel_eo = saved_sel_eo;
415 6 mark_all_lines_changed(view->buffer);
416 }
417
418 11 ExecAction out_action = actions[STDOUT_FILENO];
419
2/8
✓ Branch 68 → 69 taken 5 times.
✗ Branch 68 → 72 not taken.
✗ Branch 68 → 76 not taken.
✗ Branch 68 → 77 not taken.
✗ Branch 68 → 79 not taken.
✗ Branch 68 → 81 not taken.
✗ Branch 68 → 83 not taken.
✓ Branch 68 → 85 taken 6 times.
11 switch (out_action) {
420 5 case EXEC_BUFFER:
421
1/2
✗ Branch 69 → 70 not taken.
✓ Branch 69 → 71 taken 5 times.
5 if (view->selection) {
422 insert_to_selection(view, strview_from_string(output), &info);
423 } else {
424 // Replace any unselected text used as input, e.g. for `-i line`
425 5 size_t del_count = input_from_buffer_length;
426 5 buffer_replace_bytes(view, del_count, strview_from_string(output));
427 }
428 break;
429 case EXEC_ECHO:
430 if (output->len) {
431 size_t pos = 0;
432 StringView line = buf_slice_next_line(output->buffer, &pos, output->len);
433 info_msg(&e->err, "%.*s", (int)line.length, line.data);
434 }
435 break;
436 case EXEC_MSG:
437 case EXEC_MSG_A:
438 case EXEC_MSG_B:
439 case EXEC_MSG_C:
440 parse_and_activate_message(e, strview_from_string(output), out_action);
441 break;
442 case EXEC_OPEN:
443 open_files_from_string(e, output);
444 break;
445 case EXEC_TAG:
446 case EXEC_TAG_A:
447 case EXEC_TAG_B:
448 case EXEC_TAG_C:
449 parse_and_activate_tags(e, strview_from_string(output), out_action);
450 break;
451 case EXEC_EVAL:
452 exec_normal_config(e, strview_from_string(output));
453 break;
454 case EXEC_NULL:
455 case EXEC_TTY:
456 break;
457 // These can't be used as output actions
458 case EXEC_COMMAND:
459 case EXEC_ERRMSG:
460 case EXEC_LINE:
461 case EXEC_OPEN_REL:
462 case EXEC_SEARCH:
463 case EXEC_WORD:
464 case EXEC_INVALID:
465 default:
466 BUG("unhandled action");
467 return -1;
468 }
469
470 11 size_t output_len = output->len;
471 11 string_free(output);
472 11 return output_len;
473 }
474
475 15 void yield_terminal(EditorState *e, bool quiet)
476 {
477
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 7 taken 15 times.
15 if (e->flags & EFLAG_HEADLESS) {
478 return;
479 }
480
481 if (quiet) {
482 term_raw_isig();
483 } else {
484 need_term_reset_on_fatal_error = 0;
485 ui_end(&e->terminal, false);
486 term_cooked();
487 }
488 }
489
490 15 void resume_terminal(EditorState *e, bool quiet, bool prompt)
491 {
492
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 11 taken 15 times.
15 if (e->flags & EFLAG_HEADLESS) {
493 return;
494 }
495
496 term_raw();
497 if (!quiet) {
498 BUG_ON(need_term_reset_on_fatal_error);
499 if (prompt) {
500 any_key(&e->terminal, e->options.esc_timeout);
501 }
502 ui_start(e);
503 need_term_reset_on_fatal_error = 1;
504 }
505 }
506