Add proper testing framework & integrate with CI

This commit is contained in:
Luke Street 2025-09-26 10:39:09 -06:00
parent c14ad86d72
commit 104e9e869d
10 changed files with 300 additions and 116 deletions

View File

@ -20,7 +20,15 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y file unzip wget
sudo apt-get install -y \
file \
unzip \
wget \
cmake \
ninja-build \
g++-multilib \
gcc-mingw-w64-i686 \
binutils-mingw-w64-i686
- name: Build debug
run: docker build --build-arg build_type=Debug --target export --output build_debug .
@ -40,6 +48,12 @@ jobs:
build/wibo Wii/1.7/mwcceppc.exe -nodefaults -c test/test.c -Itest -o test.o
file test.o
- name: Fixture tests
run: |
cmake -S . -B build_ctest -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON -DWIBO_ENABLE_FIXTURE_TESTS=ON
cmake --build build_ctest
ctest --test-dir build_ctest --output-on-failure
- name: Upload release
uses: actions/upload-artifact@v4
with:

32
AGENTS.md Normal file
View File

@ -0,0 +1,32 @@
# Repository Guidelines
## Project Structure & Module Organization
- Core launcher logic lives in `main.cpp`, `loader.cpp`, `files.cpp`, `handles.cpp` and `module_registry.cpp`; shared interfaces in headers near them.
- Windows API shims reside in `dll/`, grouped by emulated DLL name; keep new APIs in the matching file instead of creating ad-hoc helpers.
- Reusable utilities sit in `strutil.*`, `processes.*` and `resources.*`; prefer extending these before introducing new singleton modules.
- Sample fixtures for exercising the loader live in `test/`; keep new repros small and self-contained.
## Build, Test, and Development Commands
- `cmake -B build -DCMAKE_BUILD_TYPE=Debug` configures a 32-bit toolchain; ensure multilib packages are present.
- `cmake --build build --target wibo` compiles the shim; switch to `-DCMAKE_BUILD_TYPE=Release` for optimised binaries.
- `./build/wibo /path/to/program.exe` runs a Windows binary through the shim; use `WIBO_DEBUG=1` for verbose logging.
- `cmake -B build -DBUILD_TESTING=ON` + `ctest --test-dir build --output-on-failure` runs the self-checking WinAPI fixtures (requires `i686-w64-mingw32-gcc` and `i686-w64-mingw32-windres`).
- `clang-format -i path/to/file.cpp` and `clang-tidy path/to/file.cpp -p build` keep contributions aligned with the repo's tooling.
## Coding Style & Naming Conventions
- Formatting follows `.clang-format` (LLVM base, tabbed indentation width 4, 120 column limit); never hand-wrap differently.
- Prefer PascalCase for emulated Win32 entry points, camelCase for internal helpers, and SCREAMING_SNAKE_CASE for constants or macros.
- Document non-obvious control flow with short comments and keep platform-specific code paths behind descriptive helper functions.
## Testing Guidelines
- Fixture binaries live in `test/` and are compiled automatically when `BUILD_TESTING` is enabled; keep new repros small and self-contained (`test_<feature>.c`).
- All fixtures must self-assert; use `test_assert.h` helpers so `ctest` fails on mismatched WinAPI behaviour.
- Cross-compile new repros with `i686-w64-mingw32-gcc` (and `i686-w64-mingw32-windres` for resources); CMake handles this during the build, but direct invocation is useful while iterating.
- Run `ctest --test-dir build --output-on-failure` after rebuilding to verify changes; ensure failures print actionable diagnostics.
## Debugging Workflow
- Reproduce crashes under `gdb` (or `lldb`) with `-q -batch` to capture backtraces, register state, and the faulting instruction without interactive prompts.
- Enable `WIBO_DEBUG=1` and tee output to a log when running the guest binary; loader traces often pinpoint missing imports, resource lookups, or API shims that misbehave.
- Inspect relevant source right away—most issues stem from stubbed shims in `dll/`; compare the guest stack from `gdb` with those implementations.
- When host-side behaviour is suspect (filesystem, execve, etc.), rerun under `strace -f -o <log>`; this highlights missing files or permissions before the shim faults.
- If the `ghidra` MCP tool is available, request that the user import and analyze the guest binary; you can then use it to disassemble/decompile code around the crash site.

