@samitouri / QOSAMI-WSL / commits / 843b4fc8

Improve wlsc cli e2e test coverage (#40753)

Add test cases: WSLCE2ETests::WSLCE2EContainerCreateTests::WSLCE2E_Container_Create_Cpus WSLCE2ETests::WSLCE2EContainerCreateTests::WSLCE2E_Container_Create_Cpus_Invalid WSLCE2ETests::WSLCE2EContainerCreateTests::WSLCE2E_Container_Create_Memory WSLCE2ETests::WSLCE2EContainerCreateTests::WSLCE2E_Container_Create_Memory_Invalid WSLCE2ETests::WSLCE2EContainerCreateTests::WSLCE2E_Container_Create_Ulimit WSLCE2ETests::WSLCE2EContainerCreateTests::WSLCE2E_Container_Create_Ulimit_Invalid WSLCE2ETests::WSLCE2EContainerCreateTests::WSLCE2E_Container_Create_Entrypoint WSLCE2ETests::WSLCE2EContainerCreateTests::WSLCE2E_Container_Create_EnvFile WSLCE2ETests::WSLCE2EContainerCreateTests::WSLCE2E_Container_Create_EnvFile_MissingFile WSLCE2ETests::WSLCE2EContainerCreateTests::WSLCE2E_Container_Create_EnvFile_InvalidContent WSLCE2ETests::WSLCE2EContainerCreateTests::WSLCE2E_Container_Create_Publish_TCP WSLCE2ETests::WSLCE2EContainerCreateTests::WSLCE2E_Container_Create_Publish_MultipleMappings WSLCE2ETests::WSLCE2EContainerCreateTests::WSLCE2E_Container_Create_Publish_Ephemeral WSLCE2ETests::WSLCE2EContainerCreateTests::WSLCE2E_Container_Create_PortUdp_NotSupported WSLCE2ETests::WSLCE2EContainerCreateTests::WSLCE2E_Container_Create_PortHostIP_NotSupported WSLCE2ETests::WSLCE2EContainerExecTests::WSLCE2E_Container_Exec_Detach WSLCE2ETests::WSLCE2EContainerLogsTests::WSLCE2E_Container_Logs_Follow WSLCE2ETests::WSLCE2EGlobalTests::WSLCE2E_Session_List_Verbose WSLCE2ETests::WSLCE2EImageBuildTests::WSLCE2E_Image_Build_NoCache_Success WSLCE2ETests::WSLCE2EImageDeleteTests::WSLCE2E_Image_DeleteNoPrune WSLCE2ETests::WSLCE2EImageListTests::WSLCE2E_Image_List_NoTrunc_ShowsFullImageId WSLCE2ETests::WSLCE2ESettingsTests::WSLCE2E_Settings_Reset_RewritesFile

Feng Wang committed Jun 11, 2026 at 13:43 UTC 843b4fc849bb8d7c41617cc17ddfdf77772618b9
10 files changed +491 -2
test/windows/Common.cpp
+42
@@ -1390,6 +1390,48 @@ WslConfigChange::~WslConfigChange()
1390 }
1391 }
1392
1393 +HostFileChange::HostFileChange(const std::filesystem::path& Path, const std::string& NewContent) : m_path(Path)
1394 +{
1395 + if (std::filesystem::exists(m_path))
1396 + {
1397 + std::ifstream file(m_path, std::ios::binary);
1398 + THROW_HR_IF(E_FAIL, !file.is_open());
1399 + std::stringstream buffer;
1400 + buffer << file.rdbuf();
1401 + m_originalContent = buffer.str();
1402 + }
1403 +
1404 + Update(NewContent);
1405 +}
1406 +
1407 +HostFileChange::~HostFileChange()
1408 +try
1409 +{
1410 + if (m_originalContent.has_value())
1411 + {
1412 + std::filesystem::create_directories(m_path.parent_path());
1413 + std::ofstream file(m_path, std::ios::binary | std::ios::trunc);
1414 + if (file.is_open())
1415 + {
1416 + file.write(m_originalContent->data(), static_cast<std::streamsize>(m_originalContent->size()));
1417 + }
1418 + }
1419 + else
1420 + {
1421 + std::filesystem::remove(m_path);
1422 + }
1423 +}
1424 +CATCH_LOG()
1425 +
1426 +void HostFileChange::Update(const std::string& NewContent) const
1427 +{
1428 + std::filesystem::create_directories(m_path.parent_path());
1429 + std::ofstream file(m_path, std::ios::binary | std::ios::trunc);
1430 + THROW_HR_IF(E_FAIL, !file.is_open());
1431 + file.write(NewContent.data(), static_cast<std::streamsize>(NewContent.size()));
1432 + THROW_HR_IF(E_FAIL, !file.good());
1433 +}
1434 +
1435 std::wstring ReadFileContent(const std::string& Path)
1436 {
1437 std::ifstream configRead(Path);
test/windows/Common.h
+21
@@ -188,6 +188,27 @@ private:
188 std::optional<std::wstring> m_originalContent;
189 };
190
191 +//
192 +// RAII wrapper for host file change.
193 +//
194 +
195 +class HostFileChange
196 +{
197 +public:
198 + HostFileChange(const std::filesystem::path& Path, const std::string& NewContent);
199 +
200 + ~HostFileChange();
201 +
202 + NON_COPYABLE(HostFileChange);
203 + NON_MOVABLE(HostFileChange);
204 +
205 + void Update(const std::string& NewContent) const;
206 +
207 +private:
208 + std::filesystem::path m_path;
209 + std::optional<std::string> m_originalContent;
210 +};
211 +
212 template <typename T>
213 class RegistryKeyChange
214 {
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
+217
@@ -827,6 +827,218 @@ class WSLCE2EContainerCreateTests
827 VerifyContainerIsNotListed(WslcContainerName);
828 }
829
830 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Cpus)
831 + {
832 + auto result = RunWslc(std::format(L"container create --name {} --cpus 0.5 {} true", WslcContainerName, DebianImage.NameAndTag()));
833 + result.Verify({.Stderr = L"", .ExitCode = 0});
834 +
835 + const auto inspect = InspectContainer(WslcContainerName);
836 + VERIFY_ARE_EQUAL(static_cast<int64_t>(500'000'000), inspect.HostConfig.NanoCpus);
837 + }
838 +
839 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Cpus_Invalid)
840 + {
841 + auto result = RunWslc(std::format(L"container create --cpus 0 --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
842 + result.Verify({.Stderr = L"Invalid cpus argument value: '0'. Expected a positive number of CPUs (e.g. 0.5, 1, 2)\r\n", .ExitCode = 1});
843 + EnsureContainerDoesNotExist(WslcContainerName);
844 + }
845 +
846 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Memory)
847 + {
848 + auto result =
849 + RunWslc(std::format(L"container create --name {} --memory 32M {} true", WslcContainerName, DebianImage.NameAndTag()));
850 + // stderr not asserted: some kernels emit a swap-limit warning when --memory is set.
851 + result.Verify({.ExitCode = 0});
852 +
853 + const auto inspect = InspectContainer(WslcContainerName);
854 + VERIFY_ARE_EQUAL(static_cast<int64_t>(32) * 1024 * 1024, inspect.HostConfig.Memory);
855 + }
856 +
857 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Memory_Invalid)
858 + {
859 + auto result =
860 + RunWslc(std::format(L"container create --memory invalid --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
861 + result.Verify({.Stderr = L"Invalid memory argument value: 'invalid'. Expected a memory size (e.g. 256M, 1G)\r\n", .ExitCode = 1});
862 + EnsureContainerDoesNotExist(WslcContainerName);
863 + }
864 +
865 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Ulimit)
866 + {
867 + auto result = RunWslc(std::format(
868 + L"container create --name {} --ulimit nofile=1024:2048 --ulimit nproc=512 {} true", WslcContainerName, DebianImage.NameAndTag()));
869 + result.Verify({.Stderr = L"", .ExitCode = 0});
870 +
871 + const auto inspect = InspectContainer(WslcContainerName);
872 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), inspect.HostConfig.Ulimits.size());
873 +
874 + std::map<std::string, std::pair<int64_t, int64_t>> byName;
875 + for (const auto& ul : inspect.HostConfig.Ulimits)
876 + {
877 + byName[ul.Name] = {ul.Soft, ul.Hard};
878 + }
879 +
880 + VERIFY_IS_TRUE(byName.contains("nofile"));
881 + VERIFY_ARE_EQUAL(static_cast<int64_t>(1024), byName["nofile"].first);
882 + VERIFY_ARE_EQUAL(static_cast<int64_t>(2048), byName["nofile"].second);
883 +
884 + VERIFY_IS_TRUE(byName.contains("nproc"));
885 + VERIFY_ARE_EQUAL(static_cast<int64_t>(512), byName["nproc"].first);
886 + VERIFY_ARE_EQUAL(static_cast<int64_t>(512), byName["nproc"].second);
887 + }
888 +
889 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Ulimit_Invalid)
890 + {
891 + auto result = RunWslc(std::format(L"container create --ulimit nofile --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
892 + result.Verify(
893 + {.Stderr = L"Invalid ulimit argument value: 'nofile'. Expected <name>=<soft>[:<hard>] (use -1 for unlimited)\r\n", .ExitCode = 1});
894 + EnsureContainerDoesNotExist(WslcContainerName);
895 + }
896 +
897 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Entrypoint)
898 + {
899 + auto result =
900 + RunWslc(std::format(L"container create --name {} --entrypoint /bin/whoami {}", WslcContainerName, DebianImage.NameAndTag()));
901 + result.Verify({.Stderr = L"", .ExitCode = 0});
902 +
903 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
904 + result.Verify({.Stdout = L"root\n", .Stderr = L"", .ExitCode = 0});
905 + }
906 +
907 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_EnvFile)
908 + {
909 + WriteTestFile(
910 + EnvTestFile1, {"WSLC_TEST_CREATE_ENV_FILE_A=create-env-file-a", "WSLC_TEST_CREATE_ENV_FILE_B=create-env-file-b"});
911 +
912 + auto result = RunWslc(std::format(
913 + L"container create --name {} --env-file {} {} env", WslcContainerName, EscapePath(EnvTestFile1.wstring()), DebianImage.NameAndTag()));
914 + result.Verify({.Stderr = L"", .ExitCode = 0});
915 +
916 + result = RunWslc(std::format(L"container start -a {}", WslcContainerName));
917 + result.Verify({.Stderr = L"", .ExitCode = 0});
918 +
919 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_CREATE_ENV_FILE_A=create-env-file-a"));
920 + VERIFY_IS_TRUE(result.StdoutContainsLine(L"WSLC_TEST_CREATE_ENV_FILE_B=create-env-file-b"));
921 + }
922 +
923 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_EnvFile_MissingFile)
924 + {
925 + auto result = RunWslc(std::format(
926 + L"container create --name {} --env-file ENV_FILE_NOT_FOUND {} env", WslcContainerName, DebianImage.NameAndTag()));
927 + result.Verify(
928 + {.Stderr = L"Environment file 'ENV_FILE_NOT_FOUND' cannot be opened for reading\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
929 + EnsureContainerDoesNotExist(WslcContainerName);
930 + }
931 +
932 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_EnvFile_InvalidContent)
933 + {
934 + WriteTestFile(EnvTestFile1, {"WSLC_TEST_ENV_VALID=ok", "BAD KEY=value"});
935 +
936 + auto result = RunWslc(std::format(
937 + L"container create --name {} --env-file {} {} env", WslcContainerName, EscapePath(EnvTestFile1.wstring()), DebianImage.NameAndTag()));
938 + result.Verify({.Stderr = L"Environment variable key 'BAD KEY' cannot contain whitespace\r\nError code: E_INVALIDARG\r\n", .ExitCode = 1});
939 + EnsureContainerDoesNotExist(WslcContainerName);
940 + }
941 +
942 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Publish_TCP)
943 + {
944 + // Port bindings only show up in inspect after start, so create then start before inspecting.
945 + auto result = RunWslc(std::format(
946 + L"container create --name {} -p {}:{} {} sleep 5", WslcContainerName, HostTestPort1, ContainerTestPort, DebianImage.NameAndTag()));
947 + result.Verify({.Stderr = L"", .ExitCode = 0});
948 +
949 + result = RunWslc(std::format(L"container start {}", WslcContainerName));
950 + result.Verify({.Stderr = L"", .ExitCode = 0});
951 +
952 + // Verify the port mapping is correct in the container inspect data
953 + const auto inspect = InspectContainer(WslcContainerName);
954 + const auto portKey = std::to_string(ContainerTestPort) + "/tcp";
955 + VERIFY_IS_TRUE(inspect.Ports.contains(portKey));
956 +
957 + const auto& bindings = inspect.Ports.at(portKey);
958 + VERIFY_ARE_EQUAL(1u, bindings.size());
959 + VERIFY_ARE_EQUAL(std::to_string(HostTestPort1), bindings[0].HostPort);
960 + VERIFY_ARE_EQUAL("127.0.0.1", bindings[0].HostIp);
961 + }
962 +
963 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Publish_MultipleMappings)
964 + {
965 + // Map two host ports to the same container port.
966 + auto result = RunWslc(std::format(
967 + L"container create --name {} -p {}:{} -p {}:{} {} sleep 5",
968 + WslcContainerName,
969 + HostTestPort1,
970 + ContainerTestPort,
971 + HostTestPort2,
972 + ContainerTestPort,
973 + DebianImage.NameAndTag()));
974 + result.Verify({.Stderr = L"", .ExitCode = 0});
975 +
976 + result = RunWslc(std::format(L"container start {}", WslcContainerName));
977 + result.Verify({.Stderr = L"", .ExitCode = 0});
978 +
979 + // Both host ports should be bound to the same container port.
980 + const auto inspect = InspectContainer(WslcContainerName);
981 + const auto portKey = std::to_string(ContainerTestPort) + "/tcp";
982 + VERIFY_IS_TRUE(inspect.Ports.contains(portKey));
983 +
984 + const auto& bindings = inspect.Ports.at(portKey);
985 + VERIFY_ARE_EQUAL(2u, bindings.size());
986 +
987 + bool foundHostPort1 = false;
988 + bool foundHostPort2 = false;
989 + for (const auto& binding : bindings)
990 + {
991 + if (binding.HostPort == std::to_string(HostTestPort1))
992 + {
993 + foundHostPort1 = true;
994 + }
995 + else if (binding.HostPort == std::to_string(HostTestPort2))
996 + {
997 + foundHostPort2 = true;
998 + }
999 + }
1000 + VERIFY_IS_TRUE(foundHostPort1);
1001 + VERIFY_IS_TRUE(foundHostPort2);
1002 + }
1003 +
1004 + // https://github.com/microsoft/WSL/issues/14433
1005 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_Publish_Ephemeral)
1006 + {
1007 + // -p <containerPort> (no host port) means the host picks a random port.
1008 + auto result = RunWslc(std::format(
1009 + L"container create --name {} -p {} {} sleep 5", WslcContainerName, ContainerTestPort, DebianImage.NameAndTag()));
1010 + result.Verify({.Stderr = L"", .ExitCode = 0});
1011 +
1012 + result = RunWslc(std::format(L"container start {}", WslcContainerName));
1013 + result.Verify({.Stderr = L"", .ExitCode = 0});
1014 +
1015 + // Inspect the container to verify a host port was allocated.
1016 + const auto inspect = InspectContainer(WslcContainerName);
1017 + const auto portKey = std::to_string(ContainerTestPort) + "/tcp";
1018 + VERIFY_IS_TRUE(inspect.Ports.contains(portKey));
1019 +
1020 + const auto& bindings = inspect.Ports.at(portKey);
1021 + VERIFY_ARE_EQUAL(1u, bindings.size());
1022 + VERIFY_IS_TRUE(std::stoi(bindings[0].HostPort) > 0);
1023 + }
1024 +
1025 + // https://github.com/microsoft/WSL/issues/14433
1026 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_PortUdp_NotSupported)
1027 + {
1028 + auto result = RunWslc(std::format(L"container create --name {} -p 80:80/udp {}", WslcContainerName, DebianImage.NameAndTag()));
1029 + result.Verify({.Stderr = L"Port mappings with specific host IPs or UDP protocol are not currently supported\r\nError code: ERROR_NOT_SUPPORTED\r\n", .ExitCode = 1});
1030 + EnsureContainerDoesNotExist(WslcContainerName);
1031 + }
1032 +
1033 + // https://github.com/microsoft/WSL/issues/14433
1034 + WSLC_TEST_METHOD(WSLCE2E_Container_Create_PortHostIP_NotSupported)
1035 + {
1036 + auto result =
1037 + RunWslc(std::format(L"container create --name {} -p 127.0.0.1:80:80 {}", WslcContainerName, DebianImage.NameAndTag()));
1038 + result.Verify({.Stderr = L"Port mappings with specific host IPs or UDP protocol are not currently supported\r\nError code: ERROR_NOT_SUPPORTED\r\n", .ExitCode = 1});
1039 + EnsureContainerDoesNotExist(WslcContainerName);
1040 + }
1041 +
1042 private:
1043 // Test container name
1044 const std::wstring WslcContainerName = L"wslc-test-container";
@@ -850,6 +1062,11 @@ private:
1062 const TestImage& DebianImage = DebianTestImage();
1063 const TestImage& InvalidImage = InvalidTestImage();
1064
1065 + // Test ports
1066 + const uint16_t ContainerTestPort = 8080;
1067 + const uint16_t HostTestPort1 = 1234;
1068 + const uint16_t HostTestPort2 = 1235;
1069 +
1070 // Test volume files
1071 std::filesystem::path VolumeTestFile1;
1072 std::filesystem::path VolumeTestFile2;
test/windows/wslc/e2e/WSLCE2EContainerExecTests.cpp
+18
@@ -411,6 +411,24 @@ class WSLCE2EContainerExecTests
411 result.Verify({.Stdout = L"/tmp\n", .Stderr = L"", .ExitCode = 0});
412 }
413
414 + WSLC_TEST_METHOD(WSLCE2E_Container_Exec_Detach)
415 + {
416 + auto result = RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
417 + result.Verify({.Stderr = L"", .ExitCode = 0});
418 +
419 + constexpr auto markerPath = L"/tmp/wslc-exec-detach-marker";
420 + result = RunWslc(std::format(L"container exec -d {} sh -c \"sleep 1 && echo wslc-detach-ok > {}\"", WslcContainerName, markerPath));
421 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
422 +
423 + VERIFY_NO_THROW(wsl::shared::retry::RetryWithTimeout<void>(
424 + [&]() {
425 + auto readResult = RunWslc(std::format(L"container exec {} cat {}", WslcContainerName, markerPath));
426 + readResult.Verify({.Stdout = L"wslc-detach-ok\n", .Stderr = L"", .ExitCode = 0});
427 + },
428 + std::chrono::milliseconds(200),
429 + std::chrono::seconds(10)));
430 + }
431 +
432 private:
433 const std::wstring WslcContainerName = L"wslc-test-container";
434 const TestImage& DebianImage = DebianTestImage();
test/windows/wslc/e2e/WSLCE2EContainerLogsTests.cpp
+24
@@ -168,6 +168,30 @@ class WSLCE2EContainerLogsTests
168 }
169 }
170
171 + WSLC_TEST_METHOD(WSLCE2E_Container_Logs_Follow)
172 + {
173 + // Sleep between the two lines so the second is not produced until after line 1 is read.
174 + constexpr int InterLineSleepSeconds = 2;
175 + auto result = RunWslc(std::format(
176 + L"container run -d --name {} {} sh -c \"echo follow-line-1; sleep {}; echo follow-line-2\"",
177 + WslcContainerName,
178 + DebianImage.NameAndTag(),
179 + InterLineSleepSeconds));
180 + result.Verify({.Stderr = L"", .ExitCode = 0});
181 +
182 + auto logsSession = RunWslcInteractive(std::format(L"container logs -f {}", WslcContainerName));
183 +
184 + // If --follow buffered, line 1 would arrive only after the container exited.
185 + logsSession.ExpectStdout("follow-line-1\n");
186 + VERIFY_IS_TRUE(logsSession.IsRunning(), L"`logs -f` should still be running mid-sleep after line 1");
187 +
188 + logsSession.ExpectStdout("follow-line-2\n");
189 +
190 + auto exitCode = logsSession.Wait(30000);
191 + VERIFY_ARE_EQUAL(0, exitCode);
192 + logsSession.VerifyNoErrors();
193 + }
194 +
195 private:
196 const std::wstring WslcContainerName = L"wslc-test-logs";
197 const TestImage& DebianImage = DebianTestImage();
test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp
+19
@@ -492,6 +492,25 @@ class WSLCE2EGlobalTests
492 }
493 }
494
495 + WSLC_TEST_METHOD(WSLCE2E_Session_List_Verbose)
496 + {
497 + auto result = RunWslc(L"container list");
498 + result.Verify({.Stderr = L"", .ExitCode = 0});
499 +
500 + auto verboseResult = RunWslc(L"system session list --verbose");
501 + verboseResult.Verify({.Stderr = L"", .ExitCode = 0});
502 + VERIFY_IS_TRUE(verboseResult.Stdout.has_value());
503 + VERIFY_IS_TRUE(
504 + verboseResult.Stdout->find(L"[wslc] Found ") != std::wstring::npos, L"--verbose should print a session summary line");
505 +
506 + auto plainResult = RunWslc(L"system session list");
507 + plainResult.Verify({.Stderr = L"", .ExitCode = 0});
508 + VERIFY_IS_TRUE(plainResult.Stdout.has_value());
509 + VERIFY_IS_TRUE(
510 + plainResult.Stdout->find(L"[wslc] Found ") == std::wstring::npos,
511 + L"plain list should not print a session summary line");
512 + }
513 +
514 private:
515 std::wstring GetHelpMessage() const
516 {
test/windows/wslc/e2e/WSLCE2EImageBuildTests.cpp
+42
@@ -241,6 +241,46 @@ class WSLCE2EImageBuildTests
241 .ExitCode = 1});
242 }
243
244 + WSLC_TEST_METHOD(WSLCE2E_Image_Build_NoCache_Success)
245 + {
246 + auto testRoot = std::filesystem::current_path() / L"wslc-e2e-build-no-cache";
247 + auto cleanup = SetupTestDirectory(testRoot);
248 +
249 + auto contextDir = testRoot / L"context";
250 + std::error_code ec;
251 + std::filesystem::create_directories(contextDir, ec);
252 + THROW_HR_IF(E_FAIL, ec.value() != 0 || !std::filesystem::exists(contextDir));
253 +
254 + // `RUN date +%N` produces a different output each invocation, so without caching the
255 + // resulting layer (and therefore the image id) changes every build.
256 + auto dockerfilePath = testRoot / L"Dockerfile";
257 + WriteTestFile(
258 + dockerfilePath,
259 + "FROM debian:latest\n"
260 + "RUN date +%N > /timestamp.txt\n");
261 +
262 + const auto buildCmd =
263 + std::format(L"build \"{}\" -f \"{}\" -t {}", contextDir.wstring(), dockerfilePath.wstring(), BuiltImageNoCache.NameAndTag());
264 +
265 + // Seed the cache.
266 + auto firstBuild = RunWslc(buildCmd);
267 + firstBuild.Verify({.Stderr = L"", .ExitCode = 0});
268 + const auto firstId = InspectImage(BuiltImageNoCache.NameAndTag()).Id;
269 + VERIFY_ARE_NOT_EQUAL(std::string{}, firstId);
270 +
271 + // A repeated build without --no-cache should hit the cache and produce the same id.
272 + auto cachedBuild = RunWslc(buildCmd);
273 + cachedBuild.Verify({.Stderr = L"", .ExitCode = 0});
274 + const auto cachedId = InspectImage(BuiltImageNoCache.NameAndTag()).Id;
275 + VERIFY_ARE_EQUAL(firstId, cachedId, L"Repeated build without --no-cache should reuse the cached layer");
276 +
277 + // --no-cache must re-run the non-deterministic step, producing a new id.
278 + auto noCacheBuild = RunWslc(buildCmd + L" --no-cache");
279 + noCacheBuild.Verify({.Stderr = L"", .ExitCode = 0});
280 + const auto noCacheId = InspectImage(BuiltImageNoCache.NameAndTag()).Id;
281 + VERIFY_ARE_NOT_EQUAL(firstId, noCacheId, L"--no-cache must rebuild the non-deterministic RUN step");
282 + }
283 +
284 private:
285 const TestImage BuiltImage{L"wslc-e2e-build-empty-context", L"latest", L""};
286 const TestImage BuiltImageTag1{L"wslc-e2e-build-args-tags", L"v1", L""};
@@ -249,6 +289,7 @@ private:
289 const TestImage BuiltImageTarget{L"wslc-e2e-build-target", L"latest", L""};
290 const TestImage BuiltImageDockerfile{L"wslc-e2e-build-dockerfile-ctx", L"latest", L""};
291 const TestImage BuiltImageContainerfile{L"wslc-e2e-build-containerfile-ctx", L"latest", L""};
292 + const TestImage BuiltImageNoCache{L"wslc-e2e-build-no-cache", L"latest", L""};
293
294 void BuildFromContextFile(const std::wstring& fileName, const TestImage& image)
295 {
@@ -275,6 +316,7 @@ private:
316 EnsureImageIsDeleted(BuiltImageTarget);
317 EnsureImageIsDeleted(BuiltImageDockerfile);
318 EnsureImageIsDeleted(BuiltImageContainerfile);
319 + EnsureImageIsDeleted(BuiltImageNoCache);
320 }
321
322 static auto SetupTestDirectory(const std::filesystem::path& testRoot)
test/windows/wslc/e2e/WSLCE2EImageDeleteTests.cpp
+24 -2
@@ -28,6 +28,7 @@ class WSLCE2EImageDeleteTests
28 EnsureContainerDoesNotExist(WslcContainerName);
29 EnsureImageIsDeleted(DebianImage);
30 EnsureImageIsDeleted(AlpineImage);
31 + EnsureImageIsDeleted(NoPruneTaggedImage);
32 return true;
33 }
34
@@ -36,6 +37,7 @@ class WSLCE2EImageDeleteTests
37 EnsureContainerDoesNotExist(WslcContainerName);
38 EnsureImageIsDeleted(DebianImage);
39 EnsureImageIsDeleted(AlpineImage);
40 + EnsureImageIsDeleted(NoPruneTaggedImage);
41 return true;
42 }
43
@@ -119,8 +121,27 @@ class WSLCE2EImageDeleteTests
121
122 WSLC_TEST_METHOD(WSLCE2E_Image_DeleteNoPrune)
123 {
122 - // TODO: Implement once 'image tag' is implemented
123 - SKIP_TEST_NOT_IMPL();
124 + // Tag debian a second time, then remove via the alias with --no-prune.
125 + // The alias must disappear while the original tag stays resolvable.
126 + EnsureImageIsLoaded(DebianImage);
127 + EnsureImageIsDeleted(NoPruneTaggedImage);
128 +
129 + auto tagResult = RunWslc(std::format(L"image tag {} {}", DebianImage.NameAndTag(), NoPruneTaggedImage.NameAndTag()));
130 + tagResult.Verify({.Stderr = L"", .ExitCode = 0});
131 +
132 + auto removeResult = RunWslc(std::format(L"image delete --no-prune {}", NoPruneTaggedImage.NameAndTag()));
133 + removeResult.Verify({.Stderr = L"", .ExitCode = 0});
134 +
135 + VerifyImageIsListed(DebianImage);
136 +
137 + auto listAfter = RunWslc(L"image list -q");
138 + listAfter.Verify({.Stderr = L"", .ExitCode = 0});
139 + for (const auto& line : listAfter.GetStdoutLines())
140 + {
141 + VERIFY_IS_TRUE(
142 + line.find(NoPruneTaggedImage.NameAndTag()) == std::wstring::npos,
143 + L"Secondary tag should have been removed by `image delete --no-prune`");
144 + }
145 }
146
147 private:
@@ -128,6 +149,7 @@ private:
149 const TestImage& DebianImage = DebianTestImage();
150 const TestImage& AlpineImage = AlpineTestImage();
151 const TestImage& InvalidImage = InvalidTestImage();
152 + const TestImage NoPruneTaggedImage{L"wslc-test-noprune", L"alias", L""};
153
154 std::wstring GetHelpMessage() const
155 {
test/windows/wslc/e2e/WSLCE2EImageListTests.cpp
+35
@@ -242,6 +242,41 @@ class WSLCE2EImageListTests
242 VERIFY_IS_TRUE(foundDebian, L"Expected debian image when combining reference and dangling filters");
243 }
244
245 + WSLC_TEST_METHOD(WSLCE2E_Image_List_NoTrunc_ShowsFullImageId)
246 + {
247 + // Pull the full image id from JSON output (always untruncated).
248 + auto jsonResult = RunWslc(L"image list --format json");
249 + jsonResult.Verify({.Stderr = L"", .ExitCode = 0});
250 + const auto images = wsl::shared::FromJson<std::vector<ImageInformation>>(jsonResult.Stdout.value().c_str());
251 +
252 + std::string fullDebianId;
253 + for (const auto& image : images)
254 + {
255 + if (image.Repository == wsl::shared::string::WideToMultiByte(DebianImage.Name))
256 + {
257 + fullDebianId = image.Id;
258 + break;
259 + }
260 + }
261 + VERIFY_ARE_NOT_EQUAL(std::string{}, fullDebianId, L"Debian image was not present in `image list --format json` output");
262 +
263 + fullDebianId = GetHashId(fullDebianId, true);
264 + VERIFY_IS_GREATER_THAN(fullDebianId.size(), 12u);
265 + const auto fullDebianIdW = wsl::shared::string::MultiByteToWide(fullDebianId);
266 + const auto truncatedDebianIdW = fullDebianIdW.substr(0, 12);
267 +
268 + // Default table truncates IMAGE ID to 12 chars.
269 + auto truncResult = RunWslc(L"image list");
270 + truncResult.Verify({.Stderr = L"", .ExitCode = 0});
271 + VERIFY_IS_TRUE(truncResult.StdoutContainsSubstring(truncatedDebianIdW));
272 + VERIFY_IS_FALSE(truncResult.StdoutContainsSubstring(fullDebianIdW));
273 +
274 + // --no-trunc must show the full id.
275 + auto noTruncResult = RunWslc(L"image list --no-trunc");
276 + noTruncResult.Verify({.Stderr = L"", .ExitCode = 0});
277 + VERIFY_IS_TRUE(noTruncResult.StdoutContainsSubstring(fullDebianIdW));
278 + }
279 +
280 private:
281 const TestImage& DebianImage = DebianTestImage();
282 const TestImage& AlpineImage = AlpineTestImage();
test/windows/wslc/e2e/WSLCE2ESettingsTests.cpp new
+49
@@ -0,0 +1,49 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2ESettingsTests.cpp
8 +
9 +Abstract:
10 +
11 + End-to-end tests for the wslc `settings` command tree. Tests mutate the
12 + user's real settings file at %LOCALAPPDATA%\wslc\settings.yaml; HostFileChange
13 + backs the file up on construction and restores it on scope exit.
14 +
15 +--*/
16 +
17 +#include "precomp.h"
18 +#include "windows/Common.h"
19 +#include "WSLCExecutor.h"
20 +#include "WSLCE2EHelpers.h"
21 +
22 +namespace WSLCE2ETests {
23 +using namespace wsl::shared;
24 +
25 +class WSLCE2ESettingsTests
26 +{
27 + WSLC_TEST_CLASS(WSLCE2ESettingsTests)
28 +
29 + WSLC_TEST_METHOD(WSLCE2E_Settings_Reset_RewritesFile)
30 + {
31 + const auto settingsPath = GetSettingsPath();
32 +
33 + HostFileChange settings(settingsPath, "session:\n cpuCount: 2\n");
34 +
35 + auto result = RunWslc(L"settings reset");
36 + result.Verify({.Stderr = L"", .ExitCode = 0});
37 +
38 + const auto content = ReadFileContent(settingsPath.wstring());
39 + VERIFY_ARE_EQUAL(std::wstring::npos, content.find(L"cpuCount: 2"));
40 + }
41 +
42 +private:
43 + static std::filesystem::path GetSettingsPath()
44 + {
45 + return wsl::windows::common::filesystem::GetLocalAppDataPath(nullptr) / L"wslc" / L"settings.yaml";
46 + }
47 +};
48 +
49 +} // namespace WSLCE2ETests