From 5c9c8aa4244cd293b87528c985b6558e409c8e1d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 28 Apr 2025 09:25:25 +0530 Subject: [PATCH] Add unit testing for launcher code --- kitty/launcher/cli-parser.h | 46 +++++++++++++++++--- kitty/launcher/main.c | 87 ++++++++++++++++++++++++++++--------- kitty/main.py | 3 +- kitty_tests/options.py | 79 ++++++++++++++++++++++++++++++++- setup.py | 4 +- 5 files changed, 189 insertions(+), 30 deletions(-) diff --git a/kitty/launcher/cli-parser.h b/kitty/launcher/cli-parser.h index 6b22194bb..caa732e13 100644 --- a/kitty/launcher/cli-parser.h +++ b/kitty/launcher/cli-parser.h @@ -18,6 +18,7 @@ static inline void cleanup_decref2(PyObject **p) { Py_CLEAR(*p); } #define RAII_PyObject(name, initializer) __attribute__((cleanup(cleanup_decref2))) PyObject *name = initializer +#undef MAX #define MAX(x, y) __extension__ ({ \ const __typeof__ (x) __a__ = (x); const __typeof__ (y) __b__ = (y); \ __a__ > __b__ ? __a__ : __b__;}) @@ -105,8 +106,11 @@ alloc_for_cli(CLISpec *spec, size_t sz) { block.used = 0; } char *ans = block.buf + block.used; - block.used += sz; ans[sz-1] = 0; + block.used += sz; + // keep returned memory regions aligned to size of pointer + size_t extra = sz % sizeof(void*); + if (extra) block.used += sizeof(void*) - extra; return ans; #undef block } @@ -311,15 +315,47 @@ parse_cli_loop(CLISpec *spec, bool save_original_argv, int argc, char **argv) { } #ifdef FOR_LAUNCHER +static void +output_argv(const char *name, int argc, char **argv) { + printf("%s:", name); + for (int i = 0; i < argc; i++) printf("\x1e%s", argv[i]); + printf("\n"); +} + +static void +output_values_for_testing(CLISpec *spec) { + value_map_for_loop(&spec->value_map) { + printf("%s: ", itr.data->key); + CLIValue v = itr.data->val; + switch (v.type) { + case CLI_VALUE_STRING: case CLI_VALUE_CHOICE: + printf("%s", v.strval ? v.strval : ""); break; + case CLI_VALUE_BOOL: + printf("%d", v.boolval); break; + case CLI_VALUE_INT: + printf("%lld", v.intval); break; + case CLI_VALUE_FLOAT: + printf("%f", v.floatval); break; + case CLI_VALUE_LIST: + break; + } + printf("\n"); + } +} + +static void +output_for_testing(CLISpec *spec) { + output_argv("original_argv", spec->original_argc, spec->original_argv); + output_argv("argv", spec->argc, spec->argv); + output_values_for_testing(spec); +} + static CLIValue get_cli_val(CLISpec *spec, const char *name) { cli_hash_itr itr = vt_get(&spec->value_map, name); if (vt_is_end(itr)) { flag_hash_itr itr = vt_get(&spec->flag_map, name); - if (vt_is_end(itr)) { - fprintf(stderr, "Trying to get value for unknown option name: %s\n", name); - exit(1); - } + if (vt_is_end(itr)) return (CLIValue){0}; return itr.data->val.defval; } return itr.data->val; diff --git a/kitty/launcher/main.c b/kitty/launcher/main.c index ef279d123..543857efc 100644 --- a/kitty/launcher/main.c +++ b/kitty/launcher/main.c @@ -32,6 +32,7 @@ static void cleanup_free(void *p) { free(*(void**) p); } #define RAII_ALLOC(type, name, initializer) __attribute__((cleanup(cleanup_free))) type *name = initializer +static bool being_tested = false; #ifndef __FreeBSD__ static bool @@ -304,12 +305,14 @@ static void exec_kitten(int argc, char *argv[], char *exe_dir) { char exe[PATH_MAX+1] = {0}; safe_snprintf(exe, PATH_MAX, "%s/kitten", exe_dir); - char **newargv = malloc(sizeof(char*) * (argc + 1)); - memcpy(newargv, argv, sizeof(char*) * argc); - newargv[argc] = 0; - newargv[0] = "kitten"; + argv[0] = "kitten"; + if (being_tested) { + printf("kitten_exe: %s\n", exe); + output_argv("argv", argc, argv); + exit(0); + } errno = 0; - execv(exe, newargv); + execv(exe, argv); fprintf(stderr, "Failed to execute kitten (%s) with error: %s\n", exe, strerror(errno)); exit(1); } @@ -357,18 +360,23 @@ static void handle_fast_commandline(CLISpec *cli_spec, const char *instance_group_prefix, int offset_for_panel_kitten) { CLIOptions opts = {0}; RAII_CLISpec(subcommand_cli_spec); +#define swap_cli_spec \ + subcommand_cli_spec.original_argc = cli_spec->original_argc; \ + subcommand_cli_spec.original_argv = cli_spec->original_argv; \ + cli_spec = &subcommand_cli_spec; if (offset_for_panel_kitten < 0) { // Look for +open int offset = offset_for_plus_subcommand(cli_spec->original_argc, cli_spec->original_argv, "open"); if (offset) { if (!parse_and_check_kitty_cli(&subcommand_cli_spec, cli_spec->original_argc - offset, cli_spec->original_argv + offset)) exit(1); - cli_spec = &subcommand_cli_spec; + swap_cli_spec; opts.open_url_count = cli_spec->argc; opts.open_urls = cli_spec->argv; } } else if (offset_for_panel_kitten > 0) { - parse_and_check_panel_kitten_cli(&subcommand_cli_spec, cli_spec->original_argc - offset_for_panel_kitten, cli_spec->original_argv + offset_for_panel_kitten); - cli_spec = &subcommand_cli_spec; + parse_and_check_panel_kitten_cli( + &subcommand_cli_spec, cli_spec->original_argc - offset_for_panel_kitten, cli_spec->original_argv + offset_for_panel_kitten); + swap_cli_spec; } if (get_bool_cli_val(cli_spec, "help")) return; if (get_bool_cli_val(cli_spec, "version")) { @@ -381,21 +389,29 @@ handle_fast_commandline(CLISpec *cli_spec, const char *instance_group_prefix, in } opts.session = get_string_cli_val(cli_spec, "session"); if (get_bool_cli_val(cli_spec, "detach")) { -#define reopen_or_fail(path, mode, which) { errno = 0; if (freopen(path, mode, which) == NULL) { int s = errno; fprintf(stderr, "Failed to redirect %s to %s with error: ", #which, path); errno = s; perror(NULL); exit(1); } } - if (!(opts.session && ((opts.session[0] == '-' && opts.session[1] == 0) || strcmp(opts.session, "/dev/stdin") == 0))) - reopen_or_fail("/dev/null", "rb", stdin); const char *detached_log = get_string_cli_val(cli_spec, "detached_log"); - if (!detached_log || !detached_log[0]) detached_log = "/dev/null"; - reopen_or_fail(detached_log, "ab", stdout); - reopen_or_fail(detached_log, "ab", stderr); + if (being_tested) { + printf("detach: true\n"); + printf("detached_log: %s\n", detached_log ? detached_log : ""); + printf("session: %s\n", opts.session ? opts.session : ""); + exit(0); + } else { +#define reopen_or_fail(path, mode, which) { errno = 0; if (freopen(path, mode, which) == NULL) { int s = errno; fprintf(stderr, "Failed to redirect %s to %s with error: ", #which, path); errno = s; perror(NULL); exit(1); } } + if (!(opts.session && ((opts.session[0] == '-' && opts.session[1] == 0) || strcmp(opts.session, "/dev/stdin") == 0))) + reopen_or_fail("/dev/null", "rb", stdin); + if (!detached_log || !detached_log[0]) detached_log = "/dev/null"; + reopen_or_fail(detached_log, "ab", stdout); + reopen_or_fail(detached_log, "ab", stderr); #undef reopen_or_fail - if (fork() != 0) exit(0); - setsid(); + if (fork() != 0) exit(0); + setsid(); + } } unsetenv("KITTY_SI_DATA"); if (get_bool_cli_val(cli_spec, "single_instance")) { char igbuf[256]; opts.wait_for_single_instance_window_close = get_bool_cli_val(cli_spec, "wait_for_single_instance_window_close"); + opts.instance_group = get_string_cli_val(cli_spec, "instance_group"); if (instance_group_prefix && instance_group_prefix[0]) { opts.instance_group = get_string_cli_val(cli_spec, "instance_group"); if (opts.instance_group && opts.instance_group[0]) { @@ -405,7 +421,17 @@ handle_fast_commandline(CLISpec *cli_spec, const char *instance_group_prefix, in opts.instance_group = instance_group_prefix; } } - single_instance_main(cli_spec->original_argc, cli_spec->original_argv, &opts); + if (being_tested) { + output_argv("argv", cli_spec->original_argc, cli_spec->original_argv); + output_argv("open_urls", opts.open_url_count, opts.open_urls); + output_values_for_testing(cli_spec); + printf("single_instance: 1\n"); + printf("instance_group: %s\n", opts.instance_group ? opts.instance_group : ""); + printf("session: %s\n", opts.session ? opts.session : ""); + exit(0); + } else { + single_instance_main(cli_spec->original_argc, cli_spec->original_argv, &opts); + } } } @@ -417,7 +443,8 @@ delegate_to_kitten_if_possible(int argc, char **argv, char* exe_dir) { const char *kitten = argv[offset + 1]; if (is_wrapped_kitten(kitten)) exec_kitten(argc - offset, argv + offset, exe_dir); if (strcmp(kitten, "panel") == 0) { - handle_fast_commandline(NULL, "panel", offset + 1); + CLISpec t = {.original_argv = argv, .original_argc=argc}; + handle_fast_commandline(&t, "panel", offset + 1); return true; } } @@ -432,8 +459,25 @@ endswith(const char *str, const char *suffix) { return strcmp(str + strLen - suffixLen, suffix) == 0; } -int main(int argc_, char *argv_[], char* envp[]) { +static void +output_test_data(RunData *rd) { + printf("launched_by_launch_services: %d\n", rd->launched_by_launch_services); + printf("is_quick_access_terminal: %d\n", rd->is_quick_access_terminal); + char buf[PATH_MAX + 1]; + if (rd->config_dir == NULL) { + if (get_config_dir(buf, sizeof(buf))) rd->config_dir = buf; + } + printf("config_dir: %s\n", rd->config_dir ? rd->config_dir : ""); + output_for_testing(rd->cli_spec); +} + +int +main(int argc_, char *argv_[], char* envp[]) { if (argc_ < 1 || !argv_) { fprintf(stderr, "Invalid argc/argv\n"); return 1; } + if (argc_ > 1 && strcmp(argv_[1], "+testing-launcher-code") == 0) { + being_tested = true; + memmove(argv_ + 1, argv_ + 2, (--argc_ - 1) * sizeof(argv_[0])); + } if (!ensure_working_stdio()) return 1; char exe[PATH_MAX+1] = {0}; if (!read_exe_path(exe, sizeof(exe))) return 1; @@ -483,8 +527,9 @@ int main(int argc_, char *argv_[], char* envp[]) { .exe = exe, .exe_dir = exe_dir, .lib_dir = lib, .cli_spec = &cli_spec, .lc_ctype = lc_ctype, .launched_by_launch_services=launched_by_launch_services, .config_dir = config_dir, .is_quick_access_terminal=is_quick_access_terminal, }; - ret = run_embedded(&run_data); + if (being_tested) output_test_data(&run_data); + else ret = run_embedded(&run_data); single_instance_main(-1, NULL, NULL); - Py_FinalizeEx(); + if (!being_tested) Py_FinalizeEx(); return ret; } diff --git a/kitty/main.py b/kitty/main.py index a27869c03..109854508 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -470,9 +470,10 @@ def kitty_main() -> None: 'Run kitty and open the specified files or URLs in it, using launch-actions.conf. For details' ' see https://sw.kovidgoyal.net/kitty/open_actions/#scripting-the-opening-of-files-with-kitty-on-macos' '\n\nAll the normal kitty options can be used.') + cli_flags = None else: + cli_flags = getattr(sys, 'kitty_run_data', {}).get('cli_flags', None) usage = msg = appname = None - cli_flags = getattr(sys, 'kitty_run_data', {}).get('cli_flags', None) cli_opts, rest = parse_args(args=args, result_class=CLIOptions, usage=usage, message=msg, appname=appname, preparsed_from_c=cli_flags) if getattr(sys, 'cmdline_args_for_open', False): setattr(sys, 'cmdline_args_for_open', rest) diff --git a/kitty_tests/options.py b/kitty_tests/options.py index ce883e13e..dd6ca52b8 100644 --- a/kitty_tests/options.py +++ b/kitty_tests/options.py @@ -3,11 +3,13 @@ import os import shutil +import subprocess import tempfile +from kitty.constants import kitty_exe from kitty.fast_data_types import Color, test_cursor_blink_easing_function from kitty.options.utils import DELETE_ENV_VAR, EasingFunction, to_color -from kitty.utils import log_error +from kitty.utils import log_error, shlex_split from . import BaseTest @@ -28,13 +30,15 @@ class TestConfParsing(BaseTest): def test_conf_parsing(self): conf_parsing(self) + def test_launcher(self): + launcher(self) + def test_cli_parsing(self): cli_parsing(self) def cli_parsing(self): from kitty.cli import CLIOptions, Options, parse_cmdline, parse_option_spec - from kitty.utils import shlex_split seq, disabled = parse_option_spec('''\ --simple-string -s a simple string @@ -119,6 +123,77 @@ version t('-f=3.142 --int 17', float=3.142, int=17) +def launcher(self): + kexe = kitty_exe() + def get_report(cmdline: str, launch_services= False): + args = list(shlex_split(cmdline)) + env = dict(os.environ) + if launch_services: + env['KITTY_LAUNCHED_BY_LAUNCH_SERVICES'] = '1' + cp = subprocess.run([kexe, "+testing-launcher-code"] + args, env=env, stdout=subprocess.PIPE) + self.assertEqual(cp.returncode, 0) + ans = {} + for line in cp.stdout.decode().split('\n'): + if not line: + continue + try: + key, val = line.split(':') + except ValueError: + raise AssertionError(f'Unexpected output from launcher: {line!r}\n{cp.stdout.decode()}') + if key in ('argv', 'original_argv', 'open_urls'): + val = [x for x in val.split('\x1e') if x] + else: + val = val.strip() + ans[key] = val + return ans, cp.stdout.decode().replace('\x1e', ' ') + def test(cmdline, assertions): + r, output = get_report(cmdline, launch_services=assertions.get('launched_by_launch_services', '0') != '0') + for key, expected in assertions.items(): + self.assertEqual(expected, r.get(key), f'Failed for {key} with command line: {cmdline}\nOutput:\n{output}') + return output + + def t(cmdline, **assertions): + assertions['is_quick_access_terminal'] = '0' + assertions['config_dir'] = os.path.join(os.environ['XDG_CONFIG_HOME'], 'kitty') + assertions.setdefault('launched_by_launch_services', '0') + test(cmdline, assertions) + + def si(cmdline, **assertions): + assertions['single_instance'] = '1' + test(cmdline, assertions) + + def dt(cmdline, **assertions): + assertions['detach'] = 'true' + test(cmdline, assertions) + + def k(cmdline): + assertions = {} + assertions['argv'] = ['kitten'] + cmdline.split() + for prefix in ('+kitten', '+ kitten'): + output = test(prefix + ' ' + cmdline, assertions) + self.assertIn('kitten_exe:', output) + + def pn(cmdline, **assertions): + ig = assertions.get('instance_group') + assertions['instance_group'] = f'panel-{ig}' if ig else 'panel' + assertions['single_instance'] = '1' + assertions['session'] = '' + test(cmdline, assertions) + + t('', original_argv=[kexe], argv=[]) + t('--title=xxx cat', title='xxx', original_argv=[kexe, '--title=xxx', 'cat'], argv=['cat']) + k('icat abc xyz') + t('+kitten unwrapped xyz', argv=['+kitten', 'unwrapped', 'xyz']) + t('+ kitten unwrapped xyz', original_argv=[kexe, '+', 'kitten', 'unwrapped', 'xyz']) + si('--single-instance --instance-group=g -T 3', argv=[kexe, '--single-instance', '--instance-group=g', '-T', '3']) + t('+open --help', argv=['+open', '--help']) + t('+open -1 --help', argv=['+open', '-1', '--help']) + si('+open -1 moose', argv=[kexe, '+open', '-1', 'moose'], open_urls=['moose']) + si('+open -1 --instance-group=g x y', instance_group='g', open_urls=['x', 'y']) + dt('--detach --session=moose --detached-log=xyz', detached_log='xyz', session='moose') + pn('+kitten panel -1 --edge=left', edge='left') + + def conf_parsing(self): from kitty.config import defaults, load_config from kitty.constants import is_macos diff --git a/setup.py b/setup.py index 121663453..043cb32d7 100755 --- a/setup.py +++ b/setup.py @@ -1299,8 +1299,10 @@ def build_launcher(args: Options, launcher_dir: str = '.', bundle_type: str = 's werror = '' if args.ignore_compiler_warnings else '-pedantic-errors -Werror' cflags = f'-Wall {werror} -fpie'.split() cppflags = [define(f'WRAPPED_KITTENS=" {wrapped_kittens()} "')] - libs: List[str] = [] ldflags = shlex.split(os.environ.get('LDFLAGS', '')) + xxhash = xxhash_flags() + cppflags.extend(xxhash[0]) + libs: list[str] = xxhash[1] if args.profile or args.sanitize: cflags.append('-g3') if args.sanitize: