From 787df87badf606fe6589d17b9728dfe58cb524b5 Mon Sep 17 00:00:00 2001 From: Jacek Caban Date: Fri, 4 Sep 2020 13:50:38 +0200 Subject: [PATCH] conhost/tests: Add initial tty tests. Signed-off-by: Jacek Caban Signed-off-by: Alexandre Julliard --- configure | 1 + configure.ac | 1 + programs/conhost/tests/Makefile.in | 6 + programs/conhost/tests/tty.c | 298 +++++++++++++++++++++++++++++ 4 files changed, 306 insertions(+) create mode 100644 programs/conhost/tests/Makefile.in create mode 100644 programs/conhost/tests/tty.c diff --git a/configure b/configure index 538218fda15..30aa531846b 100755 --- a/configure +++ b/configure @@ -21452,6 +21452,7 @@ wine_fn_config_makefile programs/clock enable_clock wine_fn_config_makefile programs/cmd enable_cmd wine_fn_config_makefile programs/cmd/tests enable_tests wine_fn_config_makefile programs/conhost enable_conhost +wine_fn_config_makefile programs/conhost/tests enable_tests wine_fn_config_makefile programs/control enable_control wine_fn_config_makefile programs/cscript enable_cscript wine_fn_config_makefile programs/dism enable_dism diff --git a/configure.ac b/configure.ac index f60cd593549..006087e05ec 100644 --- a/configure.ac +++ b/configure.ac @@ -3974,6 +3974,7 @@ WINE_CONFIG_MAKEFILE(programs/clock) WINE_CONFIG_MAKEFILE(programs/cmd) WINE_CONFIG_MAKEFILE(programs/cmd/tests) WINE_CONFIG_MAKEFILE(programs/conhost) +WINE_CONFIG_MAKEFILE(programs/conhost/tests) WINE_CONFIG_MAKEFILE(programs/control) WINE_CONFIG_MAKEFILE(programs/cscript) WINE_CONFIG_MAKEFILE(programs/dism) diff --git a/programs/conhost/tests/Makefile.in b/programs/conhost/tests/Makefile.in new file mode 100644 index 00000000000..ecaf68fe4b2 --- /dev/null +++ b/programs/conhost/tests/Makefile.in @@ -0,0 +1,6 @@ +TESTDLL = conhost.exe +IMPORTS = user32 + + +C_SRCS = \ + tty.c diff --git a/programs/conhost/tests/tty.c b/programs/conhost/tests/tty.c new file mode 100644 index 00000000000..75b695b2269 --- /dev/null +++ b/programs/conhost/tests/tty.c @@ -0,0 +1,298 @@ +/* + * 2020 Jacek Caban for CodeWeavers + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include "wine/test.h" + +#include + +static HRESULT (WINAPI *pCreatePseudoConsole)(COORD,HANDLE,HANDLE,DWORD,HPCON*); +static void (WINAPI *pClosePseudoConsole)(HPCON); + +static char console_output[4096]; +static unsigned int console_output_count; +static HANDLE console_pipe; +static HANDLE child_pipe; + +#define fetch_console_output() fetch_console_output_(__LINE__) +static void fetch_console_output_(unsigned int line) +{ + OVERLAPPED o; + DWORD count; + BOOL ret; + + if (console_output_count == sizeof(console_output)) return; + + o.hEvent = CreateEventW(NULL, TRUE, FALSE, NULL); + ret = ReadFile(console_pipe, console_output + console_output_count, + sizeof(console_output) - console_output_count, NULL, &o); + if (!ret) + { + ok_(__FILE__,line)(GetLastError() == ERROR_IO_PENDING, "read failed: %u\n", GetLastError()); + if (GetLastError() != ERROR_IO_PENDING) return; + WaitForSingleObject(o.hEvent, 1000); + } + ret = GetOverlappedResult(console_pipe, &o, &count, FALSE); + if (!ret && GetLastError() == ERROR_IO_PENDING) + CancelIoEx(console_pipe, &o); + + ok_(__FILE__,line)(ret, "Read file failed: %u\n", GetLastError()); + CloseHandle(o.hEvent); + if (ret) console_output_count += count; +} + +#define expect_empty_output() expect_empty_output_(__LINE__) +static void expect_empty_output_(unsigned int line) +{ + DWORD avail; + BOOL ret; + + ret = PeekNamedPipe(console_pipe, NULL, 0, NULL, &avail, NULL); + ok_(__FILE__,line)(ret, "PeekNamedPipe failed: %u\n", GetLastError()); + ok_(__FILE__,line)(!avail, "avail = %u\n", avail); + if (avail) fetch_console_output_(line); + ok_(__FILE__,line)(!console_output_count, "expected empty buffer, got %s\n", + wine_dbgstr_an(console_output, console_output_count)); + console_output_count = 0; +} + +#define expect_output_sequence(a) expect_output_sequence_(__LINE__,0,a) +#define expect_output_sequence_ctx(a,b) expect_output_sequence_(__LINE__,a,b) +static void expect_output_sequence_(unsigned int line, unsigned ctx, const char *expect) +{ + size_t len = strlen(expect); + if (console_output_count < len) fetch_console_output_(line); + if (len <= console_output_count && !memcmp(console_output, expect, len)) + { + console_output_count -= len; + memmove(console_output, console_output + len, console_output_count); + } + else ok_(__FILE__,line)(0, "%x: expected %s got %s\n", ctx, wine_dbgstr_a(expect), + wine_dbgstr_an(console_output, console_output_count)); +} + +#define skip_sequence(a) skip_sequence_(__LINE__,a) +static BOOL skip_sequence_(unsigned int line, const char *expect) +{ + size_t len = strlen(expect); + DWORD avail; + BOOL r; + + r = PeekNamedPipe(console_pipe, NULL, 0, NULL, &avail, NULL); + if (!console_output_count && r && !avail) + { + Sleep(50); + r = PeekNamedPipe(console_pipe, NULL, 0, NULL, &avail, NULL); + } + if (r && avail) fetch_console_output_(line); + + if (len > console_output_count || memcmp(console_output, expect, len)) return FALSE; + console_output_count -= len; + memmove(console_output, console_output + len, console_output_count); + return TRUE; +} + +#define skip_byte(a) skip_byte_(__LINE__,a) +static BOOL skip_byte_(unsigned int line, char ch) +{ + if (!console_output_count || console_output[0] != ch) return FALSE; + console_output_count--; + memmove(console_output, console_output + 1, console_output_count); + return TRUE; +} + +#define skip_hide_cursor() skip_hide_cursor_(__LINE__) +static BOOL skip_hide_cursor_(unsigned int line) +{ + if (!console_output_count) fetch_console_output_(line); + return skip_sequence_(line, "\x1b[25l") || broken(skip_sequence_(line, "\x1b[?25l")); +} + +enum req_type +{ + REQ_SET_TITLE, +}; + +struct pseudoconsole_req +{ + enum req_type type; + union + { + WCHAR string[1]; + } u; +}; + +static void child_string_request(enum req_type type, const WCHAR *title) +{ + char buf[4096]; + struct pseudoconsole_req *req = (void *)buf; + size_t len = lstrlenW(title) + 1; + DWORD count; + BOOL ret; + + req->type = type; + memcpy(req->u.string, title, len * sizeof(WCHAR)); + ret = WriteFile(child_pipe, req, FIELD_OFFSET(struct pseudoconsole_req, u.string[len]), + &count, NULL); + ok(ret, "WriteFile failed: %u\n", GetLastError()); +} + +static void child_process(HANDLE pipe) +{ + HANDLE output, input; + char buf[4096]; + DWORD size; + BOOL ret; + + output = CreateFileA("CONOUT$", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, 0); + ok(output != INVALID_HANDLE_VALUE, "could not open console output\n"); + + input = CreateFileA("CONIN$", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, 0); + ok(output != INVALID_HANDLE_VALUE, "could not open console output\n"); + + while(ReadFile(pipe, buf, sizeof(buf), &size, NULL)) + { + const struct pseudoconsole_req *req = (void *)buf; + switch (req->type) + { + case REQ_SET_TITLE: + ret = SetConsoleTitleW(req->u.string); + ok(ret, "SetConsoleTitleW failed: %u\n", GetLastError()); + break; + + default: + ok(0, "unexpected request type %u\n", req->type); + }; + } + ok(GetLastError() == ERROR_BROKEN_PIPE, "ReadFile failed: %u\n", GetLastError()); + CloseHandle(output); + CloseHandle(input); +} + +static HANDLE run_child(HANDLE console, HANDLE pipe) +{ + STARTUPINFOEXA startup = {{ sizeof(startup) }}; + char **argv, cmdline[MAX_PATH]; + PROCESS_INFORMATION info; + SIZE_T size; + BOOL ret; + + InitializeProcThreadAttributeList(NULL, 1, 0, &size); + startup.lpAttributeList = HeapAlloc(GetProcessHeap(), 0, size); + InitializeProcThreadAttributeList(startup.lpAttributeList, 1, 0, &size); + UpdateProcThreadAttribute(startup.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, console, + sizeof(console), NULL, NULL); + + winetest_get_mainargs(&argv); + sprintf(cmdline, "\"%s\" %s child %p", argv[0], argv[1], pipe); + ret = CreateProcessA(NULL, cmdline, NULL, NULL, TRUE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, + &startup.StartupInfo, &info); + ok(ret, "CreateProcessW failed: %u\n", GetLastError()); + + CloseHandle(info.hThread); + HeapFree(GetProcessHeap(), 0, startup.lpAttributeList); + return info.hProcess; +} + +static HPCON create_pseudo_console(HANDLE *console_pipe_end, HANDLE *child_process) +{ + SECURITY_ATTRIBUTES sec_attr = { sizeof(sec_attr), NULL, TRUE }; + HANDLE child_pipe_end; + COORD size = { 30, 40 }; + DWORD read_mode; + HPCON console; + HRESULT hres; + BOOL r; + + console_pipe = CreateNamedPipeW(L"\\\\.\\pipe\\pseudoconsoleconn", PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, + PIPE_WAIT | PIPE_TYPE_BYTE, 1, 4096, 4096, NMPWAIT_USE_DEFAULT_WAIT, NULL); + ok(console_pipe != INVALID_HANDLE_VALUE, "CreateNamedPipeW failed: %u\n", GetLastError()); + + *console_pipe_end = CreateFileW(L"\\\\.\\pipe\\pseudoconsoleconn", GENERIC_READ | GENERIC_WRITE, + 0, &sec_attr, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); + ok(*console_pipe_end != INVALID_HANDLE_VALUE, "CreateFile failed: %u\n", GetLastError()); + + child_pipe = CreateNamedPipeW(L"\\\\.\\pipe\\pseudoconsoleserver", PIPE_ACCESS_DUPLEX, + PIPE_WAIT | PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE, 1, 5000, 6000, + NMPWAIT_USE_DEFAULT_WAIT, NULL); + ok(child_pipe != INVALID_HANDLE_VALUE, "CreateNamedPipeW failed: %u\n", GetLastError()); + + child_pipe_end = CreateFileW(L"\\\\.\\pipe\\pseudoconsoleserver", GENERIC_READ | GENERIC_WRITE, 0, + &sec_attr, OPEN_EXISTING, 0, NULL); + ok(child_pipe_end != INVALID_HANDLE_VALUE, "CreateFile failed: %u\n", GetLastError()); + + read_mode = PIPE_READMODE_MESSAGE; + r = SetNamedPipeHandleState(child_pipe_end, &read_mode, NULL, NULL); + ok(r, "SetNamedPipeHandleState failed: %u\n", GetLastError()); + + hres = pCreatePseudoConsole(size, *console_pipe_end, *console_pipe_end, 0, &console); + ok(hres == S_OK, "CreatePseudoConsole failed: %08x\n", hres); + + *child_process = run_child(console, child_pipe_end); + CloseHandle(child_pipe_end); + return console; +} + +static void test_pseudoconsole(void) +{ + HANDLE console_pipe_end, child_process; + HPCON console; + + console = create_pseudo_console(&console_pipe_end, &child_process); + + child_string_request(REQ_SET_TITLE, L"test title"); + expect_output_sequence("\x1b[2J"); /* erase display */ + skip_hide_cursor(); + expect_output_sequence("\x1b[m"); /* default attributes */ + expect_output_sequence("\x1b[H"); /* set cursor */ + skip_sequence("\x1b[H"); /* some windows versions emit it twice */ + expect_output_sequence("\x1b]0;test title"); /* set title */ + skip_byte(0); /* some win versions emit nullbyte */ + expect_output_sequence("\x07"); + skip_sequence("\x1b[?25h"); /* show cursor */ + expect_empty_output(); + + pClosePseudoConsole(console); + CloseHandle(console_pipe_end); + CloseHandle(console_pipe); +} + +START_TEST(tty) +{ + HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll"); + char **argv; + int argc; + + argc = winetest_get_mainargs(&argv); + if (argc > 3) + { + HANDLE pipe; + sscanf(argv[3], "%p", &pipe); + child_process(pipe); + return; + } + + pCreatePseudoConsole = (void *)GetProcAddress(kernel32, "CreatePseudoConsole"); + pClosePseudoConsole = (void *)GetProcAddress(kernel32, "ClosePseudoConsole"); + if (!pCreatePseudoConsole) + { + win_skip("CreatePseudoConsole is not available\n"); + return; + } + + test_pseudoconsole(); +}