mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	Merge #8519 feat: name, test ids, sockets in stdpath(state)
This commit is contained in:
		| @@ -6628,30 +6628,29 @@ serverlist()						*serverlist()* | ||||
|  | ||||
| serverstart([{address}])				*serverstart()* | ||||
| 		Opens a socket or named pipe at {address} and listens for | ||||
| 		|RPC| messages. Clients can send |API| commands to the address | ||||
| 		to control Nvim. | ||||
| 		|RPC| messages. Clients can send |API| commands to the | ||||
| 		returned address to control Nvim. | ||||
|  | ||||
| 		Returns the address string. | ||||
| 		Returns the address string (may differ from the requested | ||||
| 		{address}). | ||||
| 		  | ||||
| 		If {address} does not contain a colon ":" it is interpreted as | ||||
| 		a named pipe or Unix domain socket path. | ||||
| 		- If {address} contains a colon ":" it is interpreted as | ||||
| 		  a TCP/IPv4/IPv6 address where the last ":" separates host | ||||
| 		  and port (empty or zero assigns a random port). | ||||
| 		- Else it is interpreted as a named pipe or Unix domain socket | ||||
| 		  path. If there are no slashes it is treated as a name and | ||||
| 		  appended to a generated path. | ||||
| 		- If {address} is empty it generates a path. | ||||
|  | ||||
| 		Example: > | ||||
| 		Example named pipe: > | ||||
| 			if has('win32') | ||||
| 			  call serverstart('\\.\pipe\nvim-pipe-1234') | ||||
| 			  echo serverstart('\\.\pipe\nvim-pipe-1234') | ||||
| 			else | ||||
| 			  call serverstart('nvim.sock') | ||||
| 			  echo serverstart('nvim.sock') | ||||
| 			endif | ||||
| < | ||||
| 		If {address} contains a colon ":" it is interpreted as a TCP | ||||
| 		address where the last ":" separates the host and port. | ||||
| 		Assigns a random port if it is empty or 0. Supports IPv4/IPv6. | ||||
|  | ||||
| 		Example: > | ||||
| 			:call serverstart('::1:12345') | ||||
| < | ||||
| 		If no address is given, it is equivalent to: > | ||||
| 			:call serverstart(tempname()) | ||||
| 		Example TCP/IP address: > | ||||
| 			echo serverstart('::1:12345') | ||||
|  | ||||
| serverstop({address})					*serverstop()* | ||||
| 		Closes the pipe or socket at {address}. | ||||
| @@ -7545,7 +7544,7 @@ stdpath({what})					*stdpath()* *E6100* | ||||
| 		data_dirs    List    Other data directories. | ||||
| 		log          String  Logs directory (for use by plugins too). | ||||
| 		state        String  Session state directory: storage for file | ||||
| 				     drafts, undo history, shada, etc. | ||||
| 				     drafts, undo, shada, named pipes, ... | ||||
|  | ||||
| 		Example: > | ||||
| 			:echo stdpath("config") | ||||
|   | ||||
| @@ -1790,8 +1790,9 @@ Dictionary nvim__stats(void) | ||||
| { | ||||
|   Dictionary rv = ARRAY_DICT_INIT; | ||||
|   PUT(rv, "fsync", INTEGER_OBJ(g_stats.fsync)); | ||||
|   PUT(rv, "redraw", INTEGER_OBJ(g_stats.redraw)); | ||||
|   PUT(rv, "log_skip", INTEGER_OBJ(g_stats.log_skip)); | ||||
|   PUT(rv, "lua_refcount", INTEGER_OBJ(nlua_get_global_ref_count())); | ||||
|   PUT(rv, "redraw", INTEGER_OBJ(g_stats.redraw)); | ||||
|   return rv; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -8497,7 +8497,7 @@ static void f_serverstart(typval_T *argvars, typval_T *rettv, FunPtr fptr) | ||||
|       address = xstrdup(tv_get_string(argvars)); | ||||
|     } | ||||
|   } else { | ||||
|     address = server_address_new(); | ||||
|     address = server_address_new(NULL); | ||||
|   } | ||||
|  | ||||
|   int result = server_start(address); | ||||
|   | ||||
| @@ -120,7 +120,7 @@ int process_spawn(Process *proc, bool in, bool out, bool err) | ||||
|   proc->internal_close_cb = decref; | ||||
|   proc->refcount++; | ||||
|   kl_push(WatcherPtr, proc->loop->children, proc); | ||||
|   DLOG("new: pid=%d argv=[%s]", proc->pid, *proc->argv); | ||||
|   DLOG("new: pid=%d argv=[%s]", proc->pid, proc->argv[0]); | ||||
|   return 0; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -16,15 +16,15 @@ | ||||
| #include <uv.h> | ||||
|  | ||||
| #include "auto/config.h" | ||||
| #include "nvim/eval.h" | ||||
| #include "nvim/log.h" | ||||
| #include "nvim/main.h" | ||||
| #include "nvim/message.h" | ||||
| #include "nvim/os/os.h" | ||||
| #include "nvim/os/time.h" | ||||
| #include "nvim/path.h" | ||||
| #include "nvim/types.h" | ||||
|  | ||||
| #define LOG_FILE_ENV "NVIM_LOG_FILE" | ||||
|  | ||||
| /// Cached location of the expanded log file path decided by log_path_init(). | ||||
| static char log_file_path[MAXPATHL + 1] = { 0 }; | ||||
|  | ||||
| @@ -52,7 +52,7 @@ static bool log_try_create(char *fname) | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| /// Initializes path to log file. Sets $NVIM_LOG_FILE if empty. | ||||
| /// Initializes the log file path and sets $NVIM_LOG_FILE if empty. | ||||
| /// | ||||
| /// Tries $NVIM_LOG_FILE, or falls back to $XDG_STATE_HOME/nvim/log. Failed | ||||
| /// initialization indicates either a bug in expand_env() or both $NVIM_LOG_FILE | ||||
| @@ -60,9 +60,8 @@ static bool log_try_create(char *fname) | ||||
| static void log_path_init(void) | ||||
| { | ||||
|   size_t size = sizeof(log_file_path); | ||||
|   expand_env((char_u *)"$" LOG_FILE_ENV, (char_u *)log_file_path, | ||||
|              (int)size - 1); | ||||
|   if (strequal("$" LOG_FILE_ENV, log_file_path) | ||||
|   expand_env((char_u *)"$" ENV_LOGFILE, (char_u *)log_file_path, (int)size - 1); | ||||
|   if (strequal("$" ENV_LOGFILE, log_file_path) | ||||
|       || log_file_path[0] == '\0' | ||||
|       || os_isdir((char_u *)log_file_path) | ||||
|       || !log_try_create(log_file_path)) { | ||||
| @@ -87,7 +86,7 @@ static void log_path_init(void) | ||||
|       log_file_path[0] = '\0'; | ||||
|       return; | ||||
|     } | ||||
|     os_setenv(LOG_FILE_ENV, log_file_path, true); | ||||
|     os_setenv(ENV_LOGFILE, log_file_path, true); | ||||
|     if (log_dir_failure) { | ||||
|       WLOG("Failed to create directory %s for writing logs: %s", | ||||
|            failed_dir, os_strerror(log_dir_failure)); | ||||
| @@ -209,7 +208,7 @@ FILE *open_log_file(void) | ||||
|   //  - Directory does not exist | ||||
|   //  - File is not writable | ||||
|   do_log_to_file(stderr, LOGLVL_ERR, NULL, __func__, __LINE__, true, | ||||
|                  "failed to open $" LOG_FILE_ENV " (%s): %s", | ||||
|                  "failed to open $" ENV_LOGFILE " (%s): %s", | ||||
|                  strerror(errno), log_file_path); | ||||
|   return stderr; | ||||
| } | ||||
| @@ -277,6 +276,9 @@ static bool v_do_log_to_file(FILE *log_file, int log_level, const char *context, | ||||
|                              va_list args) | ||||
|   FUNC_ATTR_PRINTF(7, 0) | ||||
| { | ||||
|   // Name of the Nvim instance that produced the log. | ||||
|   static char name[16] = { 0 }; | ||||
|  | ||||
|   static const char *log_levels[] = { | ||||
|     [LOGLVL_DBG] = "DBG", | ||||
|     [LOGLVL_INF] = "INF", | ||||
| @@ -291,8 +293,7 @@ static bool v_do_log_to_file(FILE *log_file, int log_level, const char *context, | ||||
|     return false; | ||||
|   } | ||||
|   char date_time[20]; | ||||
|   if (strftime(date_time, sizeof(date_time), "%Y-%m-%dT%H:%M:%S", | ||||
|                &local_time) == 0) { | ||||
|   if (strftime(date_time, sizeof(date_time), "%Y-%m-%dT%H:%M:%S", &local_time) == 0) { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
| @@ -302,14 +303,30 @@ static bool v_do_log_to_file(FILE *log_file, int log_level, const char *context, | ||||
|     millis = (int)curtime.tv_usec / 1000; | ||||
|   } | ||||
|  | ||||
|   // Get a name for this Nvim instance. | ||||
|   // TODO(justinmk): expose this as v:name ? | ||||
|   if (starting || name[0] == '\0') { | ||||
|     // Parent servername. | ||||
|     const char *parent = path_tail(os_getenv(ENV_NVIM)); | ||||
|     // Servername. Empty until starting=false. | ||||
|     const char *serv = path_tail(get_vim_var_str(VV_SEND_SERVER)); | ||||
|     if (parent && parent[0] != NUL) { | ||||
|       snprintf(name, sizeof(name), "%s/c", parent);  // "/c" indicates child. | ||||
|     } else if (serv && serv[0] != NUL) { | ||||
|       snprintf(name, sizeof(name), "%s", serv ? serv : ""); | ||||
|     } else { | ||||
|       int64_t pid = os_get_pid(); | ||||
|       snprintf(name, sizeof(name), "?.%-5" PRId64, pid); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Print the log message. | ||||
|   int64_t pid = os_get_pid(); | ||||
|   int rv = (line_num == -1 || func_name == NULL) | ||||
|     ? fprintf(log_file, "%s %s.%03d %-5" PRId64 " %s", | ||||
|               log_levels[log_level], date_time, millis, pid, | ||||
|     ? fprintf(log_file, "%s %s.%03d %-10s %s", | ||||
|               log_levels[log_level], date_time, millis, name, | ||||
|               (context == NULL ? "?:" : context)) | ||||
|                                : fprintf(log_file, "%s %s.%03d %-5" PRId64 " %s%s:%d: ", | ||||
|                                          log_levels[log_level], date_time, millis, pid, | ||||
|                                : fprintf(log_file, "%s %s.%03d %-10s %s%s:%d: ", | ||||
|                                          log_levels[log_level], date_time, millis, name, | ||||
|                                          (context == NULL ? "" : context), | ||||
|                                          func_name, line_num); | ||||
|   if (rv < 0) { | ||||
|   | ||||
| @@ -23,7 +23,6 @@ | ||||
|  | ||||
| #define MAX_CONNECTIONS 32 | ||||
| #define ENV_LISTEN "NVIM_LISTEN_ADDRESS"  // deprecated | ||||
| #define ENV_NVIM "NVIM" | ||||
|  | ||||
| static garray_T watchers = GA_EMPTY_INIT_VALUE; | ||||
|  | ||||
| @@ -43,7 +42,7 @@ bool server_init(const char *listen_addr) | ||||
|  | ||||
|   int rv = listen_addr ? server_start(listen_addr) : 1; | ||||
|   if (0 != rv) { | ||||
|     listen_addr = server_address_new(); | ||||
|     listen_addr = server_address_new(NULL); | ||||
|     if (!listen_addr) { | ||||
|       return false; | ||||
|     } | ||||
| @@ -56,6 +55,11 @@ bool server_init(const char *listen_addr) | ||||
|     os_unsetenv(ENV_LISTEN); | ||||
|   } | ||||
|  | ||||
|   // TODO(justinmk): this is for logging_spec. Can remove this after nvim_log #7062 is merged. | ||||
|   if (os_env_exists("__NVIM_TEST_LOG")) { | ||||
|     ELOG("test log message"); | ||||
|   } | ||||
|  | ||||
|   return rv == 0; | ||||
| } | ||||
|  | ||||
| @@ -83,23 +87,26 @@ void server_teardown(void) | ||||
|  | ||||
| /// Generates unique address for local server. | ||||
| /// | ||||
| /// In Windows this is a named pipe in the format | ||||
| ///     \\.\pipe\nvim-<PID>-<COUNTER>. | ||||
| /// | ||||
| /// For other systems it is a path returned by vim_tempname(). | ||||
| /// | ||||
| /// This function is NOT thread safe | ||||
| char *server_address_new(void) | ||||
| /// Named pipe format: | ||||
| /// - Windows: "\\.\pipe\<name>.<pid>.<counter>" | ||||
| /// - Other: "~/.local/state/nvim/<name>.<pid>.<counter>" | ||||
| char *server_address_new(const char *name) | ||||
| { | ||||
| #ifdef WIN32 | ||||
|   static uint32_t count = 0; | ||||
|   char template[ADDRESS_MAX_SIZE]; | ||||
|   snprintf(template, ADDRESS_MAX_SIZE, | ||||
|            "\\\\.\\pipe\\nvim-%" PRIu64 "-%" PRIu32, os_get_pid(), count++); | ||||
|   return xstrdup(template); | ||||
|   char fmt[ADDRESS_MAX_SIZE]; | ||||
| #ifdef WIN32 | ||||
|   int r = snprintf(fmt, sizeof(fmt), "\\\\.\\pipe\\%s.%" PRIu64 ".%" PRIu32, | ||||
|                    name ? name : "nvim", os_get_pid(), count++); | ||||
| #else | ||||
|   return (char *)vim_tempname(); | ||||
|   char *dir = get_xdg_home(kXDGStateHome); | ||||
|   int r = snprintf(fmt, sizeof(fmt), "%s/%s.%" PRIu64 ".%" PRIu32, | ||||
|                    dir, name ? name : "nvim", os_get_pid(), count++); | ||||
|   xfree(dir); | ||||
| #endif | ||||
|   if ((size_t)r >= sizeof(fmt)) { | ||||
|     ELOG("truncated server address"); | ||||
|   } | ||||
|   return xstrdup(fmt); | ||||
| } | ||||
|  | ||||
| /// Check if this instance owns a pipe address. | ||||
| @@ -114,35 +121,35 @@ bool server_owns_pipe_address(const char *path) | ||||
|   return false; | ||||
| } | ||||
|  | ||||
| /// Starts listening for API calls. | ||||
| /// Starts listening for RPC calls. | ||||
| /// | ||||
| /// The socket type is determined by parsing `endpoint`: If it's a valid IPv4 | ||||
| /// or IPv6 address in 'ip:[port]' format, then it will be a TCP socket. | ||||
| /// Otherwise it will be a Unix socket or named pipe (Windows). | ||||
| /// Socket type is decided by the format of `addr`: | ||||
| /// - TCP socket if it looks like an IPv4/6 address ("ip:[port]"). | ||||
| ///   - If [port] is omitted, a random one is assigned. | ||||
| /// - Unix socket (or named pipe on Windows) otherwise. | ||||
| ///   - If the name doesn't contain slashes it is appended to a generated path. #8519 | ||||
| /// | ||||
| /// If no port is given, a random one will be assigned. | ||||
| /// | ||||
| /// @param endpoint Address of the server. Either a 'ip:[port]' string or an | ||||
| ///                 arbitrary identifier (trimmed to 256 bytes) for the Unix | ||||
| ///                 socket or named pipe. | ||||
| /// @returns 0: success, 1: validation error, 2: already listening, | ||||
| ///          -errno: failed to bind or listen. | ||||
| int server_start(const char *endpoint) | ||||
| /// @param addr Server address: a "ip:[port]" string or arbitrary name or filepath (max 256 bytes) | ||||
| ///             for the Unix socket or named pipe. | ||||
| /// @returns 0: success, 1: validation error, 2: already listening, -errno: failed to bind/listen. | ||||
| int server_start(const char *addr) | ||||
| { | ||||
|   if (endpoint == NULL || endpoint[0] == '\0') { | ||||
|     WLOG("Empty or NULL endpoint"); | ||||
|   if (addr == NULL || addr[0] == '\0') { | ||||
|     WLOG("Empty or NULL address"); | ||||
|     return 1; | ||||
|   } | ||||
|  | ||||
|   bool isname = !strstr(addr, ":") && !strstr(addr, "/") && !strstr(addr, "\\"); | ||||
|   char *addr_gen = isname ? server_address_new(addr) : NULL; | ||||
|   SocketWatcher *watcher = xmalloc(sizeof(SocketWatcher)); | ||||
|  | ||||
|   int result = socket_watcher_init(&main_loop, watcher, endpoint); | ||||
|   int result = socket_watcher_init(&main_loop, watcher, isname ? addr_gen : addr); | ||||
|   xfree(addr_gen); | ||||
|   if (result < 0) { | ||||
|     xfree(watcher); | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   // Check if a watcher for the endpoint already exists | ||||
|   // Check if a watcher for the address already exists. | ||||
|   for (int i = 0; i < watchers.ga_len; i++) { | ||||
|     if (!strcmp(watcher->addr, ((SocketWatcher **)watchers.ga_data)[i]->addr)) { | ||||
|       ELOG("Already listening on %s", watcher->addr); | ||||
|   | ||||
| @@ -16,4 +16,7 @@ | ||||
| # include "os/users.h.generated.h" | ||||
| #endif | ||||
|  | ||||
| #define ENV_LOGFILE "NVIM_LOG_FILE" | ||||
| #define ENV_NVIM "NVIM" | ||||
|  | ||||
| #endif  // NVIM_OS_OS_H | ||||
|   | ||||
| @@ -163,10 +163,15 @@ static struct termios termios_default; | ||||
| /// @param tty_fd   TTY file descriptor, or -1 if not in a terminal. | ||||
| void pty_process_save_termios(int tty_fd) | ||||
| { | ||||
|   DLOG("tty_fd=%d", tty_fd); | ||||
|   if (tty_fd == -1 || tcgetattr(tty_fd, &termios_default) != 0) { | ||||
|   if (tty_fd == -1) { | ||||
|     return; | ||||
|   } | ||||
|   int rv = tcgetattr(tty_fd, &termios_default); | ||||
|   if (rv != 0) { | ||||
|     ELOG("tcgetattr failed (tty_fd=%d): %s", tty_fd, strerror(errno)); | ||||
|   } else { | ||||
|     DLOG("tty_fd=%d", tty_fd); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @returns zero on success, or negative error code | ||||
|   | ||||
| @@ -88,7 +88,12 @@ FileComparison path_full_compare(char_u *const s1, char_u *const s2, const bool | ||||
|   return kDifferentFiles; | ||||
| } | ||||
|  | ||||
| /// Gets the tail (i.e., the filename segment) of a path `fname`. | ||||
| /// Gets the tail (filename segment) of path `fname`. | ||||
| /// | ||||
| /// Examples: | ||||
| /// - "dir/file.txt" => "file.txt" | ||||
| /// - "file.txt" => "file.txt" | ||||
| /// - "dir/" => "" | ||||
| /// | ||||
| /// @return pointer just past the last path separator (empty string, if fname | ||||
| ///         ends in a slash), or empty string if fname is NULL. | ||||
|   | ||||
| @@ -91,25 +91,33 @@ or: | ||||
| Debugging tests | ||||
| --------------- | ||||
|  | ||||
| - Each test gets a test id which looks like "T123". This also appears in the | ||||
|   log file. Child processes spawned from a test appear in the logs with the | ||||
|   *parent* name followed by "/c". Example: | ||||
|   ``` | ||||
|     DBG 2022-06-15T18:37:45.226 T57.58016.0   UI: flush | ||||
|     DBG 2022-06-15T18:37:45.226 T57.58016.0   inbuf_poll:442: blocking... events_enabled=0 events_pending=0 | ||||
|     DBG 2022-06-15T18:37:45.227 T57.58016.0/c UI: stop | ||||
|     INF 2022-06-15T18:37:45.227 T57.58016.0/c os_exit:595: Nvim exit: 0 | ||||
|     DBG 2022-06-15T18:37:45.229 T57.58016.0   read_cb:118: closing Stream (0x7fd5d700ea18): EOF (end of file) | ||||
|     INF 2022-06-15T18:37:45.229 T57.58016.0   on_process_exit:400: exited: pid=58017 status=0 stoptime=0 | ||||
|   ``` | ||||
| - You can set `$GDB` to [run tests under gdbserver](https://github.com/neovim/neovim/pull/1527). | ||||
|   And if `$VALGRIND` is set it will pass `--vgdb=yes` to valgrind instead of | ||||
|   starting gdbserver directly. | ||||
| - Hanging tests often happen due to unexpected `:h press-enter` prompts. The | ||||
| - Hanging tests can happen due to unexpected "press-enter" prompts. The | ||||
|   default screen width is 50 columns. Commands that try to print lines longer | ||||
|   than 50 columns in the command-line, e.g. `:edit very...long...path`, will | ||||
|   trigger the prompt. In this case, a shorter path or `:silent edit` should be | ||||
|   used. | ||||
|   trigger the prompt. Try using a shorter path, or `:silent edit`. | ||||
| - If you can't figure out what is going on, try to visualize the screen. Put | ||||
|   this at the beginning of your test: | ||||
|  | ||||
|     ```lua | ||||
|     local Screen = require('test.functional.ui.screen') | ||||
|     local screen = Screen.new() | ||||
|     screen:attach() | ||||
|     ``` | ||||
|  | ||||
|   Afterwards, put `screen:snapshot_util()` at any position in your test. See the | ||||
|   comment at the top of `test/functional/ui/screen.lua` for more. | ||||
|   ```lua | ||||
|   local Screen = require('test.functional.ui.screen') | ||||
|   local screen = Screen.new() | ||||
|   screen:attach() | ||||
|   ``` | ||||
|   Then put `screen:snapshot_util()` anywhere in your test. See the comments in | ||||
|   `test/functional/ui/screen.lua` for more info. | ||||
|  | ||||
| Filtering Tests | ||||
| --------------- | ||||
| @@ -247,12 +255,17 @@ Number; !must be defined to function properly): | ||||
|  | ||||
| - `BUSTED_ARGS` (F) (U): arguments forwarded to `busted`. | ||||
|  | ||||
| - `CC` (U) (S): specifies which C compiler to use to preprocess files. | ||||
|   Currently only compilers with gcc-compatible arguments are supported. | ||||
|  | ||||
| - `GDB` (F) (D): makes nvim instances to be run under `gdbserver`. It will be | ||||
|   accessible on `localhost:7777`: use `gdb build/bin/nvim`, type `target remote | ||||
|   :7777` inside. | ||||
|  | ||||
| - `GDBSERVER_PORT` (F) (I): overrides port used for `GDB`. | ||||
|  | ||||
| - `LOG_DIR` (FU) (S!): specifies where to seek for valgrind and ASAN log files. | ||||
|  | ||||
| - `VALGRIND` (F) (D): makes nvim instances to be run under `valgrind`. Log | ||||
|   files are named `valgrind-%p.log` in this case. Note that non-empty valgrind | ||||
|   log may fail tests. Valgrind arguments may be seen in | ||||
| @@ -269,11 +282,7 @@ Number; !must be defined to function properly): | ||||
|  | ||||
| - `NVIM_LUA_NOTRACK` (F) (D): disable reference counting of Lua objects | ||||
|  | ||||
| - `NVIM_PROG`, `NVIM_PRG` (F) (S): override path to Neovim executable (default | ||||
|   to `build/bin/nvim`). | ||||
|  | ||||
| - `CC` (U) (S): specifies which C compiler to use to preprocess files. | ||||
|   Currently only compilers with gcc-compatible arguments are supported. | ||||
| - `NVIM_PRG` (F) (S): path to Nvim executable (default: `build/bin/nvim`). | ||||
|  | ||||
| - `NVIM_TEST_MAIN_CDEFS` (U) (1): makes `ffi.cdef` run in main process. This | ||||
|   raises a possibility of bugs due to conflicts in header definitions, despite | ||||
| @@ -295,8 +304,6 @@ Number; !must be defined to function properly): | ||||
| - `NVIM_TEST_RUN_FAILING_TESTS` (U) (1): makes `itp` run tests which are known | ||||
|   to fail (marked by setting third argument to `true`). | ||||
|  | ||||
| - `LOG_DIR` (FU) (S!): specifies where to seek for valgrind and ASAN log files. | ||||
|  | ||||
| - `NVIM_TEST_CORE_*` (FU) (S): a set of environment variables which specify | ||||
|   where to search for core files. Are supposed to be defined all at once. | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| local pretty = require 'pl.pretty' | ||||
| local global_helpers = require('test.helpers') | ||||
|  | ||||
| -- Colors are disabled by default. #15610 | ||||
| local colors = setmetatable({}, {__index = function() return function(s) return s == nil and '' or tostring(s) end end}) | ||||
| @@ -25,35 +26,35 @@ return function(options) | ||||
|  | ||||
|   local repeatSuiteString = '\nRepeating all tests (run %d of %d) . . .\n\n' | ||||
|   local randomizeString  = c.note('Note: Randomizing test order with a seed of %d.\n') | ||||
|   local globalSetup      = c.sect('[----------]') .. ' Global test environment setup.\n' | ||||
|   local fileStartString  = c.sect('[----------]') .. ' Running tests from ' .. c.file('%s') .. '\n' | ||||
|   local runString        = c.sect('[ RUN      ]') .. ' ' .. c.test('%s') .. ': ' | ||||
|   local globalSetup      = c.sect('--------') .. ' Global test environment setup.\n' | ||||
|   local fileStartString  = c.sect('--------') .. ' Running tests from ' .. c.file('%s') .. '\n' | ||||
|   local runString        = c.sect('RUN     ') .. ' ' .. c.test('%s') .. ': ' | ||||
|   local successString    = c.succ('OK')   .. '\n' | ||||
|   local skippedString    = c.skip('SKIP') .. '\n' | ||||
|   local failureString    = c.fail('FAIL') .. '\n' | ||||
|   local errorString      = c.errr('ERR')  .. '\n' | ||||
|   local fileEndString    = c.sect('[----------]') .. ' '.. c.nmbr('%d') .. ' %s from ' .. c.file('%s') .. ' ' .. c.time('(%.2f ms total)') .. '\n\n' | ||||
|   local globalTeardown   = c.sect('[----------]') .. ' Global test environment teardown.\n' | ||||
|   local suiteEndString   = c.sect('[==========]') .. ' ' .. c.nmbr('%d') .. ' %s from ' .. c.nmbr('%d') .. ' test %s ran. ' .. c.time('(%.2f ms total)') .. '\n' | ||||
|   local successStatus    = c.succ('[  PASSED  ]') .. ' ' .. c.nmbr('%d') .. ' %s.\n' | ||||
|   local fileEndString    = c.sect('--------') .. ' '.. c.nmbr('%d') .. ' %s from ' .. c.file('%s') .. ' ' .. c.time('(%.2f ms total)') .. '\n\n' | ||||
|   local globalTeardown   = c.sect('--------') .. ' Global test environment teardown.\n' | ||||
|   local suiteEndString   = c.sect('========') .. ' ' .. c.nmbr('%d') .. ' %s from ' .. c.nmbr('%d') .. ' test %s ran. ' .. c.time('(%.2f ms total)') .. '\n' | ||||
|   local successStatus    = c.succ('PASSED  ') .. ' ' .. c.nmbr('%d') .. ' %s.\n' | ||||
|   local timeString       = c.time('%.2f ms') | ||||
|  | ||||
|   local summaryStrings = { | ||||
|     skipped = { | ||||
|       header = c.skip('[ SKIPPED  ]') .. ' ' .. c.nmbr('%d') .. ' %s, listed below:\n', | ||||
|       test   = c.skip('[ SKIPPED  ]') .. ' %s\n', | ||||
|       header = c.skip('SKIPPED ') .. ' ' .. c.nmbr('%d') .. ' %s, listed below:\n', | ||||
|       test   = c.skip('SKIPPED ') .. ' %s\n', | ||||
|       footer = ' ' .. c.nmbr('%d') .. ' SKIPPED %s\n', | ||||
|     }, | ||||
|  | ||||
|     failure = { | ||||
|       header = c.fail('[  FAILED  ]') .. ' ' .. c.nmbr('%d') .. ' %s, listed below:\n', | ||||
|       test   = c.fail('[  FAILED  ]') .. ' %s\n', | ||||
|       header = c.fail('FAILED  ') .. ' ' .. c.nmbr('%d') .. ' %s, listed below:\n', | ||||
|       test   = c.fail('FAILED  ') .. ' %s\n', | ||||
|       footer = ' ' .. c.nmbr('%d') .. ' FAILED %s\n', | ||||
|     }, | ||||
|  | ||||
|     error = { | ||||
|       header = c.errr('[  ERROR   ]') .. ' ' .. c.nmbr('%d') .. ' %s, listed below:\n', | ||||
|       test   = c.errr('[  ERROR   ]') .. ' %s\n', | ||||
|       header = c.errr('ERROR   ') .. ' ' .. c.nmbr('%d') .. ' %s, listed below:\n', | ||||
|       test   = c.errr('ERROR   ') .. ' %s\n', | ||||
|       footer = ' ' .. c.nmbr('%d') .. ' %s\n', | ||||
|     }, | ||||
|   } | ||||
| @@ -193,6 +194,9 @@ return function(options) | ||||
|     io.write(globalTeardown) | ||||
|     io.write(suiteEndString:format(testCount, tests, fileCount, files, elapsedTime_ms)) | ||||
|     io.write(getSummaryString()) | ||||
|     if failureCount > 0 or errorCount > 0 then | ||||
|       io.write(global_helpers.read_nvim_log(nil, true)) | ||||
|     end | ||||
|     io.flush() | ||||
|  | ||||
|     return nil, true | ||||
| @@ -215,7 +219,9 @@ return function(options) | ||||
|   end | ||||
|  | ||||
|   handler.testStart = function(element, _parent) | ||||
|     io.write(runString:format(handler.getFullName(element))) | ||||
|     local testid = _G._nvim_test_id or '' | ||||
|     local desc = ('%s %s'):format(testid, handler.getFullName(element)) | ||||
|     io.write(runString:format(desc)) | ||||
|     io.flush() | ||||
|  | ||||
|     return nil, true | ||||
|   | ||||
| @@ -336,7 +336,7 @@ describe('nvim_get_keymap', function() | ||||
|       return GlobalCount | ||||
|     ]]) | ||||
|     local mapargs = meths.get_keymap('n') | ||||
|     assert.Truthy(type(mapargs[1].callback) == 'number', 'callback is not luaref number') | ||||
|     assert(type(mapargs[1].callback) == 'number', 'callback is not luaref number') | ||||
|     mapargs[1].callback = nil | ||||
|     eq({ | ||||
|       lhs='asdf', | ||||
| @@ -815,7 +815,7 @@ describe('nvim_set_keymap, nvim_del_keymap', function() | ||||
|     assert.truthy(string.match(funcs.maparg('asdf', 'n'), | ||||
|                   "^<Lua function %d+>")) | ||||
|     local mapargs = funcs.maparg('asdf', 'n', false, true) | ||||
|     assert.Truthy(type(mapargs.callback) == 'number', 'callback is not luaref number') | ||||
|     assert(type(mapargs.callback) == 'number', 'callback is not luaref number') | ||||
|     mapargs.callback = nil | ||||
|     eq(generate_mapargs('n', 'asdf', nil, {sid=sid_lua}), mapargs) | ||||
|   end) | ||||
|   | ||||
							
								
								
									
										57
									
								
								test/functional/core/log_spec.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								test/functional/core/log_spec.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| local helpers = require('test.functional.helpers')(after_each) | ||||
| local assert_log = helpers.assert_log | ||||
| local clear = helpers.clear | ||||
| local command = helpers.command | ||||
| local eq = helpers.eq | ||||
| local exec_lua = helpers.exec_lua | ||||
| local expect_exit = helpers.expect_exit | ||||
| local request = helpers.request | ||||
| local retry = helpers.retry | ||||
|  | ||||
| describe('log', function() | ||||
|   local testlog = 'Xtest_logging' | ||||
|  | ||||
|   after_each(function() | ||||
|     expect_exit(command, 'qa!') | ||||
|     os.remove(testlog) | ||||
|   end) | ||||
|  | ||||
|   it('skipped before log_init', function() | ||||
|     -- This test is for _visibility_: adjust as needed, after checking for regression. | ||||
|     -- | ||||
|     -- During startup some components may try to log before logging is setup. | ||||
|     -- That should be uncommon (ideally never)--and if there are MANY such | ||||
|     -- calls, that needs investigation. | ||||
|     clear() | ||||
|     eq(0, request('nvim__stats').log_skip) | ||||
|     clear{env={CDPATH='~doesnotexist'}} | ||||
|     assert(request('nvim__stats').log_skip <= 13) | ||||
|   end) | ||||
|  | ||||
|   it('messages are formatted with name or test id', function() | ||||
|     -- Examples: | ||||
|     --    ERR 2022-05-29T12:30:03.800 T2         log_init:110: test log message | ||||
|     --    ERR 2022-05-29T12:30:03.814 T2/child   log_init:110: test log message | ||||
|  | ||||
|     clear({env={ | ||||
|       NVIM_LOG_FILE=testlog, | ||||
|       -- TODO: remove this after nvim_log #7062 is merged. | ||||
|       __NVIM_TEST_LOG='1' | ||||
|       }}) | ||||
|  | ||||
|     local tid = _G._nvim_test_id | ||||
|     retry(nil, 1000, function() | ||||
|       assert_log(tid..'%.%d+%.%d +server_init:%d+: test log message', testlog, 100) | ||||
|     end) | ||||
|  | ||||
|     exec_lua([[ | ||||
|       local j1 = vim.fn.jobstart({ vim.v.progpath, '-es', '-V1', '+foochild', '+qa!' }, vim.empty_dict()) | ||||
|       vim.fn.jobwait({ j1 }, 10000) | ||||
|     ]]) | ||||
|  | ||||
|     -- Child Nvim spawned by jobstart() appends "/c" to parent name. | ||||
|     retry(nil, 1000, function() | ||||
|       assert_log('%.%d+%.%d/c +server_init:%d+: test log message', testlog, 100) | ||||
|     end) | ||||
|   end) | ||||
| end) | ||||
| @@ -580,7 +580,7 @@ describe('user config init', function() | ||||
|  | ||||
|     it('loads default lua config, but shows an error', function() | ||||
|       clear{ args_rm={'-u'}, env=xenv } | ||||
|       feed('<cr>') -- confirm "Conflicting config ..." message | ||||
|       feed('<cr><c-c>')  -- Dismiss "Conflicting config …" message. | ||||
|       eq(1, eval('g:lua_rc')) | ||||
|       matches('^E5422: Conflicting configs', meths.exec('messages', true)) | ||||
|     end) | ||||
| @@ -632,13 +632,13 @@ describe('runtime:', function() | ||||
|     eq(2, eval('g:lua_plugin')) | ||||
|     -- Check if plugin_file_path is listed in :scriptname | ||||
|     local scripts = meths.exec(':scriptnames', true) | ||||
|     assert.Truthy(scripts:find(plugin_file_path)) | ||||
|     assert(scripts:find(plugin_file_path)) | ||||
|  | ||||
|     -- Check if plugin_file_path is listed in startup profile | ||||
|     local profile_reader = io.open(profiler_file, 'r') | ||||
|     local profile_log = profile_reader:read('*a') | ||||
|     profile_reader:close() | ||||
|     assert.Truthy(profile_log :find(plugin_file_path)) | ||||
|     assert(profile_log:find(plugin_file_path)) | ||||
|  | ||||
|     os.remove(profiler_file) | ||||
|     rmdir(plugin_path) | ||||
|   | ||||
| @@ -431,18 +431,25 @@ end | ||||
| function module.new_argv(...) | ||||
|   local args = {unpack(module.nvim_argv)} | ||||
|   table.insert(args, '--headless') | ||||
|   if _G._nvim_test_id then | ||||
|     -- Set the server name to the test-id for logging. #8519 | ||||
|     table.insert(args, '--listen') | ||||
|     table.insert(args, _G._nvim_test_id) | ||||
|   end | ||||
|   local new_args | ||||
|   local io_extra | ||||
|   local env = nil | ||||
|   local opts = select(1, ...) | ||||
|   if type(opts) == 'table' then | ||||
|   if type(opts) ~= 'table' then | ||||
|     new_args = {...} | ||||
|   else | ||||
|     args = remove_args(args, opts.args_rm) | ||||
|     if opts.env then | ||||
|       local env_tbl = {} | ||||
|       local env_opt = {} | ||||
|       for k, v in pairs(opts.env) do | ||||
|         assert(type(k) == 'string') | ||||
|         assert(type(v) == 'string') | ||||
|         env_tbl[k] = v | ||||
|         env_opt[k] = v | ||||
|       end | ||||
|       for _, k in ipairs({ | ||||
|         'HOME', | ||||
| @@ -458,19 +465,18 @@ function module.new_argv(...) | ||||
|         'TMPDIR', | ||||
|         'VIMRUNTIME', | ||||
|       }) do | ||||
|         if not env_tbl[k] then | ||||
|           env_tbl[k] = os.getenv(k) | ||||
|         -- Set these from the environment unless the caller defined them. | ||||
|         if not env_opt[k] then | ||||
|           env_opt[k] = os.getenv(k) | ||||
|         end | ||||
|       end | ||||
|       env = {} | ||||
|       for k, v in pairs(env_tbl) do | ||||
|       for k, v in pairs(env_opt) do | ||||
|         env[#env + 1] = k .. '=' .. v | ||||
|       end | ||||
|     end | ||||
|     new_args = opts.args or {} | ||||
|     io_extra = opts.io_extra | ||||
|   else | ||||
|     new_args = {...} | ||||
|   end | ||||
|   for _, arg in ipairs(new_args) do | ||||
|     table.insert(args, arg) | ||||
|   | ||||
| @@ -266,8 +266,8 @@ describe('LSP', function() | ||||
|         end; | ||||
|         -- If the program timed out, then code will be nil. | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         -- Note that NIL must be used here. | ||||
|         -- on_handler(err, method, result, client_id) | ||||
| @@ -288,8 +288,8 @@ describe('LSP', function() | ||||
|           client.stop() | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(101, code, "exit code", fake_lsp_logfile)  -- See fake-lsp-server.lua | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(101, code, "exit code")  -- See fake-lsp-server.lua | ||||
|           eq(0, signal, "exit signal") | ||||
|           assert_log(pesc([[assert_eq failed: left == "\"shutdown\"", right == "\"test\""]]), | ||||
|             fake_lsp_logfile) | ||||
|         end; | ||||
| @@ -335,8 +335,8 @@ describe('LSP', function() | ||||
|           client.stop() | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         on_handler = function(...) | ||||
|           eq(table.remove(expected_handlers), {...}, "expected handler") | ||||
| @@ -367,8 +367,8 @@ describe('LSP', function() | ||||
|           client.notify('finish') | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         on_handler = function(err, result, ctx) | ||||
|           eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler") | ||||
| @@ -436,8 +436,8 @@ describe('LSP', function() | ||||
|           client = _client | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         on_handler = function(err, result, ctx) | ||||
|           eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler") | ||||
| @@ -496,8 +496,8 @@ describe('LSP', function() | ||||
|           eq(false, client.server_capabilities().codeLensProvider) | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         on_handler = function(...) | ||||
|           eq(table.remove(expected_handlers), {...}, "expected handler") | ||||
| @@ -517,8 +517,8 @@ describe('LSP', function() | ||||
|           client = c | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         on_handler = function(err, result, ctx) | ||||
|           eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler") | ||||
| @@ -547,8 +547,8 @@ describe('LSP', function() | ||||
|           client = c | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         on_handler = function(err, result, ctx) | ||||
|           eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler") | ||||
| @@ -596,8 +596,8 @@ describe('LSP', function() | ||||
|           eq(true, client.supports_method("unknown-method")) | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         on_handler = function(...) | ||||
|           eq(table.remove(expected_handlers), {...}, "expected handler") | ||||
| @@ -626,8 +626,8 @@ describe('LSP', function() | ||||
|           ]] | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         on_handler = function(...) | ||||
|           eq(table.remove(expected_handlers), {...}, "expected handler") | ||||
| @@ -651,8 +651,8 @@ describe('LSP', function() | ||||
|           exec_lua("vim.lsp.buf.type_definition()") | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         on_handler = function(...) | ||||
|           eq(table.remove(expected_handlers), {...}, "expected handler") | ||||
| @@ -672,8 +672,8 @@ describe('LSP', function() | ||||
|           client = _client | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|           eq(0, #expected_handlers, "did not call expected handler") | ||||
|         end; | ||||
|         on_handler = function(err, _, ctx) | ||||
| @@ -696,8 +696,8 @@ describe('LSP', function() | ||||
|           client = _client | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|           eq(0, #expected_handlers, "did not call expected handler") | ||||
|         end; | ||||
|         on_handler = function(err, _, ctx) | ||||
| @@ -726,8 +726,8 @@ describe('LSP', function() | ||||
|           client.notify("release") | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|           eq(0, #expected_handlers, "did not call expected handler") | ||||
|         end; | ||||
|         on_handler = function(err, _, ctx) | ||||
| @@ -759,8 +759,8 @@ describe('LSP', function() | ||||
|           client.notify("release") | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|           eq(0, #expected_handlers, "did not call expected handler") | ||||
|         end; | ||||
|         on_handler = function(err, _, ctx) | ||||
| @@ -793,8 +793,8 @@ describe('LSP', function() | ||||
|           client.notify("release") | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|           eq(0, #expected_handlers, "did not call expected handler") | ||||
|         end; | ||||
|         on_handler = function(err, _, ctx) | ||||
| @@ -828,8 +828,8 @@ describe('LSP', function() | ||||
|           client.notify("release") | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|           eq(0, #expected_handlers, "did not call expected handler") | ||||
|           eq(3, eval('g:requests')) | ||||
|         end; | ||||
| @@ -874,8 +874,8 @@ describe('LSP', function() | ||||
|           client.notify('finish') | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         on_handler = function(err, result, ctx) | ||||
|           eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler") | ||||
| @@ -917,8 +917,8 @@ describe('LSP', function() | ||||
|           ]] | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         on_handler = function(err, result, ctx) | ||||
|           if ctx.method == 'start' then | ||||
| @@ -960,8 +960,8 @@ describe('LSP', function() | ||||
|           ]] | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         on_handler = function(err, result, ctx) | ||||
|           if ctx.method == 'start' then | ||||
| @@ -1003,8 +1003,8 @@ describe('LSP', function() | ||||
|           ]] | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         on_handler = function(err, result, ctx) | ||||
|           if ctx.method == 'start' then | ||||
| @@ -1052,8 +1052,8 @@ describe('LSP', function() | ||||
|           ]] | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         on_handler = function(err, result, ctx) | ||||
|           if ctx.method == 'start' then | ||||
| @@ -1103,8 +1103,8 @@ describe('LSP', function() | ||||
|           ]] | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         on_handler = function(err, result, ctx) | ||||
|           if ctx.method == 'start' then | ||||
| @@ -1154,8 +1154,8 @@ describe('LSP', function() | ||||
|           ]] | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         on_handler = function(err, result, ctx) | ||||
|           if ctx.method == 'start' then | ||||
| @@ -1203,8 +1203,8 @@ describe('LSP', function() | ||||
|           ]] | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         on_handler = function(err, result, ctx) | ||||
|           if ctx.method == 'start' then | ||||
| @@ -1247,8 +1247,8 @@ describe('LSP', function() | ||||
|           ]] | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         on_handler = function(err, result, ctx) | ||||
|           if ctx.method == 'start' then | ||||
| @@ -1298,8 +1298,8 @@ describe('LSP', function() | ||||
|           ]] | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         on_handler = function(err, result,ctx) | ||||
|           if ctx.method == 'start' then | ||||
| @@ -1340,8 +1340,8 @@ describe('LSP', function() | ||||
|           client.stop(true) | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         on_handler = function(err, result, ctx) | ||||
|           eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler") | ||||
| @@ -1379,8 +1379,8 @@ describe('LSP', function() | ||||
|           ]] | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         on_handler = function(err, result, ctx) | ||||
|           eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler") | ||||
| @@ -1725,8 +1725,8 @@ describe('LSP', function() | ||||
|         end; | ||||
|         -- If the program timed out, then code will be nil. | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         -- Note that NIL must be used here. | ||||
|         -- on_handler(err, method, result, client_id) | ||||
| @@ -2728,8 +2728,8 @@ describe('LSP', function() | ||||
|           ]=]) | ||||
|         end; | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end; | ||||
|         on_handler = function(err, result, ctx) | ||||
|           -- Don't compare & assert params, they're not relevant for the testcase | ||||
| @@ -2768,8 +2768,8 @@ describe('LSP', function() | ||||
|         on_setup = function() | ||||
|         end, | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end, | ||||
|         on_handler = function(err, result, ctx) | ||||
|           eq(table.remove(expected_handlers), {err, result, ctx}) | ||||
| @@ -2846,8 +2846,8 @@ describe('LSP', function() | ||||
|         on_setup = function() | ||||
|         end, | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end, | ||||
|         on_handler = function(err, result, ctx) | ||||
|           eq(table.remove(expected_handlers), {err, result, ctx}) | ||||
| @@ -2919,8 +2919,8 @@ describe('LSP', function() | ||||
|         on_setup = function() | ||||
|         end, | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end, | ||||
|         on_handler = function(err, result, ctx) | ||||
|           eq(table.remove(expected_handlers), {err, result, ctx}) | ||||
| @@ -2985,8 +2985,8 @@ describe('LSP', function() | ||||
|             ]=]) | ||||
|         end, | ||||
|         on_exit = function(code, signal) | ||||
|           eq(0, code, "exit code", fake_lsp_logfile) | ||||
|           eq(0, signal, "exit signal", fake_lsp_logfile) | ||||
|           eq(0, code, "exit code") | ||||
|           eq(0, signal, "exit signal") | ||||
|         end, | ||||
|         on_handler = function(err, result, ctx) | ||||
|           eq(table.remove(expected_handlers), {err, result, ctx}) | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| -- Modules loaded here will not be cleared and reloaded by Busted. | ||||
| -- Modules loaded here will NOT be cleared and reloaded by Busted. | ||||
| -- Busted started doing this to help provide more isolation.  See issue #62 | ||||
| -- for more information about this. | ||||
| local helpers = require('test.functional.helpers')(nil) | ||||
| local iswin = helpers.iswin | ||||
| local busted = require("busted") | ||||
|  | ||||
| if iswin() then | ||||
|   local ffi = require('ffi') | ||||
| @@ -12,3 +13,28 @@ if iswin() then | ||||
|   ]] | ||||
|   ffi.C._set_fmode(0x8000) | ||||
| end | ||||
|  | ||||
| local testid = (function() | ||||
|   local id = 0 | ||||
|   return (function() | ||||
|     id = id + 1 | ||||
|     return id | ||||
|   end) | ||||
| end)() | ||||
|  | ||||
| -- Global before_each. https://github.com/Olivine-Labs/busted/issues/613 | ||||
| local function before_each(_element, _parent) | ||||
|   local id = ('T%d'):format(testid()) | ||||
|   _G._nvim_test_id = id | ||||
|   return nil, true | ||||
| end | ||||
| busted.subscribe({ 'test', 'start' }, | ||||
|   before_each, | ||||
|   { | ||||
|     -- Ensure our --helper is handled before --output (see busted/runner.lua). | ||||
|     priority = 1, | ||||
|     -- Don't generate a test-id for skipped tests. /shrug | ||||
|     predicate = function (element, _, status) | ||||
|       return not ((element.descriptor == 'pending' or status == 'pending')) | ||||
|     end | ||||
|   }) | ||||
|   | ||||
| @@ -6,7 +6,7 @@ if helpers.pending_win32(pending) then return end | ||||
|  | ||||
| describe('api', function() | ||||
|   local screen | ||||
|   local socket_name = "Xtest_functional_api.sock" | ||||
|   local socket_name = "./Xtest_functional_api.sock" | ||||
|  | ||||
|   before_each(function() | ||||
|     helpers.clear() | ||||
| @@ -29,7 +29,7 @@ describe('api', function() | ||||
|       {4:~                                                 }| | ||||
|       {4:~                                                 }| | ||||
|       {4:~                                                 }| | ||||
|       ]]..socket_name..[[                         | | ||||
|       ]]..socket_name..[[                       | | ||||
|       {3:-- TERMINAL --}                                    | | ||||
|     ]]) | ||||
|  | ||||
|   | ||||
| @@ -47,33 +47,33 @@ describe(':let', function() | ||||
|   end) | ||||
|  | ||||
|   it("multibyte env var #8398 #9267", function() | ||||
|     command("let $NVIM_TEST = 'AìaB'") | ||||
|     eq('AìaB', eval('$NVIM_TEST')) | ||||
|     command("let $NVIM_TEST = 'AaあB'") | ||||
|     eq('AaあB', eval('$NVIM_TEST')) | ||||
|     command("let $NVIM_TEST_LET = 'AìaB'") | ||||
|     eq('AìaB', eval('$NVIM_TEST_LET')) | ||||
|     command("let $NVIM_TEST_LET = 'AaあB'") | ||||
|     eq('AaあB', eval('$NVIM_TEST_LET')) | ||||
|     local mbyte = [[\p* .ม .ม .ม .ม่ .ม่ .ม่ ֹ ֹ ֹ .ֹ .ֹ .ֹ ֹֻ ֹֻ ֹֻ | ||||
|                     .ֹֻ .ֹֻ .ֹֻ ֹֻ ֹֻ ֹֻ .ֹֻ .ֹֻ .ֹֻ ֹ ֹ ֹ .ֹ .ֹ .ֹ ֹ ֹ ֹ .ֹ .ֹ .ֹ ֹֻ ֹֻ | ||||
|                     .ֹֻ .ֹֻ .ֹֻ a a a ca ca ca à à à]] | ||||
|     command("let $NVIM_TEST = '"..mbyte.."'") | ||||
|     eq(mbyte, eval('$NVIM_TEST')) | ||||
|     command("let $NVIM_TEST_LET = '"..mbyte.."'") | ||||
|     eq(mbyte, eval('$NVIM_TEST_LET')) | ||||
|   end) | ||||
|  | ||||
|   it("multibyte env var to child process #8398 #9267",  function() | ||||
|     local cmd_get_child_env = "let g:env_from_child = system(['"..nvim_dir.."/printenv-test', 'NVIM_TEST'])" | ||||
|     command("let $NVIM_TEST = 'AìaB'") | ||||
|     local cmd_get_child_env = "let g:env_from_child = system(['"..nvim_dir.."/printenv-test', 'NVIM_TEST_LET'])" | ||||
|     command("let $NVIM_TEST_LET = 'AìaB'") | ||||
|     command(cmd_get_child_env) | ||||
|     eq(eval('$NVIM_TEST'), eval('g:env_from_child')) | ||||
|     eq(eval('$NVIM_TEST_LET'), eval('g:env_from_child')) | ||||
|  | ||||
|     command("let $NVIM_TEST = 'AaあB'") | ||||
|     command("let $NVIM_TEST_LET = 'AaあB'") | ||||
|     command(cmd_get_child_env) | ||||
|     eq(eval('$NVIM_TEST'), eval('g:env_from_child')) | ||||
|     eq(eval('$NVIM_TEST_LET'), eval('g:env_from_child')) | ||||
|  | ||||
|     local mbyte = [[\p* .ม .ม .ม .ม่ .ม่ .ม่ ֹ ֹ ֹ .ֹ .ֹ .ֹ ֹֻ ֹֻ ֹֻ | ||||
|                     .ֹֻ .ֹֻ .ֹֻ ֹֻ ֹֻ ֹֻ .ֹֻ .ֹֻ .ֹֻ ֹ ֹ ֹ .ֹ .ֹ .ֹ ֹ ֹ ֹ .ֹ .ֹ .ֹ ֹֻ ֹֻ | ||||
|                     .ֹֻ .ֹֻ .ֹֻ a a a ca ca ca à à à]] | ||||
|     command("let $NVIM_TEST = '"..mbyte.."'") | ||||
|     command("let $NVIM_TEST_LET = '"..mbyte.."'") | ||||
|     command(cmd_get_child_env) | ||||
|     eq(eval('$NVIM_TEST'), eval('g:env_from_child')) | ||||
|     eq(eval('$NVIM_TEST_LET'), eval('g:env_from_child')) | ||||
|   end) | ||||
|  | ||||
|   it("release of list assigned to l: variable does not trigger assertion #12387, #12430", function() | ||||
|   | ||||
| @@ -30,7 +30,7 @@ describe('server', function() | ||||
|     eq('', eval('$NVIM_LISTEN_ADDRESS')) | ||||
|     local servers = funcs.serverlist() | ||||
|     eq(1, #servers) | ||||
|     ok(string.len(servers[1]) > 4)  -- Like /tmp/nvim…/… or \\.\pipe\… | ||||
|     ok(string.len(servers[1]) > 4)  -- "~/.local/state/nvim…/…" or "\\.\pipe\…" | ||||
|   end) | ||||
|  | ||||
|   it('sets v:servername at startup or if all servers were stopped', function() | ||||
| @@ -54,7 +54,7 @@ describe('server', function() | ||||
|  | ||||
|     -- v:servername and $NVIM take the next available server. | ||||
|     local servername = (iswin() and [[\\.\pipe\Xtest-functional-server-pipe]] | ||||
|                                 or 'Xtest-functional-server-socket') | ||||
|                                 or './Xtest-functional-server-socket') | ||||
|     funcs.serverstart(servername) | ||||
|     eq(servername, meths.get_vvar('servername')) | ||||
|     -- Not set in the current process, only in children. | ||||
| @@ -66,7 +66,7 @@ describe('server', function() | ||||
|     eq(0, eval("serverstop('bogus-socket-name')")) | ||||
|   end) | ||||
|  | ||||
|   it('parses endpoints correctly', function() | ||||
|   it('parses endpoints', function() | ||||
|     clear_serverlist() | ||||
|     eq({}, funcs.serverlist()) | ||||
|  | ||||
| @@ -101,6 +101,10 @@ describe('server', function() | ||||
|     eq(expected, funcs.serverlist()) | ||||
|     clear_serverlist() | ||||
|  | ||||
|     -- Address without slashes is a "name" which is appended to a generated path. #8519 | ||||
|     matches([[.*[/\\]xtest1%.2%.3%.4[^/\\]*]], funcs.serverstart('xtest1.2.3.4')) | ||||
|     clear_serverlist() | ||||
|  | ||||
|     eq('Vim:Failed to start server: invalid argument', | ||||
|       pcall_err(funcs.serverstart, '127.0.0.1:65536'))  -- invalid port | ||||
|     eq({}, funcs.serverlist()) | ||||
| @@ -113,7 +117,7 @@ describe('server', function() | ||||
|     -- Add some servers. | ||||
|     local servs = (iswin() | ||||
|       and { [[\\.\pipe\Xtest-pipe0934]], [[\\.\pipe\Xtest-pipe4324]] } | ||||
|       or  { [[Xtest-pipe0934]], [[Xtest-pipe4324]] }) | ||||
|       or  { [[./Xtest-pipe0934]], [[./Xtest-pipe4324]] }) | ||||
|     for _, s in ipairs(servs) do | ||||
|       eq(s, eval("serverstart('"..s.."')")) | ||||
|     end | ||||
| @@ -146,9 +150,13 @@ describe('startup --listen', function() | ||||
|  | ||||
|   it('sets v:servername, overrides $NVIM_LISTEN_ADDRESS', function() | ||||
|     local addr = (iswin() and [[\\.\pipe\Xtest-listen-pipe]] | ||||
|                           or 'Xtest-listen-pipe') | ||||
|     clear({ env={ NVIM_LISTEN_ADDRESS='Xtest-env-pipe' }, | ||||
|                           or './Xtest-listen-pipe') | ||||
|     clear({ env={ NVIM_LISTEN_ADDRESS='./Xtest-env-pipe' }, | ||||
|             args={ '--listen', addr } }) | ||||
|     eq(addr, meths.get_vvar('servername')) | ||||
|  | ||||
|     -- Address without slashes is a "name" which is appended to a generated path. #8519 | ||||
|     clear({ args={ '--listen', 'test-name' } }) | ||||
|     matches([[.*[/\\]test%-name[^/\\]*]], meths.get_vvar('servername')) | ||||
|   end) | ||||
| end) | ||||
|   | ||||
| @@ -40,10 +40,6 @@ function module.popen_r(...) | ||||
|   return io.popen(module.argss_to_cmd(...), 'r') | ||||
| end | ||||
|  | ||||
| function module.popen_w(...) | ||||
|   return io.popen(module.argss_to_cmd(...), 'w') | ||||
| end | ||||
|  | ||||
| -- sleeps the test runner (_not_ the nvim instance) | ||||
| function module.sleep(ms) | ||||
|   luv.sleep(ms) | ||||
| @@ -55,42 +51,23 @@ local check_logs_useless_lines = { | ||||
|   ['See README_MISSING_SYSCALL_OR_IOCTL for guidance']=3, | ||||
| } | ||||
|  | ||||
| --- Invokes `fn` and includes the tail of `logfile` in the error message if it | ||||
| --- fails. | ||||
| --- | ||||
| ---@param logfile string  Log file, defaults to $NVIM_LOG_FILE or '.nvimlog' | ||||
| ---@param fn string       Function to invoke | ||||
| ---@param ... string      Function arguments | ||||
| local function dumplog(logfile, fn, ...) | ||||
|   -- module.validate({ | ||||
|   --   logfile={logfile,'s',true}, | ||||
|   --   fn={fn,'f',false}, | ||||
|   -- }) | ||||
|   local status, rv = pcall(fn, ...) | ||||
|   if status == false then | ||||
|     logfile = logfile or os.getenv('NVIM_LOG_FILE') or '.nvimlog' | ||||
|     local logtail = module.read_nvim_log(logfile) | ||||
|     error(string.format('%s\n%s', tostring(rv), logtail)) | ||||
|   end | ||||
| function module.eq(expected, actual, context) | ||||
|   return assert.are.same(expected, actual, context) | ||||
| end | ||||
| function module.eq(expected, actual, context, logfile) | ||||
|   return dumplog(logfile, assert.are.same, expected, actual, context) | ||||
| function module.neq(expected, actual, context) | ||||
|   return assert.are_not.same(expected, actual, context) | ||||
| end | ||||
| function module.neq(expected, actual, context, logfile) | ||||
|   return dumplog(logfile, assert.are_not.same, expected, actual, context) | ||||
| end | ||||
| function module.ok(res, msg, logfile) | ||||
|   return dumplog(logfile, assert.is_true, res, msg) | ||||
| function module.ok(res, msg) | ||||
|   return assert.is_true(res, msg) | ||||
| end | ||||
|  | ||||
| -- TODO(bfredl): this should "failure" not "error" (issue with dumplog() ) | ||||
| local function epicfail(state, arguments, _) | ||||
|   state.failure_message = arguments[1] | ||||
|   return false | ||||
| end | ||||
| assert:register("assertion", "epicfail", epicfail) | ||||
| function module.fail(msg, logfile) | ||||
|   return dumplog(logfile, assert.epicfail, msg) | ||||
| function module.fail(msg) | ||||
|   return assert.epicfail(msg) | ||||
| end | ||||
|  | ||||
| function module.matches(pat, actual) | ||||
| @@ -104,16 +81,16 @@ end | ||||
| --- | ||||
| ---@param pat string      Lua pattern to search for in the log file | ||||
| ---@param logfile string  Full path to log file (default=$NVIM_LOG_FILE) | ||||
| function module.assert_log(pat, logfile) | ||||
| ---@param nrlines number  Search up to this many log lines | ||||
| function module.assert_log(pat, logfile, nrlines) | ||||
|   logfile = logfile or os.getenv('NVIM_LOG_FILE') or '.nvimlog' | ||||
|   local nrlines = 10 | ||||
|   nrlines = nrlines or 10 | ||||
|   local lines = module.read_file_list(logfile, -nrlines) or {} | ||||
|   for _,line in ipairs(lines) do | ||||
|     if line:match(pat) then return end | ||||
|   end | ||||
|   local logtail = module.read_nvim_log(logfile) | ||||
|   error(string.format('Pattern %q not found in log (last %d lines): %s:\n%s', | ||||
|     pat, nrlines, logfile, logtail)) | ||||
|     pat, nrlines, logfile, '    '..table.concat(lines, '\n    '))) | ||||
| end | ||||
|  | ||||
| -- Invokes `fn` and returns the error string (with truncated paths), or raises | ||||
| @@ -271,7 +248,7 @@ module.uname = (function() | ||||
|       return platform | ||||
|     end | ||||
|  | ||||
|     if os.getenv("SYSTEM_NAME") then  -- From CMAKE_SYSTEM_NAME. | ||||
|     if os.getenv("SYSTEM_NAME") then  -- From CMAKE_HOST_SYSTEM_NAME. | ||||
|       platform = string.lower(os.getenv("SYSTEM_NAME")) | ||||
|       return platform | ||||
|     end | ||||
| @@ -409,17 +386,6 @@ function module.check_cores(app, force) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function module.which(exe) | ||||
|   local pipe = module.popen_r('which', exe) | ||||
|   local ret = pipe:read('*a') | ||||
|   pipe:close() | ||||
|   if ret == '' then | ||||
|     return nil | ||||
|   else | ||||
|     return ret:sub(1, -2) | ||||
|   end | ||||
| end | ||||
|  | ||||
| function module.repeated_read_cmd(...) | ||||
|   for _ = 1, 10 do | ||||
|     local stream = module.popen_r(...) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Justin M. Keyes
					Justin M. Keyes