test(windows): build and run the test suite for Windows from WSL
scripts/test-windows.sh cross-compiles ObsidianDragonTests.exe (and the fake_dragonxd stub) with mingw into build/windows-tests, reusing the toolchain and dependency sources from build/windows, and runs it from WSL through Windows interop (or wine). All tests pass on Windows. CMake: - Windows forced BUILD_TESTING OFF in the cache to keep curl's tests out, which also removed ObsidianDragonTests from every Windows build. Hide it from curl with a scoped variable instead, and restore our cache value afterwards (curl's cmake_dependent_option FORCE-writes it). Windows still defaults to OFF, so release builds are unchanged. - ObsidianDragonTests now depends on copy_langs: i18n.cpp includes the generated embedded/lang_*.h, so a tree that builds only the tests failed (latent on Linux too). - Link the Windows system libraries the tests need. Tests: - testTmpPath/fixtureDir/removeTree helpers: no hard-coded /tmp, a runtime fixture-dir override (the compiled-in path is a Linux one), and best-effort cleanup that can't abort the suite. - Close the explorer cache before deleting its directory (Windows can't delete an open file). - Compile out the address-book and wallet-index round-trips on Windows: getConfigDir() uses SHGetFolderPath there, which no environment variable redirects, so they would overwrite the real %APPDATA%\ObsidianDragon files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+19
-1
@@ -155,6 +155,11 @@ if(DRAGONX_ENABLE_LITE_BACKEND)
|
||||
set(DRAGONX_LITE_BACKEND_READY ON)
|
||||
endif()
|
||||
|
||||
# Windows (mingw cross) builds skip the large test target unless asked for with -DBUILD_TESTING=ON
|
||||
# (scripts/test-windows.sh does that in its own build/windows-tests tree), so release builds are unchanged.
|
||||
if(WIN32 AND NOT DEFINED BUILD_TESTING)
|
||||
set(BUILD_TESTING OFF CACHE BOOL "Build the testing tree.")
|
||||
endif()
|
||||
include(CTest)
|
||||
|
||||
# Output directories
|
||||
@@ -266,7 +271,11 @@ if(WIN32)
|
||||
set(BUILD_CURL_EXE OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||
set(CURL_STATICLIB ON CACHE BOOL "" FORCE)
|
||||
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
|
||||
# Keep curl's own tests out, without switching off ObsidianDragonTests for every Windows build:
|
||||
# hide BUILD_TESTING behind a normal variable while curl configures, then restore our cache value
|
||||
# (curl's cmake_dependent_option(BUILD_TESTING ...) FORCE-writes the OFF it saw into the cache).
|
||||
set(_dragonx_build_testing "${BUILD_TESTING}")
|
||||
set(BUILD_TESTING OFF)
|
||||
set(CURL_DISABLE_LDAP ON CACHE BOOL "" FORCE)
|
||||
set(CURL_DISABLE_LDAPS ON CACHE BOOL "" FORCE)
|
||||
set(CURL_USE_SCHANNEL ON CACHE BOOL "" FORCE) # Use Windows native SSL
|
||||
@@ -275,6 +284,8 @@ if(WIN32)
|
||||
set(CURL_ZLIB OFF CACHE BOOL "" FORCE)
|
||||
set(HTTP_ONLY ON CACHE BOOL "" FORCE)
|
||||
FetchContent_MakeAvailable(curl)
|
||||
unset(BUILD_TESTING)
|
||||
set(BUILD_TESTING "${_dragonx_build_testing}" CACHE BOOL "Build the testing tree." FORCE)
|
||||
set(CURL_LIBRARIES libcurl_static)
|
||||
set(CURL_INCLUDE_DIRS ${curl_SOURCE_DIR}/include ${curl_BINARY_DIR}/lib/curl)
|
||||
else()
|
||||
@@ -1233,6 +1244,13 @@ if(BUILD_TESTING)
|
||||
if(UNIX)
|
||||
target_link_libraries(ObsidianDragonTests PRIVATE ${CMAKE_DL_LIBS})
|
||||
endif()
|
||||
if(WIN32)
|
||||
target_link_libraries(ObsidianDragonTests PRIVATE ws2_32 crypt32 wldap32 psapi iphlpapi shell32 ole32 advapi32 bcrypt)
|
||||
endif()
|
||||
# i18n.cpp includes the generated embedded/lang_*.h headers; a tree that builds only this target needs them too.
|
||||
if(TARGET copy_langs)
|
||||
add_dependencies(ObsidianDragonTests copy_langs)
|
||||
endif()
|
||||
|
||||
add_test(NAME ObsidianDragonPhase4Tests COMMAND ObsidianDragonTests)
|
||||
endif()
|
||||
|
||||
Executable
+76
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
# Cross-compile the test suite (ObsidianDragonTests.exe) and the fake daemon (tools/fake_dragonxd)
|
||||
# for Windows with mingw-w64, then run the tests: directly under WSL (Windows interop), else with
|
||||
# wine if installed, else just build.
|
||||
#
|
||||
# scripts/test-windows.sh [--no-run] [--jobs N]
|
||||
#
|
||||
# Uses its own build/windows-tests tree (full-node config), so build/windows is left alone. Needs
|
||||
# the mingw toolchain file that ./build.sh --win-release writes to build/windows/. Dependency sources
|
||||
# already fetched into build/windows/_deps are reused, so nothing is downloaded twice.
|
||||
set -euo pipefail
|
||||
|
||||
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
bd="$repo/build/windows-tests"
|
||||
toolchain="$repo/build/windows/mingw-toolchain.cmake"
|
||||
deps="$repo/build/windows/_deps"
|
||||
run=true
|
||||
jobs=8 # each test TU is large; several parallel mingw builds can exhaust RAM
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--no-run) run=false; shift ;;
|
||||
--jobs) jobs="$2"; shift 2 ;;
|
||||
-h|--help) sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) echo "unknown option: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ! -f "$toolchain" ]]; then
|
||||
echo "missing $toolchain; run ./build.sh --win-release once to create the mingw toolchain" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
src_args=()
|
||||
for dep in sdl3 json tomlplusplus sqlite3 curl libwebp; do
|
||||
if [[ -d "$deps/$dep-src" ]]; then
|
||||
src_args+=("-DFETCHCONTENT_SOURCE_DIR_${dep^^}=$deps/$dep-src")
|
||||
fi
|
||||
done
|
||||
|
||||
cmake -S "$repo" -B "$bd" \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$toolchain" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
||||
-DDRAGONX_BUILD_LITE=OFF \
|
||||
-DDRAGONX_ENABLE_LITE_BACKEND=OFF \
|
||||
-DBUILD_TESTING=ON \
|
||||
-DDRAGONX_BUILD_FAKE_DAEMON=ON \
|
||||
"${src_args[@]}"
|
||||
cmake --build "$bd" --target ObsidianDragonTests fake_dragonxd -j "$jobs"
|
||||
|
||||
exe="$bd/bin/ObsidianDragonTests.exe"
|
||||
echo "built: $exe"
|
||||
echo "built: $bd/fake_dragonxd/dragonxd.exe"
|
||||
$run || exit 0
|
||||
|
||||
# The fixture path compiled into the binary is a Linux path; hand the Windows process its own.
|
||||
# APPDATA/LOCALAPPDATA point at a sandbox so anything resolving them from the environment stays out
|
||||
# of the real profile. (SHGetFolderPath-based paths ignore the environment; the tests that would
|
||||
# write through those are compiled out on Windows.)
|
||||
export DRAGONX_TEST_FIXTURE_DIR="$repo/tests/fixtures"
|
||||
sandbox="$bd/test-appdata"
|
||||
mkdir -p "$sandbox"
|
||||
export APPDATA="$sandbox" LOCALAPPDATA="$sandbox"
|
||||
if grep -qi microsoft /proc/version 2>/dev/null && [[ -e /proc/sys/fs/binfmt_misc/WSLInterop ]]; then
|
||||
# /p: translate each to a Windows path for the Windows process
|
||||
export WSLENV="${WSLENV:+$WSLENV:}DRAGONX_TEST_FIXTURE_DIR/p:APPDATA/p:LOCALAPPDATA/p"
|
||||
cd "$bd/bin"
|
||||
exec "$exe"
|
||||
elif command -v wine >/dev/null 2>&1; then
|
||||
export DRAGONX_TEST_FIXTURE_DIR="$(winepath -w "$DRAGONX_TEST_FIXTURE_DIR")"
|
||||
cd "$bd/bin"
|
||||
exec wine "$exe"
|
||||
else
|
||||
echo "not on WSL and no wine: copy $exe (and tests/fixtures) to Windows to run it" >&2
|
||||
fi
|
||||
+54
-18
@@ -126,6 +126,29 @@ fs::path makeTempDir()
|
||||
return dir;
|
||||
}
|
||||
|
||||
// Best-effort cleanup. Windows can't delete a file that is still open, and the throwing remove_all
|
||||
// would abort the whole suite over a leftover temp file.
|
||||
void removeTree(const fs::path& path)
|
||||
{
|
||||
std::error_code ec;
|
||||
fs::remove_all(path, ec);
|
||||
}
|
||||
|
||||
// A named path under the platform temp dir (/tmp on Linux, %TEMP% on Windows, where "/tmp/..."
|
||||
// would mean C:\tmp).
|
||||
fs::path testTmpPath(const std::string& name)
|
||||
{
|
||||
return fs::temp_directory_path() / name;
|
||||
}
|
||||
|
||||
// The fixture root: compiled in, unless DRAGONX_TEST_FIXTURE_DIR is set at runtime. The Windows
|
||||
// build runs from WSL (scripts/test-windows.sh), where the compiled-in Linux path doesn't resolve.
|
||||
std::string fixtureDir()
|
||||
{
|
||||
const char* env = std::getenv("DRAGONX_TEST_FIXTURE_DIR");
|
||||
return (env && *env) ? std::string(env) : std::string(DRAGONX_TEST_FIXTURE_DIR);
|
||||
}
|
||||
|
||||
void writeTestFile(const fs::path& path, const std::string& content)
|
||||
{
|
||||
std::ofstream output(path, std::ios::binary);
|
||||
@@ -723,7 +746,7 @@ void testConnectionConfig()
|
||||
EXPECT_EQ(cookieConfig.auth_source, AuthSource::Cookie);
|
||||
EXPECT_TRUE(cookieConfig.use_tls);
|
||||
|
||||
fs::remove_all(dir);
|
||||
removeTree(dir);
|
||||
}
|
||||
|
||||
void testPaymentUri()
|
||||
@@ -763,7 +786,7 @@ void testWalletFileProbe()
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
using dragonx::util::probeWalletFile;
|
||||
const fs::path dir = fs::path("/tmp/dragonx") / "probe";
|
||||
const fs::path dir = testTmpPath("dragonx") / "probe";
|
||||
std::error_code ec; fs::create_directories(dir, ec);
|
||||
auto writef = [](const fs::path& p, const std::string& d) {
|
||||
std::ofstream o(p, std::ios::binary); o.write(d.data(), (std::streamsize)d.size());
|
||||
@@ -3292,7 +3315,7 @@ void testDaemonLifecycleAdapters()
|
||||
EXPECT_FALSE(fs::exists(dataDir / "peers.dat"));
|
||||
EXPECT_FALSE(fs::exists(dataDir / "fee_estimates.dat"));
|
||||
EXPECT_TRUE(fs::exists(dataDir / "wallet.dat"));
|
||||
fs::remove_all(dataDir);
|
||||
removeTree(dataDir);
|
||||
}
|
||||
|
||||
// Pure console wrap layout / hit-test / selection (console_text_layout.h), driven by a
|
||||
@@ -4189,7 +4212,8 @@ void testExplorerBlockCache()
|
||||
file << legacy.dump(2);
|
||||
}
|
||||
|
||||
ExplorerBlockCache cache(databasePath.string(), legacyPath.string());
|
||||
auto cacheOwner = std::make_unique<ExplorerBlockCache>(databasePath.string(), legacyPath.string());
|
||||
ExplorerBlockCache& cache = *cacheOwner;
|
||||
EXPECT_TRUE(cache.ensureOpen());
|
||||
EXPECT_EQ(cache.cachedBlockCount(), 2);
|
||||
|
||||
@@ -4234,7 +4258,8 @@ void testExplorerBlockCache()
|
||||
EXPECT_EQ(reopened.cachedBlockCount(), 0);
|
||||
}
|
||||
|
||||
fs::remove_all(dir);
|
||||
cacheOwner.reset(); // close the database first: Windows can't delete an open file
|
||||
removeTree(dir);
|
||||
}
|
||||
|
||||
void testTransactionHistoryCache()
|
||||
@@ -4345,7 +4370,7 @@ void testTransactionHistoryCache()
|
||||
EXPECT_EQ(cache.snapshotCount(), 0);
|
||||
}
|
||||
|
||||
fs::remove_all(dir);
|
||||
removeTree(dir);
|
||||
}
|
||||
|
||||
// GC: generating a new address changes the wallet-identity hash; the prior hash's snapshot
|
||||
@@ -4377,7 +4402,7 @@ void testTransactionHistoryCachePrunesOldWallets()
|
||||
auto loadedB = cache.load(idB, 100, "tip-b");
|
||||
EXPECT_TRUE(loadedB.loaded);
|
||||
|
||||
fs::remove_all(dir);
|
||||
removeTree(dir);
|
||||
}
|
||||
|
||||
void testTransactionHistoryCacheRefreshApply()
|
||||
@@ -4443,7 +4468,7 @@ void testTransactionHistoryCacheRefreshApply()
|
||||
EXPECT_EQ(lastTxBlock, 333);
|
||||
EXPECT_EQ(confirmedIds.count("rpc-refreshed-send"), static_cast<size_t>(1));
|
||||
|
||||
fs::remove_all(dir);
|
||||
removeTree(dir);
|
||||
}
|
||||
|
||||
|
||||
@@ -4753,7 +4778,7 @@ void testGeneratedResourceBehavior()
|
||||
EXPECT_TRUE(dragonx::resources::getEmbeddedResource("__missing_resource__") == nullptr);
|
||||
if (!dragonx::resources::hasEmbeddedResources()) {
|
||||
EXPECT_TRUE(themes[0].data == nullptr);
|
||||
EXPECT_EQ(dragonx::resources::extractBundledThemes("/tmp/obsidian-dragon-empty-themes"), 0);
|
||||
EXPECT_EQ(dragonx::resources::extractBundledThemes(testTmpPath("obsidian-dragon-empty-themes").string()), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6030,7 +6055,7 @@ void testLiteRolloutManifestLoader()
|
||||
{
|
||||
using namespace dragonx::wallet;
|
||||
// Missing file -> not present -> fail-open allowed.
|
||||
auto missing = loadLiteRolloutManifestFromFile("/tmp/obsidian-rollout-missing-xyz.json");
|
||||
auto missing = loadLiteRolloutManifestFromFile(testTmpPath("obsidian-rollout-missing-xyz.json").string());
|
||||
EXPECT_FALSE(missing.present);
|
||||
{
|
||||
LiteRolloutInputs in; in.appVersion = "1.2.0"; in.manifest = missing;
|
||||
@@ -6038,7 +6063,7 @@ void testLiteRolloutManifestLoader()
|
||||
}
|
||||
|
||||
// A valid manifest file parses into the expected fields.
|
||||
const std::string path = "/tmp/obsidian-rollout-test.json";
|
||||
const std::string path = testTmpPath("obsidian-rollout-test.json").string();
|
||||
{
|
||||
std::ofstream f(path);
|
||||
f << R"({"global_enabled":false,"min_version":"1.0.0","rollout_permille":250,"message":"hold"})";
|
||||
@@ -6053,7 +6078,7 @@ void testLiteRolloutManifestLoader()
|
||||
std::remove(path.c_str());
|
||||
|
||||
// Malformed JSON -> present but invalid -> fail-open allowed.
|
||||
const std::string badPath = "/tmp/obsidian-rollout-bad.json";
|
||||
const std::string badPath = testTmpPath("obsidian-rollout-bad.json").string();
|
||||
{ std::ofstream f(badPath); f << "{ this is not json"; }
|
||||
auto bad = loadLiteRolloutManifestFromFile(badPath);
|
||||
EXPECT_TRUE(bad.present);
|
||||
@@ -6098,7 +6123,7 @@ void testLiteWalletControllerRolloutGate()
|
||||
// ── xmrig updater pure core (util/xmrig_updater.h) — driven by the real release fixture ──
|
||||
static std::string readXmrigFixture()
|
||||
{
|
||||
const std::string path = std::string(DRAGONX_TEST_FIXTURE_DIR) + "/xmrig/release_latest.json";
|
||||
const std::string path = fixtureDir() + "/xmrig/release_latest.json";
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
return std::string((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
}
|
||||
@@ -6241,7 +6266,7 @@ void testAtomicFileWrite()
|
||||
using dragonx::util::Platform;
|
||||
|
||||
fs::path dir = fs::temp_directory_path() / "obsidian_atomic_test";
|
||||
fs::remove_all(dir);
|
||||
removeTree(dir);
|
||||
const std::string target = (dir / "nested" / "settings.json").string();
|
||||
|
||||
// Creates parent dirs and writes content.
|
||||
@@ -6270,7 +6295,7 @@ void testAtomicFileWrite()
|
||||
EXPECT_TRUE((perms & (fs::perms::group_all | fs::perms::others_all)) == fs::perms::none);
|
||||
#endif
|
||||
|
||||
fs::remove_all(dir);
|
||||
removeTree(dir);
|
||||
}
|
||||
|
||||
// Address checksum validation. Uses standard Base58Check / BIP173 Bech32 vectors — the
|
||||
@@ -6562,7 +6587,7 @@ void testXmrigSignatureVerify()
|
||||
// Reads any fixture file by repo-relative path under tests/fixtures/.
|
||||
static std::string readFixtureFile(const std::string& rel)
|
||||
{
|
||||
std::ifstream f(std::string(DRAGONX_TEST_FIXTURE_DIR) + "/" + rel, std::ios::binary);
|
||||
std::ifstream f(fixtureDir() + "/" + rel, std::ios::binary);
|
||||
return std::string((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
@@ -6761,7 +6786,7 @@ void testXmrigLiveInstall()
|
||||
return;
|
||||
}
|
||||
using namespace dragonx::util;
|
||||
const std::string dir = "/tmp/obsidian-xmrig-live-test";
|
||||
const std::string dir = testTmpPath("obsidian-xmrig-live-test").string();
|
||||
std::error_code ec; std::filesystem::remove_all(dir, ec);
|
||||
|
||||
XmrigUpdater up;
|
||||
@@ -7577,6 +7602,11 @@ void testAddressBookScope()
|
||||
EXPECT_TRUE(legacy.visibleInWallet("anything"));
|
||||
|
||||
// --- Round-trip + migration + scope-aware dedup, isolated under a temp $HOME ---
|
||||
#ifdef _WIN32
|
||||
// getConfigDir() uses SHGetFolderPath(CSIDL_APPDATA), which no env var redirects: this would
|
||||
// overwrite the real %APPDATA%\ObsidianDragon\addressbook.json.
|
||||
std::printf("[skip] testAddressBookScope round-trip (Windows config dir can't be redirected)\n");
|
||||
#else
|
||||
const char* oldHome = std::getenv("HOME");
|
||||
std::string savedHome = oldHome ? oldHome : "";
|
||||
fs::path tmp = fs::temp_directory_path() /
|
||||
@@ -7629,6 +7659,7 @@ void testAddressBookScope()
|
||||
|
||||
if (!savedHome.empty()) setenv("HOME", savedHome.c_str(), 1); else unsetenv("HOME");
|
||||
std::error_code ec; fs::remove_all(tmp, ec);
|
||||
#endif // !_WIN32
|
||||
}
|
||||
|
||||
// Wallet-files metadata index (multi-wallet P1b): upsert change-detection, find, extra folders,
|
||||
@@ -7658,7 +7689,11 @@ void testWalletIndex()
|
||||
EXPECT_TRUE(!idx.removeExtraFolder("/some/dir")); // already gone
|
||||
}
|
||||
|
||||
// save + reload round-trip under a temp $HOME
|
||||
// save + reload round-trip under a temp $HOME. Not on Windows: the config dir comes from
|
||||
// SHGetFolderPath there, so this would overwrite the real %APPDATA%\ObsidianDragon\wallets.json.
|
||||
#ifdef _WIN32
|
||||
std::printf("[skip] testWalletIndex round-trip (Windows config dir can't be redirected)\n");
|
||||
#else
|
||||
{
|
||||
const char* oldHome = std::getenv("HOME");
|
||||
std::string savedHome = oldHome ? oldHome : "";
|
||||
@@ -7693,6 +7728,7 @@ void testWalletIndex()
|
||||
if (!savedHome.empty()) setenv("HOME", savedHome.c_str(), 1); else unsetenv("HOME");
|
||||
std::error_code ec2; fs::remove_all(tmp, ec2);
|
||||
}
|
||||
#endif // !_WIN32
|
||||
}
|
||||
|
||||
int main()
|
||||
|
||||
@@ -61,10 +61,8 @@ Everything that matters for a test is appended to `<datadir>/fake_dragonxd.event
|
||||
cmake -S . -B build/linux -DDRAGONX_BUILD_FAKE_DAEMON=ON
|
||||
cmake --build build/linux --target fake_dragonxd # -> build/linux/fake_dragonxd/dragonxd
|
||||
|
||||
# Windows (mingw cross, from Linux/WSL), with the toolchain ./build.sh --win-release writes; own tree
|
||||
cmake -S . -B build/windows-tests -DCMAKE_TOOLCHAIN_FILE=build/windows/mingw-toolchain.cmake \
|
||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF -DDRAGONX_BUILD_FAKE_DAEMON=ON
|
||||
cmake --build build/windows-tests --target fake_dragonxd # -> build/windows-tests/fake_dragonxd/dragonxd.exe
|
||||
# Windows (mingw cross, from Linux/WSL): builds it with the Windows test suite in build/windows-tests
|
||||
scripts/test-windows.sh --no-run # -> build/windows-tests/fake_dragonxd/dragonxd.exe
|
||||
```
|
||||
|
||||
The output goes to its own `fake_dragonxd/` directory so it never replaces the real `bin/dragonxd`.
|
||||
|
||||
Reference in New Issue
Block a user