@samitouri / QOSAMI-WSL / commits / 4ebaffce

Enable unity builds for wsltests (#41247)

wsltests is a single large target whose compile time dominates local test iteration. Enabling CMake unity builds batches test sources into shared translation units, cutting wall-clock build time significantly. Batch size is controlled by the new WSL_TEST_UNITY_BATCH_SIZE cache variable (default 2). Setting it to 0 disables unity builds and compiles every file separately, which is useful for isolating a build break to a single source file. The value is validated at configure time and documented in UserConfig.cmake.sample. Concatenating sources into one translation unit surfaced latent collisions that file scope had previously hidden: - g_pipelineBuildId was defined as a separate file-static in both Common.cpp and InstallerTests.cpp. Common.cpp now owns the definition and InstallerTests.cpp declares it extern. - ProcessOutput was declared at namespace scope in both WslcSdkTests.cpp and WslcSdkWinRTTests.cpp. The latter is renamed to CapturedProcessOutput. - Broad `using namespace wsl::windows::common;` directives in WindowsUpdateTests.cpp, WSLCE2EHelpers.cpp, WSLCE2ETlsRegistryTests.cpp and WSLCExecutor.cpp leaked into neighboring sources and created ambiguous lookups. They are replaced with targeted using-declarations and namespace aliases. Unity builds also change test execution order, which broke assertions that implicitly depended on running before anything else had created a session. WSLCTests::ListSessions and WSLCTests::SessionManagement now share a ListTestSessionNames() helper that filters out the persistent sessions the wslc CLI creates for itself, so the assertions no longer depend on what else has run on the machine. SessionManagement is the only test that creates persistent sessions, which outlive the COM reference that created them. It now records the ones it creates and terminates them from a scope_exit handler, so a failure partway through cannot leak a session into subsequent runs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

ggarzia-MSFT committed Aug 6, 2026 at 17:53 UTC 4ebaffce6f04bb6a054fd1aeb22737f90f9c57f0
11 files changed +99 -40
CMakeLists.txt
+9
@@ -188,6 +188,15 @@ if (NOT DEFINED WSL_INCLUDE_SDK_CSHARP)
188 set(WSL_INCLUDE_SDK_CSHARP false)
189 endif ()
190
191 +# Number of test sources combined into each unity translation unit.
192 +# Set to 0 to compile every file separately.
193 +if (NOT DEFINED WSL_TEST_UNITY_BATCH_SIZE)
194 + set(WSL_TEST_UNITY_BATCH_SIZE 2)
195 +endif ()
196 +
197 +if (NOT WSL_TEST_UNITY_BATCH_SIZE MATCHES "^[0-9]+$")
198 + message(FATAL_ERROR "WSL_TEST_UNITY_BATCH_SIZE must be a non-negative integer: got '${WSL_TEST_UNITY_BATCH_SIZE}'")
199 +endif ()
200 find_commit_hash(COMMIT_HASH)
201
202 if (NOT PACKAGE_VERSION)
UserConfig.cmake.sample
+4
@@ -64,3 +64,7 @@ endif()
64 # # error - block the commit when formatting issues are found
65 # # fix - automatically fix formatting and re-stage files
66 # set(WSL_PRE_COMMIT_MODE "warn")
67 +
68 +# # Uncomment to change how many test sources share a unity translation unit (default: 2).
69 +# # Use 0 to disable unity builds and compile every test file separately.
70 +# set(WSL_TEST_UNITY_BATCH_SIZE 4)
test/windows/CMakeLists.txt
+7
@@ -23,6 +23,13 @@ add_compile_definitions(INLINE_TEST_METHOD_MARKUP)
23
24 add_library(wsltests SHARED ${SOURCES} ${HEADERS})
25
26 +if (DEFINED WSL_TEST_UNITY_BATCH_SIZE AND WSL_TEST_UNITY_BATCH_SIZE GREATER 0)
27 + set_target_properties(wsltests PROPERTIES
28 + UNITY_BUILD ON
29 + UNITY_BUILD_MODE BATCH
30 + UNITY_BUILD_BATCH_SIZE ${WSL_TEST_UNITY_BATCH_SIZE})
31 +endif ()
32 +
33 target_include_directories(wsltests PRIVATE
34 ${CMAKE_SOURCE_DIR}/src/windows/WslcSDK
35 ${CMAKE_BINARY_DIR}/src/windows/WslcSDK/winrt/${TARGET_PLATFORM}/${CMAKE_BUILD_TYPE})
test/windows/Common.cpp
+1 -1
@@ -66,7 +66,7 @@ static std::wstring g_originalDefaultDistro;
66 std::wstring g_dumpFolder;
67 std::optional<std::wstring> g_dumpToolPath;
68 static bool g_enableWerReport = false;
69 -static std::wstring g_pipelineBuildId;
69 +std::wstring g_pipelineBuildId;
70 std::wstring g_testDistroPath;
71 std::wstring g_testDataPath;
72 bool g_fastTestRun = false; // True when test.bat was invoked with -f
test/windows/InstallerTests.cpp
+1 -1
@@ -25,7 +25,7 @@ using namespace wsl::windows::common::registry;
25 using unique_msi_handle = wil::unique_any<MSIHANDLE, decltype(MsiCloseHandle), &MsiCloseHandle>;
26
27 extern std::wstring g_dumpFolder;
28 -static std::wstring g_pipelineBuildId;
28 +extern std::wstring g_pipelineBuildId;
29
30 class InstallerTests
31 {
test/windows/WSLCTests.cpp
+62 -30
@@ -23,6 +23,7 @@ Abstract:
23 #include "ContainerNameGenerator.h"
24 #include "wslc/e2e/WSLCE2EHelpers.h"
25 #include "HttpHeaderEndDetector.h"
26 +#include "WSLCSessionDefaults.h"
27 #include <nlohmann/json.hpp>
28
29 using namespace std::literals::chrono_literals;
@@ -149,6 +150,38 @@ class WSLCTests
150 return sessionManager;
151 }
152
153 + // Returns true for the names the wslc CLI reserves for its default sessions.
154 + static bool IsCliSessionName(std::wstring_view Name)
155 + {
156 + constexpr std::wstring_view prefix{wsl::windows::wslc::DefaultSessionName};
157 +
158 + return Name.size() >= prefix.size() && wsl::shared::string::IsEqual(Name.substr(0, prefix.size()), prefix, true) &&
159 + (Name.size() == prefix.size() || Name[prefix.size()] == L'-');
160 + }
161 +
162 + // ListSessions() reports every session on the machine, including the persistent sessions the
163 + // wslc CLI creates for itself. Those are outside this class's control, so they are filtered
164 + // out to keep assertions independent of what else has run on the machine.
165 + static std::set<std::wstring> ListTestSessionNames(IWSLCSessionManager* SessionManager)
166 + {
167 + wil::unique_cotaskmem_array_ptr<WSLCSessionListEntry> sessions;
168 + VERIFY_SUCCEEDED(SessionManager->ListSessions(&sessions, sessions.size_address<ULONG>()));
169 +
170 + std::set<std::wstring> names;
171 + for (const auto& e : sessions)
172 + {
173 + if (IsCliSessionName(e.DisplayName))
174 + {
175 + continue;
176 + }
177 +
178 + auto [it, inserted] = names.emplace(e.DisplayName);
179 + VERIFY_IS_TRUE(inserted);
180 + }
181 +
182 + return names;
183 + }
184 +
185 wil::com_ptr<IWSLCSession> CreateSession(const WSLCSessionSettings& sessionSettings, WSLCSessionFlags Flags = WSLCSessionFlagsNone)
186 {
187 const auto sessionManager = OpenSessionManager();
@@ -364,36 +397,24 @@ class WSLCTests
397
398 // Act: list sessions
399 {
367 - wil::unique_cotaskmem_array_ptr<WSLCSessionListEntry> sessions;
368 - VERIFY_SUCCEEDED(sessionManager->ListSessions(&sessions, sessions.size_address<ULONG>()));
400 + const auto names = ListTestSessionNames(sessionManager.get());
401
402 // Assert
371 - VERIFY_ARE_EQUAL(sessions.size(), 1u);
372 - const auto& info = sessions[0];
403 + VERIFY_ARE_EQUAL(names.size(), 1u);
404
405 // SessionId is implementation detail (starts at 1), so we only assert DisplayName here.
375 - VERIFY_ARE_EQUAL(std::wstring(info.DisplayName), c_testSessionName);
406 + VERIFY_IS_TRUE(names.contains(c_testSessionName));
407 }
408
409 // List multiple sessions.
410 {
411 auto session2 = CreateSession(GetDefaultSessionSettings(L"wslc-test-list-2"));
412
382 - wil::unique_cotaskmem_array_ptr<WSLCSessionListEntry> sessions;
383 - VERIFY_SUCCEEDED(sessionManager->ListSessions(&sessions, sessions.size_address<ULONG>()));
413 + const auto names = ListTestSessionNames(sessionManager.get());
414
385 - VERIFY_ARE_EQUAL(sessions.size(), 2);
386 -
387 - std::vector<std::wstring> displayNames;
388 - for (const auto& e : sessions)
389 - {
390 - displayNames.push_back(e.DisplayName);
391 - }
392 -
393 - std::ranges::sort(displayNames);
394 -
395 - VERIFY_ARE_EQUAL(displayNames[0], c_testSessionName);
396 - VERIFY_ARE_EQUAL(displayNames[1], L"wslc-test-list-2");
415 + VERIFY_ARE_EQUAL(names.size(), 2u);
416 + VERIFY_IS_TRUE(names.contains(c_testSessionName));
417 + VERIFY_IS_TRUE(names.contains(L"wslc-test-list-2"));
418 }
419 }
420
@@ -10343,16 +10364,7 @@ class WSLCTests
10364 auto manager = OpenSessionManager();
10365
10366 auto expectSessions = [&](const std::vector<std::wstring>& expectedSessions) {
10346 - wil::unique_cotaskmem_array_ptr<WSLCSessionListEntry> sessions;
10347 - VERIFY_SUCCEEDED(manager->ListSessions(&sessions, sessions.size_address<ULONG>()));
10348 -
10349 - std::set<std::wstring> displayNames;
10350 - for (const auto& e : sessions)
10351 - {
10352 - auto [_, inserted] = displayNames.insert(e.DisplayName);
10353 -
10354 - VERIFY_IS_TRUE(inserted);
10355 - }
10367 + auto displayNames = ListTestSessionNames(manager.get());
10368
10369 for (const auto& e : expectedSessions)
10370 {
@@ -10373,7 +10385,27 @@ class WSLCTests
10385 }
10386 };
10387
10376 - auto create = [this](LPCWSTR Name, WSLCSessionFlags Flags) {
10388 + // Persistent sessions outlive the COM reference that created them, so a test that fails
10389 + // partway through would leave them behind for the next run to trip over. Terminate the ones
10390 + // this test created, however it exits.
10391 + std::set<std::wstring> persistentSessions;
10392 + auto terminatePersistentSessions = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
10393 + for (const auto& name : persistentSessions)
10394 + {
10395 + wil::com_ptr<IWSLCSession> session;
10396 + if (SUCCEEDED(manager->OpenSessionByName(name.c_str(), &session)))
10397 + {
10398 + LOG_IF_FAILED(session->Terminate());
10399 + }
10400 + }
10401 + });
10402 +
10403 + auto create = [&](LPCWSTR Name, WSLCSessionFlags Flags) {
10404 + if (WI_IsFlagSet(Flags, WSLCSessionFlagsPersistent))
10405 + {
10406 + persistentSessions.emplace(Name);
10407 + }
10408 +
10409 return CreateSession(GetDefaultSessionSettings(Name), Flags);
10410 };
10411
test/windows/WindowsUpdateTests.cpp
+2 -1
@@ -18,7 +18,8 @@ Abstract:
18 #include "Common.h"
19 #include "WindowsUpdateIntegration.h"
20
21 -using namespace wsl::windows::common;
21 +using wsl::windows::common::WindowsUpdateClassFactory;
22 +using wsl::windows::common::WindowsUpdateContext;
23 namespace WRL = Microsoft::WRL;
24
25 namespace {
test/windows/WslcSdkWinRTTests.cpp
+4 -4
@@ -54,7 +54,7 @@ extern bool g_fastTestRun;
54 #define DELETE_CONTAINER_ON_SCOPE_EXIT(container) SCOPE_CLEANUP(container.Delete(WSLCSDK::DeleteContainerOption::Force))
55 #define DELETE_IMAGE_ON_SCOPE_EXIT(imageName) SCOPE_CLEANUP(m_defaultSession.DeleteImage(imageName))
56
57 -struct ProcessOutput
57 +struct CapturedProcessOutput
58 {
59 uint32_t ExitCode;
60 std::wstring StandardOutput;
@@ -105,9 +105,9 @@ class WslcSdkWinRtTests
105 VERIFY_ARE_EQUAL(promise.get_future().wait_for(timeout), std::future_status::ready);
106 }
107
108 - ProcessOutput GetProcessOutput(WSLCSDK::Process const& process)
108 + CapturedProcessOutput GetProcessOutput(WSLCSDK::Process const& process)
109 {
110 - ProcessOutput output;
110 + CapturedProcessOutput output;
111 output.ExitCode = process.ExitCode();
112 output.StandardOutput = ReadStream(process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardOutput));
113 output.StandardError = ReadStream(process.GetOutputStream(WSLCSDK::ProcessOutputHandle::StandardError));
@@ -161,7 +161,7 @@ class WslcSdkWinRtTests
161
162 // Creates and starts a one-shot container, waits for the init process to
163 // exit, and returns the exit code.
164 - ProcessOutput RunContainerAndWaitForExit(winrt::hstring imageName, RunContainerOptions options = {})
164 + CapturedProcessOutput RunContainerAndWaitForExit(winrt::hstring imageName, RunContainerOptions options = {})
165 {
166 auto procSettings = WSLCSDK::ProcessSettings();
167 if (!options.commandLine.empty())
test/windows/wslc/e2e/WSLCE2EHelpers.cpp
+4 -1
@@ -26,7 +26,10 @@ extern std::wstring g_testDataPath;
26 namespace WSLCE2ETests {
27
28 using namespace WEX::Logging;
29 -using namespace wsl::windows::common;
29 +
30 +namespace wslc_schema = wsl::windows::common::wslc_schema;
31 +using wsl::windows::common::RunningWSLCContainer;
32 +using wsl::windows::common::WSLCContainerLauncher;
33
34 namespace {
35 // Lazily compute the session storage base path.
test/windows/wslc/e2e/WSLCE2ETlsRegistryTests.cpp
+2 -1
@@ -24,7 +24,8 @@ Abstract:
24
25 namespace WSLCE2ETests {
26 using namespace wsl::shared;
27 -using namespace wsl::windows::common;
27 +
28 +namespace wslutil = wsl::windows::common::wslutil;
29
30 namespace {
31 // The bridge IP assigned to the first container started in a fresh session. The registry is always
test/windows/wslc/e2e/WSLCExecutor.cpp
+3 -1
@@ -21,7 +21,9 @@ Abstract:
21 namespace WSLCE2ETests {
22
23 using namespace WEX::Logging;
24 -using namespace wsl::windows::common;
24 +
25 +namespace wslutil = wsl::windows::common::wslutil;
26 +using wsl::windows::common::SubProcess;
27
28 namespace {
29 wil::unique_handle GetNonElevatedPrimaryToken()