From: Eric Pouech epouech@codeweavers.com
Signed-off-by: Eric Pouech epouech@codeweavers.com --- dlls/ucrtbase/tests/Makefile.in | 1 + dlls/ucrtbase/tests/file.c | 352 ++++++++++++++++++++++++++++++++ 2 files changed, 353 insertions(+) create mode 100644 dlls/ucrtbase/tests/file.c
diff --git a/dlls/ucrtbase/tests/Makefile.in b/dlls/ucrtbase/tests/Makefile.in index cd01a3aacb9..ea9e589113c 100644 --- a/dlls/ucrtbase/tests/Makefile.in +++ b/dlls/ucrtbase/tests/Makefile.in @@ -5,6 +5,7 @@ EXTRADEFS = -fno-builtin SOURCES = \ cpp.c \ environ.c \ + file.c \ misc.c \ printf.c \ scanf.c \ diff --git a/dlls/ucrtbase/tests/file.c b/dlls/ucrtbase/tests/file.c new file mode 100644 index 00000000000..97f2f82ff75 --- /dev/null +++ b/dlls/ucrtbase/tests/file.c @@ -0,0 +1,352 @@ +/* + * Unit test suite for file functions + * + * Copyright 2024 Eric Pouech 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 <stdlib.h> +#include <stdio.h> +#include <fcntl.h> +#include <share.h> +#include <sys/stat.h> +#include <io.h> +#include <direct.h> +#include <windef.h> +#include <winbase.h> +#include <winnls.h> +#include <winreg.h> +#include <process.h> +#include <locale.h> +#include <winternl.h> + +static unsigned __cdecl winetest_log(const char *format, ...) +{ + const char *logname = "file.log"; + va_list args; + FILE *log; + unsigned ret = -1; + + if (!strcmp( format, "delete" )) + return DeleteFileA( logname ); + if (!strcmp( format, "trace" )) + { + if ((log = fopen( logname, "r" ))) + { + char tmp[256]; + + while (fgets( tmp, ARRAY_SIZE(tmp), log )) + trace( "%s", tmp ); + fclose( log ); + return 1; + } + return 0; + } + va_start( args, format ); + if ((log = fopen( logname, "a" ))) + { + ret = vfprintf( log, format, args ); + fclose( log ); + } + va_end( args ); + return ret; +} + +static void create_file(const char *filename, const char *content) +{ + DWORD ret; + HANDLE file = CreateFileA( filename, GENERIC_WRITE, + FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, 0, NULL ); + ok( file != INVALID_HANDLE_VALUE, "Couldn't create file\n" ); + if (content) + { + DWORD nw; + ret = WriteFile( file, content, strlen( content ), &nw, NULL ); + ok( ret && nw == strlen( content ), "Couldn't write file\n" ); + } + CloseHandle( file ); +} + +enum buffer_state +{ + BS_NO_BUFFER, + BS_CHAR_BUFFER, + BS_FULL_BUFFER, + BS_ERROR, +}; + +struct UCRT_FILE +{ + char *_ptr; + char *_base; + int _cnt; + int _flags; + int _file; + int _charbuf; + int _bufsiz; +}; + +static enum buffer_state get_buffer_state(FILE *f) +{ + unsigned bufsize = 0xffffffff; + char *base = NULL; + char *charbuf = (char*)(f - 1); /* so that it's not inside f structure */ + + if (!f) return BS_ERROR; + + if (winetest_platform_is_wine) + { + bufsize = f->_bufsiz; + base = f->_base; + charbuf = (char*)&f->_charbuf; + } + else + { + struct UCRT_FILE *uf = (struct UCRT_FILE*)f; + + bufsize = uf->_bufsiz; + base = uf->_base; + charbuf = (char*)&uf->_charbuf; + } + /* we could expect bufsize to be 0, but it's sometimes 2 */ + if (base == NULL && bufsize != 4096) return BS_NO_BUFFER; + if (base == charbuf && bufsize == 2) return BS_CHAR_BUFFER; + if (base != NULL && bufsize == 4096) return BS_FULL_BUFFER; + winetest_log( "Can't guess buffer state: bufsize=%04u base=%p\n", bufsize, base ); + return BS_ERROR; +} + +static BOOL check_buffer_state_discovery(void) +{ + static char file_test_buffer[4096]; + unsigned cond = 0; + FILE *file_test = fopen( "foo.tmp", "w" ); + errno_t err; + + /* check that get_buffer_state is coherent */ + if (!file_test) return FALSE; + + winetest_log("delete"); + + if (get_buffer_state( file_test ) == BS_NO_BUFFER) cond++; + err = setvbuf( file_test, NULL, _IONBF, 0 ); + ok( err == 0, "setvbuf failed\n" ); + if (get_buffer_state( file_test ) == BS_CHAR_BUFFER) cond++; + err = setvbuf( file_test, file_test_buffer, _IOFBF, sizeof(file_test_buffer) ); + ok( err == 0, "setvbuf failed\n" ); + if (get_buffer_state( file_test ) == BS_FULL_BUFFER) cond++; + err = setvbuf( file_test, file_test_buffer, _IOLBF, sizeof(file_test_buffer) ); + ok( err == 0, "setvbuf failed\n" ); + if (get_buffer_state( file_test ) == BS_FULL_BUFFER) cond++; + fclose( file_test ); + + if (cond != 4) + { + win_skip("Incompatible Windows platform, skipping test\n"); + trace("DLL2 ucrt=%u msvcrt=%u wine=%u\n", + GetModuleHandleA("ucrtbase.dll") != NULL, + GetModuleHandleA("msvcrt.dll") != NULL, + winetest_platform_is_wine); + winetest_log("trace"); + return FALSE; + } + return TRUE; +} + +/* We could test more types: + * - pipes: + * - console: we assume that NUL and console behave similary (they are of FILE_TYPE_CHAR) + */ +enum handle_creation {H_FILE, H_NUL, H_REOPEN = 0x10, H_KIND_MASK = 0x0F}; + +static void test_std_buffer_state(const char *selfname) +{ + static struct + { + /* input */ + unsigned type; + /* output */ + enum buffer_state std_buffer_state[3]; + int broken_stderr; /* win7,8 and early win10 require it (at least on files) */ + BOOL with_todo; + } + test_file_type[] = + { + /* inheriting from parent */ + /* Note: wine/test.h calls set_vbuf(_IONBF) (even in child) + * hence the result BS_CHAR_BUFFER for stdout without reopen + */ + {H_FILE, {BS_FULL_BUFFER, BS_CHAR_BUFFER, BS_NO_BUFFER}, BS_FULL_BUFFER, TRUE}, + {H_NUL, {BS_FULL_BUFFER, BS_CHAR_BUFFER, BS_NO_BUFFER}, -1}, + /* reopen in child */ + {H_FILE | H_REOPEN, {BS_FULL_BUFFER, BS_FULL_BUFFER, BS_NO_BUFFER}, BS_FULL_BUFFER, TRUE}, + {H_NUL | H_REOPEN, {BS_FULL_BUFFER, BS_NO_BUFFER, BS_NO_BUFFER}, -1}, + }; + char cmdline[MAX_PATH]; + SECURITY_ATTRIBUTES sa = {.nLength = sizeof(sa), .lpSecurityDescriptor = NULL, .bInheritHandle = TRUE}; + STARTUPINFOA startup; + PROCESS_INFORMATION proc; + DWORD exit_code, expected_exit_code, broken_exit_code; + unsigned i; + BOOL ret; + + /* check that get_buffer_state is coherent */ + if (!check_buffer_state_discovery()) return; + + for (i = 0; i < ARRAY_SIZE(test_file_type); i++) + { + winetest_push_context( "stdbuf[%d]", i) ; + memset( &startup, 0, sizeof(startup) ); + startup.cb = sizeof(startup); + + winetest_log("delete"); + + switch ((int)test_file_type[i].type) + { + case H_FILE: + /* so that we can read from stdin */ + create_file( "file0.tmp", "first content\r\n" ); + startup.hStdInput = CreateFileA( "file0.tmp", GENERIC_READ, + FILE_SHARE_READ, &sa, OPEN_EXISTING, 0, NULL ); + startup.hStdOutput = CreateFileA( "file1.tmp", GENERIC_WRITE, + FILE_SHARE_WRITE, &sa, CREATE_ALWAYS, 0, NULL ); + startup.hStdError = CreateFileA( "file2.tmp", GENERIC_WRITE, + FILE_SHARE_WRITE, &sa, CREATE_ALWAYS, 0, NULL ); + startup.dwFlags = STARTF_USESTDHANDLES; + break; + case H_NUL: + startup.hStdInput = CreateFileA( "NUL", GENERIC_READ, + FILE_SHARE_READ, &sa, OPEN_EXISTING, 0, NULL ); + startup.hStdOutput = CreateFileA( "NUL", GENERIC_WRITE, + FILE_SHARE_WRITE, &sa, CREATE_ALWAYS, 0, NULL ); + startup.hStdError = CreateFileA( "NUL", GENERIC_WRITE, + FILE_SHARE_WRITE, &sa, CREATE_ALWAYS, 0, NULL ); + startup.dwFlags = STARTF_USESTDHANDLES; + break; + case H_FILE | H_REOPEN: + create_file( "file0.tmp", "first content\r\n" ); + break; + case H_NUL | H_REOPEN: + break; + default: ok(0, "\n"); break; + } + + sprintf( cmdline, "%s file stdbuf %#x", selfname, test_file_type[i].type ); + ret = CreateProcessA( NULL, cmdline, NULL, NULL, (startup.dwFlags & STARTF_USESTDHANDLES), 0, NULL, NULL, &startup, &proc ); + ok(ret, "Couldn't create child process '%s' %p %p %p: %lu\n", cmdline, startup.hStdInput, startup.hStdOutput, startup.hStdError, GetLastError()); + ret = WaitForSingleObject( proc.hProcess, 30000 ); + ok(ret == WAIT_OBJECT_0, "Unexpected process termination\n"); + ret = GetExitCodeProcess( proc.hProcess, &exit_code ); + ok( ret, "Unexpected result\n" ); + expected_exit_code = test_file_type[i].std_buffer_state[0] << 0 | + test_file_type[i].std_buffer_state[1] << 2 | + test_file_type[i].std_buffer_state[2] << 4; + if (test_file_type[i].broken_stderr == -1) + broken_exit_code = expected_exit_code; + else + broken_exit_code = test_file_type[i].std_buffer_state[0] << 0 | + test_file_type[i].std_buffer_state[1] << 2 | + test_file_type[i].broken_stderr << 4; + + todo_wine_if( test_file_type[i].with_todo ) + ok( exit_code == expected_exit_code || broken(exit_code == broken_exit_code), + "Unexpected buffer state %lx: stdin=%lx/%x, stdout=%lx/%x, stderr=%lx/%x\n", + exit_code >> 6, + (exit_code >> 0) & 3, test_file_type[i].std_buffer_state[0], + (exit_code >> 2) & 3, test_file_type[i].std_buffer_state[1], + (exit_code >> 4) & 3, test_file_type[i].std_buffer_state[2] ); + + CloseHandle( proc.hProcess ); + CloseHandle( proc.hThread ); + + winetest_log("trace"); + + switch ((int)test_file_type[i].type) + { + case H_FILE: + case H_NUL: + CloseHandle( startup.hStdInput ); + CloseHandle( startup.hStdOutput ); + CloseHandle( startup.hStdError ); + break; + default: + break; + } + DeleteFileA( "file0.tmp" ); + DeleteFileA( "file1.tmp" ); + DeleteFileA( "file2.tmp" ); + winetest_pop_context(); + } +} + +static void test_std_buffer_state_child(int argc, char **argv) +{ + unsigned flags = 0; + unsigned ret = 0; + char ch; + + if (argc >= 4) flags = strtol( argv[3], NULL, 0 ); + + if (flags & H_REOPEN) + { + switch (flags & H_KIND_MASK) + { + case H_FILE: + if (!freopen( "file0.tmp", "r", stdin )) ret |= 1 << 6; + if (!freopen( "file1.tmp", "w", stdout )) ret |= 1 << 6; + if (!freopen( "file2.tmp", "w", stderr )) ret |= 1 << 6; + break; + case H_NUL: + if (!freopen( "NUL", "r", stdin )) ret |= 1 << 6; + if (!freopen( "NUL", "w", stdout )) ret |= 1 << 6; + if (!freopen( "NUL", "w", stderr )) ret |= 1 << 6; + break; + default: + ret |= 1 << 6; + break; + } + } + + /* these operations shall force buffer creation (if required) */ + fscanf( stdin, "%c", &ch ); + fprintf( stdout, "f" ); + fprintf( stderr, "f" ); + + ret |= get_buffer_state( stdin ) << 0; + ret |= get_buffer_state( stdout ) << 2; + ret |= get_buffer_state( stderr ) << 4; + + ExitProcess( ret ); +} + +START_TEST(file) +{ + int arg_c; + char** arg_v; + + arg_c = winetest_get_mainargs( &arg_v ); + + if (arg_c >= 3) + { + if (strcmp( arg_v[2], "stdbuf" ) == 0) + test_std_buffer_state_child( arg_c, arg_v ); + else + ok( 0, "invalid argument '%s'\n", arg_v[2] ); + return; + } + test_std_buffer_state( arg_v[0] ); +}