dte test coverage


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