diff --git a/README.md b/README.md index c571551..95fa24c 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@ - `sp_da`, a dynamic array - `sp_ht`, hash tables with arbitrary keys and values (including strings) - `sp_ps`, subprocesses -- `sp_io`, synchronous IO on top of files and buffers -- File monitors, [beautiful, interactive CLI prompts](https://spader.zone/prompt), ELF parsing, memory allocators, concurrency, UTF-8, globbing, and a whole lot more! +- `sp_io`, synchronous, bufferable, zero-copy IO +- File monitors, [beautiful, interactive CLIs](https://spader.zone/prompt), ELF parsing, memory allocators, concurrency, UTF-8, screaming fast globbing, and a whole lot more! -It's written in ~15,000 lines of plain C99[^1] and has zero dependencies. It can be used with virtually any 64-bit environment and toolchain: +It's written in ~15,000 lines of plain C99[^1] and has zero dependencies. It does not depend on libc. It can be used with virtually any environment and toolchain: - Linux, macOS, Windows - x86, ARM, or WASM - gcc, clang, MSVC, mingw, zig cc, tcc, cosmocc @@ -22,7 +22,7 @@ It's written in ~15,000 lines of plain C99[^1] and has zero dependencies. It can #include "sp.h" ``` -`sp.h` can be also be compiled as a traditional shared or static library. +`sp.h` can be also be compiled as a traditional shared or static library. `sp.h` makes no assumptions about its place in your code. You can use any small piece of it as a standalone utility, or you can use it as the foundation for almost any program. Give it a try! ## example: `ls` Here's a minimal `ls` in 30 lines of code. @@ -58,9 +58,10 @@ s32 main(s32 num_args, const c8** args) { } ``` A few modules showcased in this example: +- `sp_mem_t` is an allocator; everything that allocates takes one. In the example, we use the default heap allocator. - `sp_str_t` is a non-null-terminated string which trivially gives us views, substrings, and many path operations - `sp_fs` is more or less equivalent to `std::fs` in C++, but in plain C and implemented against the lowest level APIs -- `sp_da` is a `std::vector` equivalent which can hold arbitrary types, does not need initialization, and is stored as `T*` +- `sp_da` is a `std::vector` equivalent which can hold arbitrary types and is stored as and usable as a plain `T*` - `sp_fmt` implements modern format strings (like Zig, or Rust) whose arguments are type-safe # modules @@ -107,6 +108,7 @@ These are available in `sp/*.h` as separate headers, for various reasons.[^3] | `sp_prompt` | Very beautiful [`clack`](https://github.com/bombshell-dev/clack)-inspired interactive prompts for CLIs | | # principles +I wrote about some of the core design [here](https://spader.zone/sp/). The short version: - Prefer the lowest level interface to the OS by default - Ergonomics are the most important thing and by a lot - Errors are propagated up the call stack @@ -114,77 +116,9 @@ These are available in `sp/*.h` as separate headers, for various reasons.[^3] - Null terminated strings are the devil's work and are to be shunned - A little Assembly never hurt anyone -# known issues -`sp.h` is a library that grows with my understanding of systems programming. That means that some of the code is naive, underspecified, or just bad. Everything that exists is tested extremely thoroughly. +Please note that `sp.h` is in alpha. I [use](https://github.com/tspader/spn) [it](https://github.com/tspader/space), [a](https://github.com/tspader/mbench) [lot](https://github.com/tspader/tomlc17), and there are about a thousand tests, but...it's still in alpha. The core API shape is done, but there will likely be breakage around the edges. There are also several POSIX-isms which have clung around, and `sp_io` was recently finished. -| module | problem | platform | -| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -| `sp_ps` | Implemented with `pthread` instead of `fork` + `exec` | Linux | -| `sp_ht` | Keys, by default, are simply `memcmp`'d for equality. If your key is a struct which the compiler pads, it is silently wrong. | | -| `sp_io` | Writes are objectively worse than libc, because we don't use `writev` to batch when we know we want to do more than one write (e.g. "flush the buffer and then immediately write the requested data") | | - -# more examples -[Source code is often the best documentation](https://github.com/tspader/sp/tree/main/example), but here are a few more. - -## wc -Here's a minimal version of a word frequency counter. It uses a few very handy and common functions: -- We use `sp_fs_join_path()` to find the target's absolute path -- Then, read it in one go with `sp_io_read_file()` (a thin wrapper over `sp_io_reader_t`) -- Split the content into lines, and then words. This is all zero copy; `lines` and `words` contain *views* into the content. -- A `sp_str_ht(u32)` (`str` -> `u32`) keeps the counts. `sp_str_ht_for_kv()` lets us iterate with a strongly typed (!) iterator -```c -#define SP_IMPLEMENTATION -#include "sp.h" - -s32 main(s32 num_args, const c8** args) { - if (num_args < 2) { - sp_log("usage: wc {.fg cyan}", sp_fmt_cstr("$file")); - return 1; - } - - sp_str_t path = sp_fs_join_path(sp_fs_get_cwd(), sp_str_view(args[1])); - sp_str_t content = sp_zero; - sp_io_read_file(path, &content); - - sp_str_ht(u32) counts = sp_zero; - sp_da(sp_str_t) lines = sp_str_split_c8(content, '\n'); - sp_da_for(lines, i) { - sp_da(sp_str_t) words = sp_str_split_c8(lines[i], ' '); - - sp_da_for(words, j) { - u32* count = sp_str_ht_get(counts, words[j]); - if (count) { - *count = *count + 1; - } else { - sp_str_ht_insert(counts, words[j], 1); - } - } - } - - sp_str_ht_for_kv(counts, it) { - sp_log("{} {}", sp_fmt_uint(*it.val), sp_fmt_str(*it.key)); - } - return 0; -} -``` - -## dynamic array -```c -sp_da(u32) years = sp_zero; -sp_da_push(years, 1969); -sp_da_push(years, 1972); -sp_da_for(years, it) { - sp_log("{}", sp_fmt_uint(years[it])); -} -``` - -## hash table -```c - sp_cstr_ht(s32) ht = sp_zero; - sp_cstr_ht_insert(ht, "veneta", 72); - s32* veneta = sp_cstr_ht_get(ht, "veneta"); - sp_log("the best dead show was in 19{}", sp_fmt_int(*veneta)); -``` +Thankfully, since the library is a single file, it's very easy for you or an LLM to diff two copies of the library and make any changes needed. More, since the library is *not* build on decades of cruft, there are very few layers between your code and the syscalls it boils down to. Nevertheless, you should feel comfortable reading the library's source code if you plan to use it seriously. # development Install any C compiler, and then: diff --git a/sp.h b/sp.h index 5b7f796..ca385ae 100644 --- a/sp.h +++ b/sp.h @@ -2514,6 +2514,11 @@ typedef enum { SP_OS_FOLLOW_SYMLINK, } sp_os_follow_symlink_t; +typedef enum { + SP_FS_PATH_POSIX, + SP_FS_PATH_WINDOWS, +} sp_fs_path_kind_t; + typedef struct { sp_str_t path; sp_str_t name; @@ -2544,7 +2549,11 @@ SP_API sp_str_t sp_fs_trim_path(sp_str_t path); SP_API sp_str_t sp_fs_normalize_path(sp_mem_t mem, sp_str_t path); SP_API sp_str_t sp_fs_get_ext(sp_str_t path); SP_API sp_str_t sp_fs_get_stem(sp_str_t path); +SP_API bool sp_fs_is_sep(c8 c); SP_API bool sp_fs_is_root(sp_str_t path); +SP_API bool sp_fs_is_absolute(sp_str_t path); +SP_API bool sp_fs_is_absolute_for(sp_str_t path, sp_fs_path_kind_t kind); +SP_API bool sp_fs_is_absolute_w(sp_wide_str_t path); SP_API bool sp_fs_is_glob(sp_str_t path); SP_API sp_str_t sp_fs_join_path(sp_mem_t mem, sp_str_t a, sp_str_t b); SP_API sp_str_t sp_fs_replace_ext(sp_mem_t mem, sp_str_t path, sp_str_t ext); @@ -2739,7 +2748,8 @@ SP_TYPEDEF_FN(void, sp_os_signal_handler_t, sp_os_signal_t signal, void* userdat SP_API void sp_sleep_ns(u64 ns); SP_API void sp_sleep_ms(f64 ms); -SP_API sp_os_kind_t sp_os_get_kind(); +SP_API sp_os_kind_t sp_os_get_kind(); +SP_API sp_fs_path_kind_t sp_os_get_path_kind(); SP_API sp_str_t sp_os_get_name(); SP_API sp_str_t sp_os_get_executable_ext(); SP_API sp_str_t sp_os_lib_kind_to_extension(sp_os_lib_kind_t kind); @@ -4399,14 +4409,6 @@ SP_PRIVATE u8* sp_nt_process_params(void) { #define SP_NT_OBJECT_NAME_INFORMATION 1 -SP_PRIVATE bool sp_sys_is_absolute_wtf16(const u16* p, u32 len) { - if (len < 1) return false; - if (p[0] == '\\' || p[0] == '/') return true; - if (len < 2 || p[1] != ':') return false; - if (len < 3) return false; - return p[2] == '\\' || p[2] == '/'; -} - SP_PRIVATE u32 sp_sys_normalize_relative_wtf16(u16* p, u32 len, bool* saw_dotdot) { u32 w = 0; u32 r = 0; @@ -4473,7 +4475,7 @@ SP_PRIVATE sp_nt_status_t sp_sys_nt_target(sp_sys_fd_t root_fd, sp_str_t utf8, u sp_wide_str_t wpath = sp_wtf8_to_wtf16(sp_mem_fixed_as_allocator(&fixed), utf8); if (!wpath.data) return SP_NT_STATUS_OBJECT_NAME_INVALID; - if (sp_sys_is_absolute_wtf16(wpath.data, wpath.len)) { + if (sp_fs_is_absolute_w(wpath)) { sp_nt_unicode_string_t nt = sp_zero; u16* file_part = SP_NULLPTR; sp_nt_status_t status = SP_NT(RtlDosPathNameToNtPathName_U_WithStatus)(wpath.data, &nt, &file_part, SP_NULLPTR); @@ -9717,6 +9719,10 @@ sp_os_kind_t sp_os_get_kind() { return SP_OS_WIN32; } +sp_fs_path_kind_t sp_os_get_path_kind() { + return SP_FS_PATH_WINDOWS; +} + sp_str_t sp_os_get_executable_ext() { return sp_str_lit("exe"); } @@ -9740,6 +9746,10 @@ sp_os_kind_t sp_os_get_kind() { return SP_OS_MACOS; } +sp_fs_path_kind_t sp_os_get_path_kind() { + return SP_FS_PATH_POSIX; +} + sp_str_t sp_os_get_executable_ext() { return sp_str_lit(""); } @@ -9763,6 +9773,10 @@ sp_os_kind_t sp_os_get_kind() { return SP_OS_LINUX; } +sp_fs_path_kind_t sp_os_get_path_kind() { + return SP_FS_PATH_POSIX; +} + sp_str_t sp_os_get_executable_ext() { return sp_str_lit(""); } @@ -9796,6 +9810,16 @@ sp_os_kind_t sp_os_get_kind() { SP_UNREACHABLE_RETURN(SP_OS_LINUX); } +sp_fs_path_kind_t sp_os_get_path_kind() { + switch (sp_os_get_kind()) { + case SP_OS_LINUX: return SP_FS_PATH_POSIX; + case SP_OS_MACOS: return SP_FS_PATH_POSIX; + case SP_OS_WIN32: return SP_FS_PATH_WINDOWS; + } + + SP_UNREACHABLE_RETURN(SP_FS_PATH_POSIX); +} + sp_str_t sp_os_get_executable_ext() { switch (sp_os_get_kind()) { case SP_OS_LINUX: return sp_str_lit(""); @@ -9820,6 +9844,12 @@ sp_str_t sp_os_lib_to_file_name(sp_mem_t mem, sp_str_t lib_name, sp_os_lib_kind_ } #endif +#if defined(SP_WASM) +sp_fs_path_kind_t sp_os_get_path_kind() { + return SP_FS_PATH_POSIX; +} +#endif + //////////// // SIGNAL // //////////// @@ -14926,11 +14956,37 @@ sp_str_t sp_fs_get_stem(sp_str_t path) { return stem; } +bool sp_fs_is_sep(c8 c) { + return c == '/' || c == '\\'; +} + +bool sp_fs_is_absolute_for(sp_str_t path, sp_fs_path_kind_t kind) { + if (path.len == 0) return false; + + if (sp_fs_is_sep(path.data[0])) { + return true; + } + + if (kind != SP_FS_PATH_WINDOWS) return false; + + return (path.len >= 3 && path.data[1] == ':' && sp_fs_is_sep(path.data[2])); +} + +bool sp_fs_is_absolute(sp_str_t path) { + return sp_fs_is_absolute_for(path, sp_os_get_path_kind()); +} + bool sp_fs_is_root(sp_str_t path) { - if (path.len == 0) return true; - if (path.len == 1 && path.data[0] == '/') return true; - if (path.len == 2 && path.data[1] == ':') return true; - if (path.len == 3 && path.data[1] == ':' && (path.data[2] == '/' || path.data[2] == '\\')) return true; + if (path.len == 1 && sp_fs_is_sep(path.data[0])) return true; + if (path.len == 3 && path.data[1] == ':' && sp_fs_is_sep(path.data[2])) return true; + return false; +} + +bool sp_fs_is_absolute_w(sp_wide_str_t path) { + if (path.len == 0) return false; + if (path.data[0] == '/' || path.data[0] == '\\') return true; + if (path.len >= 3 && path.data[1] == ':' && + (path.data[2] == '/' || path.data[2] == '\\')) return true; return false; } diff --git a/test/fs.c b/test/fs.c index 7f9cf38..8fef3c3 100644 --- a/test/fs.c +++ b/test/fs.c @@ -9,6 +9,7 @@ SP_TEST_MAIN() #include "fs/get_stem.c" #include "fs/join_path.c" #include "fs/replace_ext.c" +#include "fs/is_absolute.c" #include "fs/is_root.c" #include "fs/is_glob.c" #include "fs/canonicalize_path.c" diff --git a/test/fs/canonicalize_path.c b/test/fs/canonicalize_path.c index 7fa5a8f..5231020 100644 --- a/test/fs/canonicalize_path.c +++ b/test/fs/canonicalize_path.c @@ -288,24 +288,10 @@ UTEST_F(fs, canon_idempotent) { SP_EXPECT_STR_EQ(first, second); } -UTEST_F(fs, canon_exe_idempotent) { +UTEST_F(fs, canon_dot_resolves_to_cwd) { SKIP_ON_WASM() sp_mem_t a = ut.file_manager.mem; - sp_str_t exe = sp_fs_get_exe_path(a); - sp_str_t canonical = sp_fs_canonicalize_path(a, exe); - SP_EXPECT_STR_EQ(canonical, exe); -} - -UTEST_F(fs, canon_cwd_matches_dot) { - SKIP_ON_WASM() - sp_mem_t a = ut.file_manager.mem; - sp_str_t old_cwd = sp_fs_get_cwd(a); - sp_str_t sandbox = sp_test_file_path(&ut.file_manager, sp_str_lit("canon_cwd")); - sp_fs_create_dir(sandbox); - - ASSERT_EQ(sp_sys_chdir_s(sandbox), 0); sp_str_t cwd = sp_fs_get_cwd(a); - sp_str_t canonical_dot = sp_fs_canonicalize_path(a, sp_str_lit(".")); - SP_EXPECT_STR_EQ(cwd, canonical_dot); - ASSERT_EQ(sp_sys_chdir_s(old_cwd), 0); + sp_str_t dot = sp_fs_canonicalize_path(a, sp_str_lit(".")); + SP_EXPECT_STR_EQ(cwd, dot); } diff --git a/test/fs/create_file.c b/test/fs/create_file.c index 05ac7bc..680d8c8 100644 --- a/test/fs/create_file.c +++ b/test/fs/create_file.c @@ -1,20 +1,49 @@ #include "fs.h" #include "test.h" +typedef enum { + CREATE_FILE_EMPTY, + CREATE_FILE_SLICE, + CREATE_FILE_STR, + CREATE_FILE_CSTR, +} create_file_variant_t; + typedef struct { const c8* label; fs_setup_t setup[16]; const c8* path; + create_file_variant_t variant; + c8 content [32]; fs_expected_path_t expect[16]; } create_file_test_t; static void run_create_file_test(s32* utest_result, sp_test_file_manager_t* fm, create_file_test_t t) { - sp_str_t sandbox = sp_test_file_path(fm, sp_str_view(t.label)); + sp_str_t sandbox = sp_test_file_path(fm, sp_cstr_as_str(t.label)); sp_fs_create_dir(sandbox); fs_apply_setup(utest_result, fm, sandbox, t.setup); - sp_str_t path = sp_fs_join_path(fm->mem, sandbox, sp_str_view(t.path)); - sp_fs_create_file(path); + sp_str_t path = sp_fs_join_path(fm->mem, sandbox, sp_cstr_as_str(t.path)); + sp_err_t result = SP_OK; + switch (t.variant) { + case CREATE_FILE_EMPTY: { + result = sp_fs_create_file(path); + break; + } + case CREATE_FILE_SLICE: { + sp_mem_slice_t slice = { (u8*)t.content, sp_cstr_len(t.content) }; + result = sp_fs_create_file_slice(path, slice); + break; + } + case CREATE_FILE_STR: { + result = sp_fs_create_file_str(path, sp_cstr_as_str(t.content)); + break; + } + case CREATE_FILE_CSTR: { + result = sp_fs_create_file_cstr(path, t.content); + break; + } + } + EXPECT_EQ(result, SP_OK); fs_expect_paths(utest_result, fm, sandbox, t.expect); } @@ -55,38 +84,41 @@ UTEST_F(fs, create_file_unicode) { }); } -UTEST_F(fs, create_file_with_content) { +UTEST_F(fs, create_file_slice) { SKIP_ON_WASM() - sp_mem_t a = ut.file_manager.mem; - sp_str_t sandbox = sp_test_file_path(&ut.file_manager, sp_str_lit("create_file_with_content")); - sp_fs_create_dir(sandbox); - - u8 buffer [] = { 's', 'p', 'u', 'm', 0 }; - sp_str_t path = sp_zero; - sp_str_t content = sp_zero; - sp_err_t result = SP_OK; - - path = sp_fs_join_path(a, sandbox, sp_str_lit("slice.file")); - result = sp_fs_create_file_slice(path, (sp_mem_slice_t) { buffer, 4 }); - sp_io_read_file(a, path, &content); - EXPECT_EQ(result, SP_OK); - EXPECT_TRUE(sp_fs_exists(path)); - SP_EXPECT_STR_EQ_CSTR(content, "spum"); - - path = sp_fs_join_path(a, sandbox, sp_str_lit("str.file")); - result = sp_fs_create_file_str(path, (sp_str_t) { (c8*)buffer, 4 }); - sp_io_read_file(a, path, &content); - EXPECT_EQ(result, SP_OK); - EXPECT_TRUE(sp_fs_exists(path)); - SP_EXPECT_STR_EQ_CSTR(content, "spum"); - - path = sp_fs_join_path(a, sandbox, sp_str_lit("cstr.file")); - result = sp_fs_create_file_cstr(path, (const c8*)buffer); - sp_io_read_file(a, path, &content); - EXPECT_EQ(result, SP_OK); - EXPECT_TRUE(sp_fs_exists(path)); - SP_EXPECT_STR_EQ_CSTR(content, "spum"); - + run_create_file_test(&ur, &ut.file_manager, (create_file_test_t){ + .label = "create_file_slice", + .path = "slice.file", + .variant = CREATE_FILE_SLICE, + .content = "spum", + .expect = { + { .path = "slice.file", .exists = FS_EXPECT_EXIST, .attr = SP_FS_KIND_FILE, .content = "spum" }, + }, + }); } +UTEST_F(fs, create_file_str) { + SKIP_ON_WASM() + run_create_file_test(&ur, &ut.file_manager, (create_file_test_t){ + .label = "create_file_str", + .path = "str.file", + .variant = CREATE_FILE_STR, + .content = "spum", + .expect = { + { .path = "str.file", .exists = FS_EXPECT_EXIST, .attr = SP_FS_KIND_FILE, .content = "spum" }, + }, + }); +} +UTEST_F(fs, create_file_cstr) { + SKIP_ON_WASM() + run_create_file_test(&ur, &ut.file_manager, (create_file_test_t){ + .label = "create_file_cstr", + .path = "cstr.file", + .variant = CREATE_FILE_CSTR, + .content = "spum", + .expect = { + { .path = "cstr.file", .exists = FS_EXPECT_EXIST, .attr = SP_FS_KIND_FILE, .content = "spum" }, + }, + }); +} diff --git a/test/fs/fd_relative.c b/test/fs/fd_relative.c index cef0d37..bb30bf0 100644 --- a/test/fs/fd_relative.c +++ b/test/fs/fd_relative.c @@ -1,68 +1,81 @@ #include "fs.h" +#include "utest.h" typedef enum { - FD_REL_STAT, - FD_REL_OPEN, - FD_REL_RENAME, - FD_REL_LINK, + FD_REL_GET_METADATA, + FD_REL_OPEN_FROM_RELATIVE_PATH, + FD_REL_OPEN_FROM_ABSOLUTE_PATH, + FD_REL_OP_RENAME, + FD_REL_OP_LINK, } fd_rel_op_t; typedef struct { const c8* label; fs_setup_t setup[16]; - const c8* dir; // directory opened to obtain the relative fd - fd_rel_op_t op; - const c8* path; // STAT/OPEN target, or RENAME/LINK source (relative to dir_fd) - const c8* dst; // RENAME/LINK destination (relative to dir_fd) - bool absolute; // OPEN: join path against the sandbox so the absolute path ignores the fd - const c8* expect_content; // OPEN: file contents read back through the fd + struct { + const c8* cwd; + fd_rel_op_t kind; + union { + struct { const c8* path; } metadata; + struct { const c8* path; const c8* content; } open; + struct { const c8* source; const c8* dest; } rename; + struct { const c8* source; const c8* dest; } link; + }; + } op; fs_expected_path_t expected[16]; } fd_rel_test_t; -static void run_fd_rel_test(s32* utest_result, sp_test_file_manager_t* fm, fd_rel_test_t t) { - sp_mem_t a = fm->mem; - sp_str_t sandbox = sp_test_file_path(fm, sp_str_view(t.label)); - sp_fs_create_dir(sandbox); - fs_apply_setup(utest_result, fm, sandbox, t.setup); +static void run_fd_rel_test(s32* utest_result, sp_test_file_manager_t* fs, fd_rel_test_t t) { + sp_mem_t mem = fs->mem; + sp_str_t sandbox = sp_test_file_create_dir(fs, t.label); + fs_apply_setup(utest_result, fs, sandbox, t.setup); - sp_str_t dir = sp_fs_join_path(a, sandbox, sp_str_view(t.dir)); - sp_sys_fd_t dir_fd = sp_sys_open_s(sp_sys_get_root(0), dir, SP_O_RDONLY | SP_O_DIRECTORY, 0); - ASSERT_NE(dir_fd, SP_SYS_INVALID_FD); + struct { + sp_str_t path; + sp_sys_fd_t fd; + } cwd = sp_zero; + cwd.path = sp_fs_join_path(mem, sandbox, sp_cstr_as_str(t.op.cwd)); + cwd.fd = sp_sys_open_s(sp_sys_get_root(0), cwd.path, SP_O_RDONLY | SP_O_DIRECTORY, 0); + ASSERT_NE(cwd.fd, SP_SYS_INVALID_FD); - switch (t.op) { - case FD_REL_STAT: { - sp_sys_file_meta_t st = sp_zero; - EXPECT_EQ(sp_sys_get_path_metadata_s(dir_fd, sp_str_view(t.path), &st), 0); + switch (t.op.kind) { + case FD_REL_GET_METADATA: { + sp_str_t path = sp_cstr_as_str(t.op.metadata.path); + sp_sys_file_meta_t metadata = sp_zero; + EXPECT_OK(sp_sys_get_path_metadata_s(cwd.fd, path, &metadata)); break; } - case FD_REL_OPEN: { - sp_str_t path = t.absolute - ? sp_fs_join_path(a, sandbox, sp_str_view(t.path)) - : sp_str_view(t.path); - sp_sys_fd_t file_fd = sp_sys_open_s(dir_fd, path, SP_O_RDONLY | SP_O_BINARY, 0); - EXPECT_NE(file_fd, SP_SYS_INVALID_FD); - if (file_fd != SP_SYS_INVALID_FD) { - if (t.expect_content) { + case FD_REL_OPEN_FROM_ABSOLUTE_PATH: + case FD_REL_OPEN_FROM_RELATIVE_PATH: { + sp_str_t path = t.op.kind == FD_REL_OPEN_FROM_ABSOLUTE_PATH + ? sp_fs_join_path(mem, sandbox, sp_cstr_as_str(t.op.open.path)) + : sp_cstr_as_str(t.op.open.path); + sp_sys_fd_t fd = sp_sys_open_s(cwd.fd, path, SP_O_RDONLY | SP_O_BINARY, 0); + EXPECT_NE(fd, SP_SYS_INVALID_FD); + + if (fd != SP_SYS_INVALID_FD) { + if (t.op.open.content) { c8 buf[64] = sp_zero; - s64 n = sp_sys_read(file_fd, buf, sizeof(buf)); - SP_EXPECT_STR_EQ_CSTR(sp_str(buf, n < 0 ? 0 : (u32)n), t.expect_content); + s64 n = sp_sys_read(fd, buf, sizeof(buf)); + EXPECT_GE(n, 0); + EXPECT_TRUE(sp_mem_is_equal(buf, t.op.open.content, sp_max(n, 0))); } - sp_sys_close(file_fd); + sp_sys_close(fd); } break; } - case FD_REL_RENAME: { - EXPECT_EQ(sp_sys_rename_s(dir_fd, sp_str_view(t.path), dir_fd, sp_str_view(t.dst)), 0); + case FD_REL_OP_RENAME: { + EXPECT_EQ(sp_sys_rename_s(cwd.fd, sp_cstr_as_str(t.op.rename.source), cwd.fd, sp_cstr_as_str(t.op.rename.dest)), 0); break; } - case FD_REL_LINK: { - EXPECT_EQ(sp_sys_link_s(dir_fd, sp_str_view(t.path), dir_fd, sp_str_view(t.dst)), 0); + case FD_REL_OP_LINK: { + EXPECT_EQ(sp_sys_link_s(cwd.fd, sp_cstr_as_str(t.op.link.source), cwd.fd, sp_cstr_as_str(t.op.link.dest)), 0); break; } } - sp_sys_close(dir_fd); - fs_expect_paths(utest_result, fm, sandbox, t.expected); + sp_sys_close(cwd.fd); + fs_expect_paths(utest_result, fs, sandbox, t.expected); } UTEST_F(fs, fd_relative_stat_dotdot) { @@ -72,9 +85,13 @@ UTEST_F(fs, fd_relative_stat_dotdot) { .setup = { { .path = "a/file.txt", .kind = FS_SETUP_FILE, .content = "hello" }, }, - .dir = "a", - .op = FD_REL_STAT, - .path = "../a/file.txt", + .op = { + .cwd = "a", + .kind = FD_REL_GET_METADATA, + .metadata = { + .path = "../a/file.txt", + } + } }); } @@ -85,10 +102,14 @@ UTEST_F(fs, fd_relative_open_dotdot) { .setup = { { .path = "a/file.txt", .kind = FS_SETUP_FILE, .content = "hello" }, }, - .dir = "a", - .op = FD_REL_OPEN, - .path = "../a/file.txt", - .expect_content = "hello", + .op = { + .cwd = "a", + .kind = FD_REL_OPEN_FROM_RELATIVE_PATH, + .open = { + .path = "../a/file.txt", + .content = "hello" + } + } }); } @@ -99,10 +120,14 @@ UTEST_F(fs, fd_relative_rename_dotdot) { .setup = { { .path = "a/file.txt", .kind = FS_SETUP_FILE, .content = "hello" }, }, - .dir = "a", - .op = FD_REL_RENAME, - .path = "file.txt", - .dst = "../a/file2.txt", + .op = { + .cwd = "a", + .kind = FD_REL_OP_RENAME, + .rename = { + .source = "file.txt", + .dest = "../a/file2.txt", + } + }, .expected = { { .path = "a/file.txt", .exists = FS_EXPECT_NOT_EXIST, .attr = SP_FS_KIND_NONE }, { .path = "a/file2.txt", .exists = FS_EXPECT_EXIST, .attr = SP_FS_KIND_FILE }, @@ -117,10 +142,14 @@ UTEST_F(fs, fd_relative_link_dotdot) { .setup = { { .path = "a/file.txt", .kind = FS_SETUP_FILE, .content = "hello" }, }, - .dir = "a", - .op = FD_REL_LINK, - .path = "file.txt", - .dst = "../a/file.hard", + .op = { + .cwd = "a", + .kind = FD_REL_OP_LINK, + .link = { + .source = "file.txt", + .dest = "../a/file.hard", + } + }, .expected = { { .path = "a/file.hard", .exists = FS_EXPECT_EXIST, .attr = SP_FS_KIND_FILE }, }, @@ -134,9 +163,13 @@ UTEST_F(fs, fd_relative_open_mixed_separators) { .setup = { { .path = "a/file.txt", .kind = FS_SETUP_FILE, .content = "hello" }, }, - .dir = "a", - .op = FD_REL_OPEN, - .path = "..\\a/file.txt", + .op = { + .cwd = "a", + .kind = FD_REL_OPEN_FROM_RELATIVE_PATH, + .open = { + .path = "..\\a/file.txt", + } + } }); } @@ -147,9 +180,13 @@ UTEST_F(fs, fd_relative_open_dot_segments) { .setup = { { .path = "a/file.txt", .kind = FS_SETUP_FILE, .content = "hello" }, }, - .dir = "a", - .op = FD_REL_OPEN, - .path = "./././file.txt", + .op = { + .cwd = "a", + .kind = FD_REL_OPEN_FROM_RELATIVE_PATH, + .open = { + .path = "./././file.txt", + } + } }); } @@ -161,11 +198,14 @@ UTEST_F(fs, fd_relative_open_absolute_ignores_fd) { { .path = "a/file.txt", .kind = FS_SETUP_FILE, .content = "hello" }, { .path = "b/file.txt", .kind = FS_SETUP_FILE, .content = "world" }, }, - .dir = "a", - .op = FD_REL_OPEN, - .path = "b/file.txt", - .absolute = true, - .expect_content = "world", + .op = { + .cwd = "a", + .kind = FD_REL_OPEN_FROM_ABSOLUTE_PATH, + .open = { + .path = "b/file.txt", + .content = "world", + } + } }); } @@ -177,9 +217,13 @@ UTEST_F(fs, fd_relative_open_deep_dotdot) { { .path = "a/b/inner.txt", .kind = FS_SETUP_FILE, .content = "deep" }, { .path = "a/sibling.txt", .kind = FS_SETUP_FILE, .content = "side" }, }, - .dir = "a/b", - .op = FD_REL_OPEN, - .path = "../sibling.txt", - .expect_content = "side", + .op = { + .cwd = "a/b", + .kind = FD_REL_OPEN_FROM_RELATIVE_PATH, + .open = { + .path = "../sibling.txt", + .content = "side", + } + } }); } diff --git a/test/fs/fs.h b/test/fs/fs.h index edc32f9..176c43d 100644 --- a/test/fs/fs.h +++ b/test/fs/fs.h @@ -71,6 +71,7 @@ typedef struct { const c8* path; bool exists; sp_fs_kind_t attr; + const c8* content; } fs_expected_path_t; static u32 fs_count_setup(fs_setup_t* setup) { @@ -110,14 +111,15 @@ static void fs_expect_attr(s32* utest_result, sp_str_t path, sp_fs_kind_t actual SP_FAIL(); } -static void fs_expect_paths(s32* utest_result, sp_test_file_manager_t* fm, sp_str_t sandbox, fs_expected_path_t* expected) { +static void fs_expect_paths(s32* utest_result, sp_test_file_manager_t* fs, sp_str_t sandbox, fs_expected_path_t* expected) { u32 expected_count = fs_count_expected_paths(expected); sp_for(i, expected_count) { - fs_expected_path_t* exp = &expected[i]; - sp_str_t path = sp_fs_join_path(fm->mem, sandbox, sp_str_view(exp->path)); + fs_expected_path_t* info = &expected[i]; + sp_str_t path = sp_fs_join_path(fs->mem, sandbox, sp_str_view(info->path)); + bool exists = sp_fs_exists(path); - if (exists != exp->exists) { - if (exp->exists) { + if (exists != info->exists) { + if (info->exists) { SP_TEST_REPORT("expected {} to exist", sp_fmt_str(path)); } else { SP_TEST_REPORT("expected {} not to exist", sp_fmt_str(path)); @@ -125,17 +127,27 @@ static void fs_expect_paths(s32* utest_result, sp_test_file_manager_t* fm, sp_st SP_FAIL(); } - if (exp->exists) { - fs_expect_attr(utest_result, path, sp_fs_get_kind(path), exp->attr); + if (info->exists) { + fs_expect_attr(utest_result, path, sp_fs_get_kind(path), info->attr); + } + + if (info->content) { + sp_str_t actual = sp_zero; + sp_io_read_file(fs->mem, path, &actual); + sp_str_t expected_content = sp_str_view(info->content); + if (!sp_str_equal(actual, expected_content)) { + SP_TEST_REPORT("{} content was {} but expected {}", sp_fmt_str(path), sp_fmt_str(actual), sp_fmt_str(expected_content)); + SP_FAIL(); + } } } } -static void fs_apply_setup(s32* utest_result, sp_test_file_manager_t* fm, sp_str_t sandbox, fs_setup_t* setup) { +static void fs_apply_setup(s32* utest_result, sp_test_file_manager_t* fs, sp_str_t sandbox, fs_setup_t* setup) { u32 setup_count = fs_count_setup(setup); sp_for(i, setup_count) { fs_setup_t* ent = &setup[i]; - sp_str_t path = sp_fs_join_path(fm->mem, sandbox, sp_str_view(ent->path)); + sp_str_t path = sp_fs_join_path(fs->mem, sandbox, sp_str_view(ent->path)); sp_str_t parent = sp_fs_parent_path(path); if (!sp_str_empty(parent) && !sp_str_equal(parent, path) && !sp_fs_exists(parent)) { @@ -155,7 +167,7 @@ static void fs_apply_setup(s32* utest_result, sp_test_file_manager_t* fm, sp_str break; } case FS_SETUP_SYMLINK: { - sp_str_t target = sp_fs_join_path(fm->mem, sandbox, sp_str_view(ent->target)); + sp_str_t target = sp_fs_join_path(fs->mem, sandbox, sp_str_view(ent->target)); if (sp_fs_create_sym_link(target, path) != SP_OK) { SP_TEST_REPORT("failed to create symlink {} -> {}", sp_fmt_str(path), sp_fmt_str(target)); SP_FAIL(); @@ -163,7 +175,7 @@ static void fs_apply_setup(s32* utest_result, sp_test_file_manager_t* fm, sp_str break; } case FS_SETUP_HARD_LINK: { - sp_str_t target = sp_fs_join_path(fm->mem, sandbox, sp_str_view(ent->target)); + sp_str_t target = sp_fs_join_path(fs->mem, sandbox, sp_str_view(ent->target)); if (sp_fs_create_hard_link(target, path) != SP_OK) { SP_TEST_REPORT("failed to create hard link {} -> {}", sp_fmt_str(path), sp_fmt_str(target)); SP_FAIL(); diff --git a/test/fs/get_cwd.c b/test/fs/get_cwd.c index d64fe47..b9f5d4e 100644 --- a/test/fs/get_cwd.c +++ b/test/fs/get_cwd.c @@ -1,30 +1,11 @@ #include "fs.h" -UTEST(fs_get_cwd, basic_properties) { +UTEST(fs_get_cwd, contract) { SKIP_ON_WASM() sp_mem_t a = sp_mem_os_new(); sp_str_t cwd = sp_fs_get_cwd(a); - ASSERT_GT(cwd.len, 0); ASSERT_TRUE(sp_fs_is_dir(cwd)); - ASSERT_NE(cwd.data[cwd.len - 1], '/'); -} - -UTEST(fs_get_cwd, is_normalized) { - SKIP_ON_WASM() - sp_mem_t a = sp_mem_os_new(); - sp_str_t cwd = sp_fs_get_cwd(a); - // no backslashes - sp_for(i, cwd.len) { - ASSERT_NE(cwd.data[i], '\\'); - } -} - -UTEST(fs_get_cwd, is_absolute) { - SKIP_ON_WASM() - sp_mem_t a = sp_mem_os_new(); - sp_str_t cwd = sp_fs_get_cwd(a); - bool is_absolute = (cwd.data[0] == '/') || (cwd.len >= 2 && cwd.data[1] == ':'); - ASSERT_TRUE(is_absolute); + ASSERT_TRUE(sp_fs_is_absolute(cwd)); } UTEST(fs_get_cwd, unlinked_cwd_does_not_leak_deleted_suffix) { diff --git a/test/fs/get_exe_path.c b/test/fs/get_exe_path.c index 8519144..2390350 100644 --- a/test/fs/get_exe_path.c +++ b/test/fs/get_exe_path.c @@ -1,6 +1,6 @@ #include "fs.h" -UTEST(fs_get_exe_path, basic_properties) { +UTEST_F(fs, basic_properties) { SKIP_ON_WASM() sp_mem_t a = sp_mem_os_new(); sp_str_t exe = sp_fs_get_exe_path(a); @@ -19,7 +19,7 @@ UTEST(fs_get_exe_path, basic_properties) { ASSERT_GT(name.len, 0); } -UTEST(fs_get_exe_path, is_absolute) { +UTEST_F(fs, is_absolute) { SKIP_ON_WASM() sp_mem_t a = sp_mem_os_new(); sp_str_t exe = sp_fs_get_exe_path(a); @@ -28,23 +28,22 @@ UTEST(fs_get_exe_path, is_absolute) { ASSERT_TRUE(is_absolute); } -UTEST(fs_get_exe_path, exists_on_disk) { +UTEST_F(fs, exists_on_disk) { SKIP_ON_WASM() sp_mem_t a = sp_mem_os_new(); sp_str_t exe = sp_fs_get_exe_path(a); ASSERT_TRUE(sp_fs_exists(exe)); } -UTEST(fs_get_exe_path, is_canonical) { +UTEST_F(fs, is_canonical) { SKIP_ON_WASM() - // canonicalizing the exe path should be a no-op sp_mem_t a = sp_mem_os_new(); sp_str_t exe = sp_fs_get_exe_path(a); sp_str_t canonical = sp_fs_canonicalize_path(a, exe); SP_EXPECT_STR_EQ(canonical, exe); } -UTEST(fs_get_exe_path, no_dotdot) { +UTEST_F(fs, no_dotdot) { SKIP_ON_WASM() sp_mem_t a = sp_mem_os_new(); sp_str_t exe = sp_fs_get_exe_path(a); diff --git a/test/fs/is_absolute.c b/test/fs/is_absolute.c new file mode 100644 index 0000000..8d6e51f --- /dev/null +++ b/test/fs/is_absolute.c @@ -0,0 +1,34 @@ +#include "fs.h" + +typedef struct { + const c8* input; + bool posix; + bool windows; +} is_absolute_case_t; + +UTEST(fs_is_absolute, cases) { + SKIP_ON_WASM() + is_absolute_case_t cases[] = { + { "", false, false }, + { "/", true, true }, + { "\\", true, true }, + { "//", true, true }, + { "/foo", true, true }, + { "\\foo", true, true }, + { "foo", false, false }, + { "foo/bar", false, false }, + { "C:", false, false }, + { "a:", false, false }, + { "C:foo", false, false }, + { "C:/", false, true }, + { "C:\\", false, true }, + { "C:/foo", false, true }, + { "C:\\foo", false, true }, + }; + + SP_CARR_FOR(cases, i) { + sp_str_t path = sp_str_view(cases[i].input); + EXPECT_EQ(sp_fs_is_absolute_for(path, SP_FS_PATH_POSIX), cases[i].posix); + EXPECT_EQ(sp_fs_is_absolute_for(path, SP_FS_PATH_WINDOWS), cases[i].windows); + } +} diff --git a/test/fs/is_root.c b/test/fs/is_root.c index e430325..e930454 100644 --- a/test/fs/is_root.c +++ b/test/fs/is_root.c @@ -8,16 +8,17 @@ typedef struct { UTEST(fs_is_root, cases) { SKIP_ON_WASM() is_root_case_t cases[] = { - { "", true }, - { "/", true }, - { "C:", true }, - { "a:", true }, - { "C:/", true }, - { "C:\\", true }, - { "foo", false }, - { "/foo", false }, - { "C:/foo",false }, - { "//", false }, + { "", false }, + { "/", true }, + { "\\", true }, + { "C:", false }, + { "a:", false }, + { "C:/", true }, + { "C:\\", true }, + { "foo", false }, + { "/foo", false }, + { "C:/foo", false }, + { "//", false }, }; SP_CARR_FOR(cases, i) { diff --git a/test/process.c b/test/process.c index f8d3c22..d1539be 100644 --- a/test/process.c +++ b/test/process.c @@ -190,6 +190,25 @@ s32 main(s32 num_args, const c8** args) { } break; } + case TEST_PROC_FUNCTION_BLOCK_UNTIL_EOF: { + u8 buffer[256]; + while (true) { + s64 n = sp_sys_read(sp_sys_stdin, buffer, sizeof(buffer)); + if (n == 0) break; + if (n < 0) return 1; + } + return exit_code; + } + case TEST_PROC_FUNCTION_DELAY_AFTER_EOF: { + u8 buffer[256]; + while (true) { + s64 n = sp_sys_read(sp_sys_stdin, buffer, sizeof(buffer)); + if (n == 0) break; + if (n < 0) return 1; + } + sp_os_sleep_ms(200); + return exit_code; + } default: { sp_log_err("Unknown function: {}", sp_fmt_cstr(function_str)); return 1; diff --git a/test/process.h b/test/process.h index 93b0b8e..585d79f 100644 --- a/test/process.h +++ b/test/process.h @@ -24,7 +24,9 @@ typedef enum { X(TEST_PROC_FUNCTION_WAIT, "wait") \ X(TEST_PROC_FUNCTION_EXIT_CODE, "exit_code") \ X(TEST_PROC_FUNCTION_FLOOD, "flood") \ - X(TEST_PROC_FUNCTION_PATTERN, "pattern") + X(TEST_PROC_FUNCTION_PATTERN, "pattern") \ + X(TEST_PROC_FUNCTION_BLOCK_UNTIL_EOF, "block_until_eof") \ + X(TEST_PROC_FUNCTION_DELAY_AFTER_EOF, "delay_after_eof") typedef enum { TEST_PROC_FUNCTION(SP_X_NAMED_ENUM_DEFINE) diff --git a/test/ps.c b/test/ps.c index ecb0e2a..90ed1d1 100644 --- a/test/ps.c +++ b/test/ps.c @@ -667,19 +667,28 @@ UTEST_F(ps, run) { } UTEST_F(ps, poll_while_process_running) { - sp_ps_t ps = sp_ps_create(ut.mem, (sp_ps_config_t) { - .command = get_process_path(ut.mem), + sp_ps_t ps = sp_ps_create_c(ut.mem, (sp_ps_config_cstr_t) { + .command = get_process_path_c(ut.mem), .args = { - sp_str_lit("--fn"), sp_str_lit("wait"), - sp_str_lit("100") + "--fn", "block_until_eof", + "--exit-code", "69", + }, + .io = { + .in = { .mode = SP_PS_IO_MODE_CREATE }, + .out = { .mode = SP_PS_IO_MODE_NULL }, + .err = { .mode = SP_PS_IO_MODE_NULL }, } }); + ASSERT_NE(ps.os, SP_NULLPTR); sp_ps_status_t result = sp_ps_poll(&ps, 0); EXPECT_EQ(result.state, SP_PS_STATE_RUNNING); + EXPECT_EQ(sp_io_stream_writer_close(sp_ps_io_in(&ps)), SP_OK); + result = sp_ps_wait(&ps); EXPECT_EQ(result.state, SP_PS_STATE_DONE); + EXPECT_EQ(result.exit_code, 69); } UTEST_F(ps, process_complete_during_poll) { @@ -746,43 +755,86 @@ UTEST_F(ps, wait_twice_while_process_running) { EXPECT_EQ(result.exit_code, -1); } -UTEST_F(ps, poll_then_wait) { +UTEST_F(ps, poll_zero_idempotent) { sp_ps_t ps = sp_ps_create_c(ut.mem, (sp_ps_config_cstr_t) { .command = get_process_path_c(ut.mem), .args = { - "--fn", "wait", - "100" + "--fn", "block_until_eof", + "--exit-code", "69", + }, + .io = { + .in = { .mode = SP_PS_IO_MODE_CREATE }, + .out = { .mode = SP_PS_IO_MODE_NULL }, + .err = { .mode = SP_PS_IO_MODE_NULL }, } }); + ASSERT_NE(ps.os, SP_NULLPTR); - sp_ps_status_t result = sp_ps_poll(&ps, 0); - EXPECT_EQ(result.state, SP_PS_STATE_RUNNING); + sp_ps_status_t result = sp_zero; - result = sp_ps_wait(&ps); + sp_for(i, 3) { + result = sp_ps_poll(&ps, 0); + EXPECT_EQ(result.state, SP_PS_STATE_RUNNING); + } + + EXPECT_EQ(sp_io_stream_writer_close(sp_ps_io_in(&ps)), SP_OK); + + result = sp_ps_poll(&ps, 10000); EXPECT_EQ(result.state, SP_PS_STATE_DONE); - EXPECT_EQ(result.exit_code, sp_test_ps_wait_exit_code); + EXPECT_EQ(result.exit_code, 69); } -UTEST_F(ps, poll_multiple) { +UTEST_F(ps, poll_timeout_with_running_process) { sp_ps_t ps = sp_ps_create_c(ut.mem, (sp_ps_config_cstr_t) { .command = get_process_path_c(ut.mem), .args = { - "--fn", "wait", - "300" + "--fn", "block_until_eof", + "--exit-code", "69", + }, + .io = { + .in = { .mode = SP_PS_IO_MODE_CREATE }, + .out = { .mode = SP_PS_IO_MODE_NULL }, + .err = { .mode = SP_PS_IO_MODE_NULL }, } }); + ASSERT_NE(ps.os, SP_NULLPTR); - sp_ps_status_t result = sp_zero; + sp_tm_timer_t timer = sp_tm_start_timer(); + sp_ps_status_t result = sp_ps_poll(&ps, 100); + u64 elapsed_ms = (u64)sp_tm_ns_to_ms_f((f64)sp_tm_read_timer(&timer)); - result = sp_ps_poll(&ps, 50); EXPECT_EQ(result.state, SP_PS_STATE_RUNNING); + EXPECT_GE(elapsed_ms, (u64)80); - result = sp_ps_poll(&ps, 50); - EXPECT_EQ(result.state, SP_PS_STATE_RUNNING); + EXPECT_EQ(sp_io_stream_writer_close(sp_ps_io_in(&ps)), SP_OK); + result = sp_ps_wait(&ps); + EXPECT_EQ(result.state, SP_PS_STATE_DONE); +} + +UTEST_F(ps, poll_returns_on_completion) { + sp_ps_t ps = sp_ps_create_c(ut.mem, (sp_ps_config_cstr_t) { + .command = get_process_path_c(ut.mem), + .args = { + "--fn", "delay_after_eof", + "--exit-code", "69", + }, + .io = { + .in = { .mode = SP_PS_IO_MODE_CREATE }, + .out = { .mode = SP_PS_IO_MODE_NULL }, + .err = { .mode = SP_PS_IO_MODE_NULL }, + } + }); + ASSERT_NE(ps.os, SP_NULLPTR); + + sp_tm_timer_t timer = sp_tm_start_timer(); + EXPECT_EQ(sp_io_stream_writer_close(sp_ps_io_in(&ps)), SP_OK); + sp_ps_status_t result = sp_ps_poll(&ps, 10000); + u64 elapsed_ms = (u64)sp_tm_ns_to_ms_f((f64)sp_tm_read_timer(&timer)); - result = sp_ps_poll(&ps, 300); EXPECT_EQ(result.state, SP_PS_STATE_DONE); - EXPECT_EQ(result.exit_code, sp_test_ps_wait_exit_code); + EXPECT_EQ(result.exit_code, 69); + EXPECT_GE(elapsed_ms, (u64)150); + EXPECT_LE(elapsed_ms, (u64)5000); } UTEST_F(ps, wait_with_output) { @@ -813,8 +865,8 @@ UTEST_F(ps, poll_with_io) { sp_ps_t ps = sp_ps_create_c(ut.mem, (sp_ps_config_cstr_t) { .command = get_process_path_c(ut.mem), .args = { - "--fn", "wait", - "100" + "--fn", "block_until_eof", + "--exit-code", "69", }, .io = { .in = { .mode = SP_PS_IO_MODE_CREATE }, @@ -822,15 +874,19 @@ UTEST_F(ps, poll_with_io) { .err = { .mode = SP_PS_IO_MODE_NULL }, } }); + ASSERT_NE(ps.os, SP_NULLPTR); - sp_ps_status_t r1 = sp_ps_poll(&ps, 10); + sp_ps_status_t r1 = sp_ps_poll(&ps, 0); EXPECT_EQ(r1.state, SP_PS_STATE_RUNNING); sp_io_stream_writer_t* in = sp_ps_io_in(&ps); - EXPECT_NE(in, SP_NULLPTR); + ASSERT_NE(in, SP_NULLPTR); + + EXPECT_EQ(sp_io_stream_writer_close(in), SP_OK); sp_ps_status_t r2 = sp_ps_wait(&ps); EXPECT_EQ(r2.state, SP_PS_STATE_DONE); + EXPECT_EQ(r2.exit_code, 69); } UTEST_F(ps, interleaved_read_write) { diff --git a/test/tools/test.h b/test/tools/test.h index 255f43a..0561e0d 100644 --- a/test/tools/test.h +++ b/test/tools/test.h @@ -103,6 +103,8 @@ typedef struct { void sp_test_file_manager_init(sp_test_file_manager_t* manager); sp_str_t sp_test_file_path(sp_test_file_manager_t* manager, sp_str_t name); +sp_str_t sp_test_file_path_c(sp_test_file_manager_t* manager, const c8* name); +sp_str_t sp_test_file_create_dir(sp_test_file_manager_t* fs, const c8* relative); void sp_test_file_create_ex(sp_test_file_config_t config); sp_str_t sp_test_file_create_empty(sp_test_file_manager_t* manager, sp_str_t path); void sp_test_file_manager_cleanup(sp_test_file_manager_t* manager); @@ -190,6 +192,10 @@ void sp_test_file_manager_init(sp_test_file_manager_t* fs) { } } +sp_str_t sp_test_file_path_c(sp_test_file_manager_t* manager, const c8* name) { + return sp_test_file_path(manager, sp_cstr_as_str(name)); +} + sp_str_t sp_test_file_path(sp_test_file_manager_t* manager, sp_str_t name) { return sp_fs_join_path(manager->mem, manager->paths.test, name); } @@ -225,6 +231,12 @@ sp_str_t sp_test_file_create_empty(sp_test_file_manager_t* manager, sp_str_t rel return path; } +sp_str_t sp_test_file_create_dir(sp_test_file_manager_t* fs, const c8* relative) { + sp_str_t path = sp_test_file_path_c(fs, relative); + sp_fs_create_dir(path); + return path; +} + void sp_test_file_manager_cleanup(sp_test_file_manager_t* manager) { if (sp_str_empty(manager->paths.test)) { return; diff --git a/test/tools/utest.h b/test/tools/utest.h index 290a4b6..92e511d 100644 --- a/test/tools/utest.h +++ b/test/tools/utest.h @@ -321,6 +321,8 @@ static UTEST_INLINE int utest_strncmp(const c8 *a, const c8 *b, u32 n) { UTEST_SURPRESS_WARNING_END #endif +#define EXPECT_OK(x) EXPECT_EQ(x, SP_OK) + #define EXPECT_EQ(x, y) UTEST_COND(x, y, ==, "", 0) #define EXPECT_EQ_MSG(x, y, msg) UTEST_COND(x, y, ==, msg, 0) #define ASSERT_EQ(x, y) UTEST_COND(x, y, ==, "", 1)