| 1 | /*++ |
| 2 | |
| 3 | Copyright (c) Microsoft. All rights reserved. |
| 4 | |
| 5 | Module Name: |
| 6 | |
| 7 | SimpleTests.cpp |
| 8 | |
| 9 | Abstract: |
| 10 | |
| 11 | This file contains smoke tests for WSL. |
| 12 | |
| 13 | --*/ |
| 14 | |
| 15 | #include "precomp.h" |
| 16 | #include "Common.h" |
| 17 | |
| 18 | namespace SimpleTests { |
| 19 | class SimpleTests |
| 20 | { |
| 21 | WSL_TEST_CLASS(SimpleTests) |
| 22 | |
| 23 | // Initialize the tests |
| 24 | TEST_CLASS_SETUP(TestClassSetup) |
| 25 | { |
| 26 | VERIFY_ARE_EQUAL(LxsstuInitialize(FALSE), TRUE); |
| 27 | |
| 28 | return true; |
| 29 | } |
| 30 | |
| 31 | TEST_CLASS_CLEANUP(TestClassCleanup) |
| 32 | { |
| 33 | LxsstuUninitialize(FALSE); |
| 34 | return true; |
| 35 | } |
| 36 | |
| 37 | TEST_METHOD(EchoTest) |
| 38 | { |
| 39 | const std::wstring echoExpected = L"LOW!\n"; |
| 40 | auto [output, __] = LxsstuLaunchWslAndCaptureOutput(L"echo LOW!"); |
| 41 | VERIFY_ARE_EQUAL(output, echoExpected); |
| 42 | } |
| 43 | |
| 44 | TEST_METHOD(WhoamiTest) |
| 45 | { |
| 46 | const std::wstring whoamiExpected = L"root\n"; |
| 47 | auto [output, __] = LxsstuLaunchWslAndCaptureOutput(L"-u root whoami"); |
| 48 | VERIFY_ARE_EQUAL(output, whoamiExpected); |
| 49 | } |
| 50 | |
| 51 | TEST_METHOD(ChangeDirTest) |
| 52 | { |
| 53 | const std::wstring cdExpected = L"/root\n"; |
| 54 | auto [output, __] = LxsstuLaunchWslAndCaptureOutput(L"--cd ~ --user root pwd"); |
| 55 | VERIFY_ARE_EQUAL(output, cdExpected); |
| 56 | } |
| 57 | |
| 58 | TEST_METHOD(Daemonize) |
| 59 | { |
| 60 | WslConfigChange config(LxssGenerateTestConfig({.vmIdleTimeout = 0})); |
| 61 | WslShutdown(); |
| 62 | VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"-- eval \"touch /dev/shm/backgroundmagic; daemonize $(which sleep) 30\""), (DWORD)0); |
| 63 | |
| 64 | std::this_thread::sleep_for(std::chrono::seconds(20)); |
| 65 | |
| 66 | VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"-- ls /dev/shm/backgroundmagic"), (DWORD)0); |
| 67 | } |
| 68 | |
| 69 | static void VerifySparse(wchar_t const* path, bool sparse) |
| 70 | { |
| 71 | DWORD attributes = ::GetFileAttributesW(path); |
| 72 | VERIFY_IS_FALSE(attributes == INVALID_FILE_ATTRIBUTES); |
| 73 | VERIFY_IS_TRUE(WI_IsFlagSet(attributes, FILE_ATTRIBUTE_SPARSE_FILE) == sparse); |
| 74 | } |
| 75 | |
| 76 | WSL2_TEST_METHOD(CheckSparse) |
| 77 | { |
| 78 | WslConfigChange config(LxssGenerateTestConfig({.sparse = true})); |
| 79 | |
| 80 | std::filesystem::path tar = std::tmpnam(nullptr); |
| 81 | tar += ".tar"; |
| 82 | LogInfo("tar %ls", tar.c_str()); |
| 83 | auto cleanupTar = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] { |
| 84 | try |
| 85 | { |
| 86 | std::filesystem::remove(tar); |
| 87 | } |
| 88 | CATCH_LOG() |
| 89 | }); |
| 90 | |
| 91 | const std::wstring tempDistro = L"temp_distro"; |
| 92 | const std::filesystem::path vhdDir = std::tmpnam(nullptr); |
| 93 | LogInfo("vhdDir %ls", vhdDir.c_str()); |
| 94 | VERIFY_IS_TRUE(std::filesystem::create_directory(vhdDir)); |
| 95 | auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] { |
| 96 | try |
| 97 | { |
| 98 | LxsstuLaunchWsl(std::format(L"{} {}", WSL_UNREGISTER_ARG, tempDistro).c_str()); |
| 99 | std::filesystem::remove_all(vhdDir); |
| 100 | } |
| 101 | CATCH_LOG() |
| 102 | }); |
| 103 | |
| 104 | VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"{} {} {}", WSL_EXPORT_ARG, LXSS_DISTRO_NAME_TEST, tar.wstring()).c_str()), (DWORD)0); |
| 105 | LxsstuLaunchWsl(std::format(L"{} {}", WSL_UNREGISTER_ARG, tempDistro).c_str()); |
| 106 | ValidateOutput( |
| 107 | std::format(L"{} {} {} {}", WSL_IMPORT_ARG, tempDistro, vhdDir.wstring(), tar.wstring()).c_str(), |
| 108 | L"The operation completed successfully. \r\n", |
| 109 | L"wsl: Sparse VHD support is currently disabled due to potential data corruption.\r\n" |
| 110 | L"To force a distribution to use a sparse VHD, please run:\r\n" |
| 111 | L"wsl.exe --manage <DistributionName> --set-sparse true --allow-unsafe\r\n", |
| 112 | 0); |
| 113 | |
| 114 | std::filesystem::path vhdPath = vhdDir / LXSS_VM_MODE_VHD_NAME; |
| 115 | VerifySparse(vhdPath.c_str(), false); |
| 116 | |
| 117 | WslShutdown(); |
| 118 | |
| 119 | // Setting a distro VHD to sparse requires the allow unsafe flag. |
| 120 | ValidateOutput( |
| 121 | std::format(L"{} {} {} {}", WSL_MANAGE_ARG, tempDistro, WSL_MANAGE_ARG_SET_SPARSE_OPTION_LONG, L"true").c_str(), |
| 122 | FormatErrorMessage( |
| 123 | L"Sparse VHD support is currently disabled due to potential data corruption.\r\n" |
| 124 | L"To force a distribution to use a sparse VHD, please run:\r\n" |
| 125 | L"wsl.exe --manage <DistributionName> --set-sparse true --allow-unsafe", |
| 126 | L"Wsl/Service/E_INVALIDARG"), |
| 127 | L"", |
| 128 | -1); |
| 129 | |
| 130 | VerifySparse(vhdPath.c_str(), false); |
| 131 | |
| 132 | ValidateOutput( |
| 133 | std::format(L"{} {} {} {} {}", WSL_MANAGE_ARG, tempDistro, WSL_MANAGE_ARG_SET_SPARSE_OPTION_LONG, L"true", WSL_MANAGE_ARG_ALLOW_UNSAFE) |
| 134 | .c_str(), |
| 135 | L"The operation completed successfully. \r\n", |
| 136 | L"", |
| 137 | 0); |
| 138 | |
| 139 | VerifySparse(vhdPath.c_str(), true); |
| 140 | |
| 141 | // Disabling sparse on a VHD does not require the allow unsafe flag. |
| 142 | ValidateOutput( |
| 143 | std::format(L"{} {} {} {}", WSL_MANAGE_ARG, tempDistro, WSL_MANAGE_ARG_SET_SPARSE_OPTION_LONG, L"false").c_str(), |
| 144 | L"The operation completed successfully. \r\n", |
| 145 | L"", |
| 146 | 0); |
| 147 | |
| 148 | VerifySparse(vhdPath.c_str(), false); |
| 149 | } |
| 150 | |
| 151 | TEST_METHOD(StringHelpers) |
| 152 | { |
| 153 | std::string string1 = "aaaBBB"; |
| 154 | std::string string2 = "aaabbb"; |
| 155 | VERIFY_IS_TRUE(wsl::shared::string::IsEqual(string1, string2, true)); |
| 156 | VERIFY_IS_FALSE(wsl::shared::string::IsEqual(string1, string2, false)); |
| 157 | VERIFY_IS_TRUE(wsl::shared::string::IsEqual(string1.c_str(), string2.c_str(), true)); |
| 158 | VERIFY_IS_FALSE(wsl::shared::string::IsEqual(string1.c_str(), string2.c_str(), false)); |
| 159 | VERIFY_IS_TRUE(wsl::shared::string::StartsWith(string1, string2.substr(0, 3), true)); |
| 160 | VERIFY_IS_FALSE(wsl::shared::string::StartsWith(string1, string2, false)); |
| 161 | |
| 162 | std::wstring wstring1 = L"aaaBBB"; |
| 163 | std::wstring wstring2 = L"aaabbb"; |
| 164 | VERIFY_IS_TRUE(wsl::shared::string::IsEqual(wstring1, wstring2, true)); |
| 165 | VERIFY_IS_FALSE(wsl::shared::string::IsEqual(wstring1, wstring2, false)); |
| 166 | VERIFY_IS_TRUE(wsl::shared::string::IsEqual(wstring1.c_str(), wstring2.c_str(), true)); |
| 167 | VERIFY_IS_FALSE(wsl::shared::string::IsEqual(wstring1.c_str(), wstring2.c_str(), false)); |
| 168 | VERIFY_IS_TRUE(wsl::shared::string::StartsWith(wstring1, wstring2.substr(0, 3), true)); |
| 169 | VERIFY_IS_FALSE(wsl::shared::string::StartsWith(wstring1, wstring2, false)); |
| 170 | |
| 171 | // Test wsl::shared::string::ParseBool |
| 172 | std::vector<std::pair<LPCSTR, std::optional<bool>>> boolTests = { |
| 173 | {"1", true}, |
| 174 | {"0", false}, |
| 175 | {"true", true}, |
| 176 | {"false", false}, |
| 177 | {"True", true}, |
| 178 | {"False", false}, |
| 179 | {"t", std::nullopt}, |
| 180 | {"f", std::nullopt}, |
| 181 | {"T", std::nullopt}, |
| 182 | {"F", std::nullopt}, |
| 183 | {nullptr, std::nullopt}, |
| 184 | {"", std::nullopt}, |
| 185 | {"2", std::nullopt}, |
| 186 | {"true_", std::nullopt}, |
| 187 | {"false_", std::nullopt}, |
| 188 | }; |
| 189 | |
| 190 | for (const auto& [input, expected] : boolTests) |
| 191 | { |
| 192 | VERIFY_ARE_EQUAL(expected, wsl::shared::string::ParseBool(input)); |
| 193 | |
| 194 | std::wstring wideString = wsl::shared::string::MultiByteToWide(input); |
| 195 | VERIFY_ARE_EQUAL(expected, wsl::shared::string::ParseBool(wideString.c_str())); |
| 196 | } |
| 197 | |
| 198 | // With AllowExtendedForms the single-letter "t"/"f" forms (case-insensitive) are also |
| 199 | // recognized, matching Go's strconv.ParseBool (and therefore the Docker CLI). Every |
| 200 | // form accepted by default must still parse identically in extended mode. |
| 201 | std::vector<std::pair<LPCSTR, std::optional<bool>>> extendedBoolTests = { |
| 202 | {"1", true}, |
| 203 | {"0", false}, |
| 204 | {"true", true}, |
| 205 | {"false", false}, |
| 206 | {"True", true}, |
| 207 | {"False", false}, |
| 208 | {"t", true}, |
| 209 | {"T", true}, |
| 210 | {"f", false}, |
| 211 | {"F", false}, |
| 212 | {nullptr, std::nullopt}, |
| 213 | {"", std::nullopt}, |
| 214 | {"2", std::nullopt}, |
| 215 | {"tr", std::nullopt}, |
| 216 | {"true_", std::nullopt}, |
| 217 | {"false_", std::nullopt}, |
| 218 | }; |
| 219 | |
| 220 | for (const auto& [input, expected] : extendedBoolTests) |
| 221 | { |
| 222 | VERIFY_ARE_EQUAL(expected, wsl::shared::string::ParseBool(input, true)); |
| 223 | |
| 224 | std::wstring wideString = wsl::shared::string::MultiByteToWide(input); |
| 225 | VERIFY_ARE_EQUAL(expected, wsl::shared::string::ParseBool(wideString.c_str(), true)); |
| 226 | } |
| 227 | |
| 228 | // Test wsl::shared::string GUID helpers |
| 229 | const GUID guid = {0x1234567a, 0x1234, 0x5678, {0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78}}; |
| 230 | const std::string guidString = "{1234567a-1234-5678-1234-567812345678}"; |
| 231 | const std::string guidStringNoBraces = "1234567a-1234-5678-1234-567812345678"; |
| 232 | const std::vector<std::pair<LPCSTR, std::optional<GUID>>> guidTestCases{ |
| 233 | {guidString.c_str(), guid}, |
| 234 | {guidStringNoBraces.c_str(), guid}, |
| 235 | {nullptr, std::nullopt}, |
| 236 | {"", std::nullopt}, |
| 237 | {"foo", std::nullopt}, |
| 238 | {"1234567G-1234-5678-1234-5678123456789", std::nullopt}, |
| 239 | {"{1234567a-1234-5678-1234-567812345678", std::nullopt}, |
| 240 | {"{1234567aB-1234-5678-1234-567812345678}", std::nullopt}}; |
| 241 | |
| 242 | for (const auto& [input, expected] : guidTestCases) |
| 243 | { |
| 244 | VERIFY_ARE_EQUAL(expected, wsl::shared::string::ToGuid(input)); |
| 245 | const auto wideInput = wsl::shared::string::MultiByteToWide(input); |
| 246 | VERIFY_ARE_EQUAL(expected, wsl::shared::string::ToGuid(wideInput)); |
| 247 | } |
| 248 | |
| 249 | VERIFY_ARE_EQUAL(guidString, wsl::shared::string::GuidToString<char>(guid)); |
| 250 | VERIFY_ARE_EQUAL(guidString, wsl::shared::string::GuidToString<char>(guid, wsl::shared::string::GuidToStringFlags::AddBraces)); |
| 251 | VERIFY_ARE_EQUAL(guidStringNoBraces, wsl::shared::string::GuidToString<char>(guid, wsl::shared::string::GuidToStringFlags::None)); |
| 252 | |
| 253 | auto upperCaseGuidString = guidStringNoBraces; |
| 254 | std::transform(upperCaseGuidString.begin(), upperCaseGuidString.end(), upperCaseGuidString.begin(), toupper); |
| 255 | VERIFY_ARE_EQUAL(upperCaseGuidString, wsl::shared::string::GuidToString<char>(guid, wsl::shared::string::GuidToStringFlags::Uppercase)); |
| 256 | |
| 257 | const auto wideGuidString = wsl::shared::string::MultiByteToWide(guidString); |
| 258 | VERIFY_ARE_EQUAL(wideGuidString, wsl::shared::string::GuidToString<wchar_t>(guid)); |
| 259 | |
| 260 | VERIFY_ARE_EQUAL(wideGuidString, wsl::shared::string::GuidToString<wchar_t>(guid, wsl::shared::string::GuidToStringFlags::AddBraces)); |
| 261 | const auto wideGuidStringNoBraces = wsl::shared::string::MultiByteToWide(guidStringNoBraces); |
| 262 | VERIFY_ARE_EQUAL(wideGuidStringNoBraces, wsl::shared::string::GuidToString<wchar_t>(guid, wsl::shared::string::GuidToStringFlags::None)); |
| 263 | |
| 264 | auto upperCaseGuidStringWide = wideGuidStringNoBraces; |
| 265 | std::transform(upperCaseGuidStringWide.begin(), upperCaseGuidStringWide.end(), upperCaseGuidStringWide.begin(), toupper); |
| 266 | VERIFY_ARE_EQUAL(upperCaseGuidStringWide, wsl::shared::string::GuidToString<wchar_t>(guid, wsl::shared::string::GuidToStringFlags::Uppercase)); |
| 267 | |
| 268 | VERIFY_ARE_EQUAL(wsl::shared::string::Trim(std::string{" \tvalue\r\n"}), std::string{"value"}); |
| 269 | VERIFY_ARE_EQUAL(wsl::shared::string::Trim(std::string{" \t\r\n"}), std::string{}); |
| 270 | VERIFY_ARE_EQUAL(wsl::shared::string::Trim(std::wstring{L" \tvalue\r\n"}), std::wstring{L"value"}); |
| 271 | |
| 272 | const std::vector<std::pair<std::string, std::string>> shellStrings{ |
| 273 | {"", ""}, |
| 274 | {"plain", "plain"}, |
| 275 | {"\"\"", ""}, |
| 276 | {"''", ""}, |
| 277 | {"\"double quoted\"", "double quoted"}, |
| 278 | {"'single quoted'", "single quoted"}, |
| 279 | {"one' two'\" three\"", "one two three"}, |
| 280 | {"escaped\\ value", "escaped value"}, |
| 281 | {"escaped\\q", "escapedq"}, |
| 282 | {"escaped\\'quote", "escaped'quote"}, |
| 283 | {"abc\\\nedf", "abcedf"}, |
| 284 | {"\"abc\\\nedf\"", "abcedf"}, |
| 285 | {"'abc\\\nedf'", "abc\\\nedf"}, |
| 286 | {"\"escaped \\\"quote\\\" and \\\\ slash\"", "escaped \"quote\" and \\ slash"}, |
| 287 | {"\"escaped \\$dollar and \\`backtick\"", "escaped $dollar and `backtick"}, |
| 288 | {"\"literal \\q\"", "literal \\q"}, |
| 289 | {"'literal \\ value'", "literal \\ value"}, |
| 290 | {"unterminated'", "unterminated'"}, |
| 291 | {"\"unterminated", "\"unterminated"}, |
| 292 | {"trailing\\", "trailing\\"}, |
| 293 | {"\"trailing\\", "\"trailing\\"}}; |
| 294 | |
| 295 | for (const auto& [input, expected] : shellStrings) |
| 296 | { |
| 297 | VERIFY_ARE_EQUAL(wsl::shared::string::UnescapeShell(input), expected); |
| 298 | VERIFY_ARE_EQUAL( |
| 299 | wsl::shared::string::UnescapeShell(wsl::shared::string::MultiByteToWide(input)), |
| 300 | wsl::shared::string::MultiByteToWide(expected)); |
| 301 | } |
| 302 | } |
| 303 | |
| 304 | TEST_METHOD(WindowsPathWithSpaces) |
| 305 | { |
| 306 | wil::unique_environstrings_ptr originalPath; |
| 307 | const DWORD pathLength = GetEnvironmentVariableW(L"PATH", nullptr, 0); |
| 308 | if (pathLength > 0) |
| 309 | { |
| 310 | originalPath.reset(static_cast<PWSTR>(HeapAlloc(GetProcessHeap(), 0, pathLength * sizeof(wchar_t)))); |
| 311 | THROW_LAST_ERROR_IF_NULL(originalPath.get()); |
| 312 | THROW_LAST_ERROR_IF(GetEnvironmentVariableW(L"PATH", originalPath.get(), pathLength) == 0); |
| 313 | } |
| 314 | |
| 315 | auto cleanup = wil::scope_exit([&]() { |
| 316 | if (originalPath) |
| 317 | { |
| 318 | THROW_LAST_ERROR_IF(!SetEnvironmentVariableW(L"PATH", originalPath.get())); |
| 319 | } |
| 320 | }); |
| 321 | |
| 322 | const wchar_t* testPath = |
| 323 | L"C:\\Program Files\\Git\\cmd;" |
| 324 | L"C:\\Program Files\\PowerShell\\7;" |
| 325 | L"C:\\Program Files (x86)\\Common Files;" |
| 326 | L"C:\\Users\\Test User\\AppData\\Local\\Programs\\Microsoft VS Code\\bin"; |
| 327 | |
| 328 | THROW_LAST_ERROR_IF(!SetEnvironmentVariableW(L"PATH", testPath)); |
| 329 | |
| 330 | auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"echo $PATH"); |
| 331 | |
| 332 | VERIFY_IS_TRUE(output.find(L"/mnt/c/Program Files/Git/cmd") != std::wstring::npos); |
| 333 | VERIFY_IS_TRUE(output.find(L"/mnt/c/Program Files/PowerShell/7") != std::wstring::npos); |
| 334 | VERIFY_IS_TRUE(output.find(L"/mnt/c/Program Files (x86)/Common Files") != std::wstring::npos); |
| 335 | VERIFY_IS_TRUE(output.find(L"/mnt/c/Users/Test User/AppData/Local/Programs/Microsoft VS Code/bin") != std::wstring::npos); |
| 336 | } |
| 337 | }; |
| 338 | } // namespace SimpleTests |