View File

@ -39,3 +39,94 @@ add_executable(wibo
)
target_link_libraries(wibo PRIVATE std::filesystem)
install(TARGETS wibo DESTINATION bin)
include(CTest)
if(BUILD_TESTING)
find_program(WIBO_MINGW_CC i686-w64-mingw32-gcc)
find_program(WIBO_MINGW_WINDRES i686-w64-mingw32-windres)
set(WIBO_HAVE_MINGW_TOOLCHAIN FALSE)
if(WIBO_MINGW_CC AND WIBO_MINGW_WINDRES)
set(WIBO_HAVE_MINGW_TOOLCHAIN TRUE)
endif()
option(WIBO_ENABLE_FIXTURE_TESTS "Build and run Windows fixture binaries through wibo" ${WIBO_HAVE_MINGW_TOOLCHAIN})
if(WIBO_ENABLE_FIXTURE_TESTS)
if(NOT WIBO_HAVE_MINGW_TOOLCHAIN)
message(WARNING "MinGW toolchain not found; skipping fixture tests")
else()
set(WIBO_TEST_BIN_DIR ${CMAKE_CURRENT_BINARY_DIR}/test)
file(MAKE_DIRECTORY ${WIBO_TEST_BIN_DIR})
add_custom_command(
OUTPUT ${WIBO_TEST_BIN_DIR}/external_exports.dll
COMMAND ${WIBO_MINGW_CC} -Wall -Wextra -O2 -shared
-o external_exports.dll
${CMAKE_CURRENT_SOURCE_DIR}/test/external_exports.c
WORKING_DIRECTORY ${WIBO_TEST_BIN_DIR}
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/test/external_exports.c)
add_custom_command(
OUTPUT ${WIBO_TEST_BIN_DIR}/test_external_dll.exe
COMMAND ${WIBO_MINGW_CC} -Wall -Wextra -O2
-I${CMAKE_CURRENT_SOURCE_DIR}/test
-o test_external_dll.exe
${CMAKE_CURRENT_SOURCE_DIR}/test/test_external_dll.c
WORKING_DIRECTORY ${WIBO_TEST_BIN_DIR}
DEPENDS
${CMAKE_CURRENT_SOURCE_DIR}/test/test_external_dll.c
${CMAKE_CURRENT_SOURCE_DIR}/test/test_assert.h)
add_custom_command(
OUTPUT ${WIBO_TEST_BIN_DIR}/test_resources_res.o
COMMAND ${WIBO_MINGW_WINDRES}
${CMAKE_CURRENT_SOURCE_DIR}/test/test_resources.rc
-O coff -o test_resources_res.o
WORKING_DIRECTORY ${WIBO_TEST_BIN_DIR}
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/test/test_resources.rc)
add_custom_command(
OUTPUT ${WIBO_TEST_BIN_DIR}/test_resources.exe
COMMAND ${WIBO_MINGW_CC} -Wall -Wextra -O2
-I${CMAKE_CURRENT_SOURCE_DIR}/test
-o test_resources.exe
${CMAKE_CURRENT_SOURCE_DIR}/test/test_resources.c
test_resources_res.o -lversion
WORKING_DIRECTORY ${WIBO_TEST_BIN_DIR}
DEPENDS
${CMAKE_CURRENT_SOURCE_DIR}/test/test_resources.c
${CMAKE_CURRENT_SOURCE_DIR}/test/test_assert.h
${WIBO_TEST_BIN_DIR}/test_resources_res.o)
add_custom_target(wibo_test_fixtures
DEPENDS
${WIBO_TEST_BIN_DIR}/external_exports.dll
${WIBO_TEST_BIN_DIR}/test_external_dll.exe
${WIBO_TEST_BIN_DIR}/test_resources.exe)
if(CMAKE_CONFIGURATION_TYPES)
set(_wibo_fixture_build_command
${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --config $<CONFIG> --target wibo_test_fixtures)
else()
set(_wibo_fixture_build_command
${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target wibo_test_fixtures)
endif()
add_test(NAME wibo.build_fixtures COMMAND ${_wibo_fixture_build_command})
add_test(NAME wibo.test_external_dll
COMMAND $<TARGET_FILE:wibo> ${WIBO_TEST_BIN_DIR}/test_external_dll.exe)
set_tests_properties(wibo.test_external_dll PROPERTIES
WORKING_DIRECTORY ${WIBO_TEST_BIN_DIR}
DEPENDS wibo.build_fixtures)
add_test(NAME wibo.test_resources
COMMAND $<TARGET_FILE:wibo> ${WIBO_TEST_BIN_DIR}/test_resources.exe)
set_tests_properties(wibo.test_resources PROPERTIES
WORKING_DIRECTORY ${WIBO_TEST_BIN_DIR}
DEPENDS wibo.build_fixtures)
endif()
endif()
endif()

View File

@ -4,9 +4,36 @@ A minimal, low-fuss wrapper that can run really simple command-line 32-bit Windo
Don't run this on any untrusted executables, I implore you. (Or probably just don't run it at all... :p)
cmake -B build
cmake --build build
build/wibo
## Building
```sh
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build --target wibo
```
`cmake -B build -DCMAKE_BUILD_TYPE=Release` to produce an optimized binary instead.
## Running
```sh
./build/wibo /path/to/program.exe
# or, with debug logging:
WIBO_DEBUG=1 ./build/wibo /path/to/program.exe
```
## Tests
Self-checking Windows fixtures run through CTest. They require a 32-bit MinGW cross toolchain (`i686-w64-mingw32-gcc` and `i686-w64-mingw32-windres`).
With the toolchain installed:
```sh
cmake -B build -DBUILD_TESTING=ON
cmake --build build
ctest --test-dir build --output-on-failure
```
This will cross-compile the fixture executables, run them through `wibo`, and fail if any WinAPI expectations are not met.
---

View File

@ -117,6 +117,8 @@ int *WIN_ENTRY __p___argc() { return &wibo::argc; }
size_t WIN_ENTRY strlen(const char *str) { return ::strlen(str); }
int WIN_ENTRY strcmp(const char *lhs, const char *rhs) { return ::strcmp(lhs, rhs); }
int WIN_ENTRY strncmp(const char *lhs, const char *rhs, size_t count) { return ::strncmp(lhs, rhs, count); }
void *WIN_ENTRY malloc(size_t size) { return ::malloc(size); }
@ -247,6 +249,8 @@ static void *resolveByName(const char *name) {
return (void *)crt::__p___argc;
if (strcmp(name, "strlen") == 0)
return (void *)crt::strlen;
if (strcmp(name, "strcmp") == 0)
return (void *)crt::strcmp;
if (strcmp(name, "strncmp") == 0)
return (void *)crt::strncmp;
if (strcmp(name, "malloc") == 0)

3
test/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*.o
*.dll
*.exe

View File

@ -1,32 +0,0 @@
CC = i686-w64-mingw32-gcc
WINDRES = i686-w64-mingw32-windres
CFLAGS = -Wall -Wextra -O2
DLL_SRC = external_exports.c
EXE_SRC = test_external_dll.c
DLL = external_exports.dll
EXE = test_external_dll.exe
RES_EXE_SRC = test_resources.c
RES_RC = test_resources.rc
RES_OBJ = test_resources_res.o
RES_EXE = test_resources.exe
all: $(DLL) $(EXE) $(RES_EXE)
$(DLL): $(DLL_SRC)
$(CC) $(CFLAGS) -shared -o $@ $<
$(EXE): $(EXE_SRC)
$(CC) $(CFLAGS) -o $@ $<
$(RES_OBJ): $(RES_RC)
$(WINDRES) $< -O coff -o $@
$(RES_EXE): $(RES_EXE_SRC) $(RES_OBJ)
$(CC) $(CFLAGS) -o $@ $^ -lversion
clean:
rm -f $(DLL) $(EXE) $(RES_EXE) $(RES_OBJ)
.PHONY: all clean

56
test/test_assert.h Normal file
View File

@ -0,0 +1,56 @@
#ifndef WIBO_TEST_ASSERT_H
#define WIBO_TEST_ASSERT_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define TEST_FAIL(fmt, ...) \
do { \
fprintf(stderr, "FAIL:%s:%d: " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__); \
exit(EXIT_FAILURE); \
} while (0)
#define TEST_CHECK(cond) \
do { \
if (!(cond)) { \
TEST_FAIL("Assertion '%s' failed", #cond); \
} \
} while (0)
#define TEST_CHECK_MSG(cond, fmt, ...) \
do { \
if (!(cond)) { \
TEST_FAIL(fmt, ##__VA_ARGS__); \
} \
} while (0)
#define TEST_CHECK_EQ(expected, actual) \
do { \
long long _expected_value = (long long)(expected); \
long long _actual_value = (long long)(actual); \
if (_expected_value != _actual_value) { \
TEST_FAIL("Expected %s (%lld) == %s (%lld)", \
#expected, _expected_value, #actual, _actual_value); \
} \
} while (0)
#define TEST_CHECK_U64_EQ(expected, actual) \
do { \
unsigned long long _expected_value = (unsigned long long)(expected); \
unsigned long long _actual_value = (unsigned long long)(actual); \
if (_expected_value != _actual_value) { \
TEST_FAIL("Expected %s (%llu) == %s (%llu)", \
#expected, _expected_value, #actual, _actual_value); \
} \
} while (0)
#define TEST_CHECK_STR_EQ(expected, actual) \
do { \
if (strcmp((expected), (actual)) != 0) { \
TEST_FAIL("Expected %s (\"%s\") == %s (\"%s\")", \
#expected, (expected), #actual, (actual)); \
} \
} while (0)
#endif

View File

@ -1,32 +1,32 @@
#include <windows.h>
#include <stdint.h>
#include <stdio.h>
#include "test_assert.h"
int main(void) {
typedef int (__stdcall *add_numbers_fn)(int, int);
typedef int (__stdcall *was_attached_fn)(void);
typedef int(__stdcall *add_numbers_fn)(int, int);
typedef int(__stdcall *was_attached_fn)(void);
HMODULE mod = LoadLibraryA("external_exports.dll");
if (!mod) {
printf("LoadLibraryA failed: %lu\n", GetLastError());
return 1;
}
HMODULE mod = LoadLibraryA("external_exports.dll");
TEST_CHECK_MSG(mod != NULL, "LoadLibraryA failed: %lu", (unsigned long)GetLastError());
add_numbers_fn add_numbers = (add_numbers_fn)GetProcAddress(mod, "add_numbers@8");
was_attached_fn was_attached = (was_attached_fn)GetProcAddress(mod, "was_attached@0");
if (!add_numbers || !was_attached) {
printf("GetProcAddress failed: %lu\n", GetLastError());
return 1;
}
FARPROC raw_add_numbers = GetProcAddress(mod, "add_numbers@8");
FARPROC raw_was_attached = GetProcAddress(mod, "was_attached@0");
TEST_CHECK_MSG(raw_add_numbers != NULL, "GetProcAddress(add_numbers@8) failed: %lu", (unsigned long)GetLastError());
TEST_CHECK_MSG(raw_was_attached != NULL, "GetProcAddress(was_attached@0) failed: %lu", (unsigned long)GetLastError());
int sum = add_numbers(2, 40);
int attached = was_attached();
add_numbers_fn add_numbers = (add_numbers_fn)(uintptr_t)raw_add_numbers;
was_attached_fn was_attached = (was_attached_fn)(uintptr_t)raw_was_attached;
printf("sum=%d attached=%d\n", sum, attached);
int sum = add_numbers(2, 40);
int attached = was_attached();
if (!FreeLibrary(mod)) {
printf("FreeLibrary failed: %lu\n", GetLastError());
return 1;
}
TEST_CHECK_EQ(42, sum);
TEST_CHECK_EQ(1, attached);
return (sum == 42 && attached == 1) ? 0 : 2;
TEST_CHECK_MSG(FreeLibrary(mod) != 0, "FreeLibrary failed: %lu", (unsigned long)GetLastError());
printf("external_exports: sum=%d attached=%d\n", sum, attached);
return EXIT_SUCCESS;
}

View File

@ -2,86 +2,75 @@
#include <stdio.h>
#include <stdlib.h>
#include "test_assert.h"
int main(void) {
char buffer[128];
int copied = LoadStringA(GetModuleHandleA(NULL), 100, buffer, sizeof(buffer));
if (copied <= 0) {
printf("LoadString failed: %lu\n", GetLastError());
return 1;
}
printf("STRING[100]=%s\n", buffer);
TEST_CHECK_MSG(copied > 0, "LoadStringA failed: %lu", (unsigned long)GetLastError());
TEST_CHECK_EQ((int)strlen("Resource string 100"), copied);
TEST_CHECK_STR_EQ("Resource string 100", buffer);
HRSRC versionInfo = FindResourceA(NULL, MAKEINTRESOURCEA(1), MAKEINTRESOURCEA(RT_VERSION));
if (!versionInfo) {
printf("FindResource version failed: %lu\n", GetLastError());
return 1;
}
TEST_CHECK_MSG(versionInfo != NULL, "FindResourceA version failed: %lu", (unsigned long)GetLastError());
DWORD versionSize = SizeofResource(NULL, versionInfo);
if (!versionSize) {
printf("SizeofResource failed: %lu\n", GetLastError());
return 1;
}
printf("VERSION size=%lu\n", (unsigned long)versionSize);
TEST_CHECK_MSG(versionSize != 0, "SizeofResource failed: %lu", (unsigned long)GetLastError());
TEST_CHECK_EQ(364, (int)versionSize);
char modulePath[MAX_PATH];
DWORD moduleLen = GetModuleFileNameA(NULL, modulePath, sizeof(modulePath));
if (moduleLen == 0 || moduleLen >= sizeof(modulePath)) {
printf("GetModuleFileNameA failed: %lu\n", GetLastError());
return 1;
}
TEST_CHECK_MSG(moduleLen > 0 && moduleLen < sizeof(modulePath),
"GetModuleFileNameA failed: %lu", (unsigned long)GetLastError());
DWORD handle = 0;
DWORD infoSize = GetFileVersionInfoSizeA(modulePath, &handle);
if (!infoSize) {
printf("GetFileVersionInfoSizeA failed: %lu\n", GetLastError());
return 1;
}
TEST_CHECK_MSG(infoSize != 0, "GetFileVersionInfoSizeA failed: %lu", (unsigned long)GetLastError());
char *infoBuffer = (char *)malloc(infoSize);
if (!infoBuffer) {
printf("malloc failed\n");
return 1;
}
TEST_CHECK_MSG(infoBuffer != NULL, "malloc(%lu) failed", (unsigned long)infoSize);
if (!GetFileVersionInfoA(modulePath, 0, infoSize, infoBuffer)) {
printf("GetFileVersionInfoA failed: %lu\n", GetLastError());
free(infoBuffer);
return 1;
}
TEST_CHECK_MSG(GetFileVersionInfoA(modulePath, 0, infoSize, infoBuffer) != 0,
"GetFileVersionInfoA failed: %lu", (unsigned long)GetLastError());
VS_FIXEDFILEINFO *fixedInfo = NULL;
unsigned int fixedSize = 0;
if (!VerQueryValueA(infoBuffer, "\\", (void **)&fixedInfo, &fixedSize)) {
printf("VerQueryValueA root failed\n");
free(infoBuffer);
return 1;
}
printf("FILEVERSION=%u.%u.%u.%u\n",
fixedInfo->dwFileVersionMS >> 16,
fixedInfo->dwFileVersionMS & 0xFFFF,
fixedInfo->dwFileVersionLS >> 16,
fixedInfo->dwFileVersionLS & 0xFFFF);
TEST_CHECK_MSG(VerQueryValueA(infoBuffer, "\\", (void **)&fixedInfo, &fixedSize) != 0 &&
fixedInfo != NULL,
"VerQueryValueA root failed");
TEST_CHECK_MSG(fixedSize >= sizeof(*fixedInfo),
"Unexpected VS_FIXEDFILEINFO size: %u", fixedSize);
TEST_CHECK_EQ(1, (int)(fixedInfo->dwFileVersionMS >> 16));
TEST_CHECK_EQ(2, (int)(fixedInfo->dwFileVersionMS & 0xFFFF));
TEST_CHECK_EQ(3, (int)(fixedInfo->dwFileVersionLS >> 16));
TEST_CHECK_EQ(4, (int)(fixedInfo->dwFileVersionLS & 0xFFFF));
struct { WORD wLanguage; WORD wCodePage; } *translations = NULL;
unsigned int transSize = 0;
if (VerQueryValueA(infoBuffer, "\\VarFileInfo\\Translation", (void **)&translations, &transSize) &&
translations && transSize >= sizeof(*translations)) {
printf("Translation=%04X %04X\n", translations[0].wLanguage, translations[0].wCodePage);
char subBlock[64];
snprintf(subBlock, sizeof(subBlock), "\\StringFileInfo\\%04X%04X\\ProductVersion",
translations[0].wLanguage, translations[0].wCodePage);
char *productVersion = NULL;
unsigned int pvSize = 0;
printf("Querying %s\n", subBlock);
if (VerQueryValueA(infoBuffer, subBlock, (void **)&productVersion, &pvSize) && productVersion) {
printf("PRODUCTVERSION=%s\n", productVersion);
} else {
printf("ProductVersion lookup failed\n");
}
} else {
printf("ProductVersion lookup failed\n");
}
TEST_CHECK_MSG(VerQueryValueA(infoBuffer, "\\VarFileInfo\\Translation",
(void **)&translations, &transSize) != 0 &&
translations != NULL,
"Translation lookup failed");
TEST_CHECK_MSG(transSize >= sizeof(*translations),
"Translation block too small: %u", transSize);
TEST_CHECK_EQ(0x0409, translations[0].wLanguage);
TEST_CHECK_EQ(0x04B0, translations[0].wCodePage);
char subBlock[64];
int subLen = snprintf(subBlock, sizeof(subBlock),
"\\StringFileInfo\\%04X%04X\\ProductVersion",
translations[0].wLanguage, translations[0].wCodePage);
TEST_CHECK_MSG(subLen > 0 && (size_t)subLen < sizeof(subBlock),
"Failed to build ProductVersion path");
char *productVersion = NULL;
unsigned int pvSize = 0;
TEST_CHECK_MSG(VerQueryValueA(infoBuffer, subBlock, (void **)&productVersion, &pvSize) != 0 &&
productVersion != NULL,
"ProductVersion lookup failed");
TEST_CHECK_STR_EQ("1.2.3-test", productVersion);
free(infoBuffer);
return 0;
puts("resource metadata validated");
return EXIT_SUCCESS;
}