diff --git a/src/shared/inc/CommandLine.h b/src/shared/inc/CommandLine.h index da4e2056e..eb5397694 100644 --- a/src/shared/inc/CommandLine.h +++ b/src/shared/inc/CommandLine.h @@ -307,8 +307,8 @@ class ArgumentParser public: #ifdef WIN32 - ArgumentParser(const std::wstring& CommandLine, LPCWSTR Name, int StartIndex = 1, bool ignoreUnknownArgs = false) : - m_startIndex(StartIndex), m_name(Name), m_ignoreUnknownArgs(ignoreUnknownArgs) + ArgumentParser(const std::wstring& CommandLine, LPCWSTR Name, int StartIndex = 1, bool IgnoreUnknownArgs = false) : + m_startIndex(StartIndex), m_name(Name), m_ignoreUnknownArgs(IgnoreUnknownArgs) { m_argv.reset(CommandLineToArgvW(std::wstring(CommandLine).c_str(), &m_argc)); THROW_LAST_ERROR_IF(!m_argv); @@ -316,8 +316,8 @@ class ArgumentParser #else - ArgumentParser(int argc, const char* const* argv, bool ignoreUnknownArgs = false) : - m_argc(argc), m_argv(argv), m_startIndex(1), m_ignoreUnknownArgs(ignoreUnknownArgs) + ArgumentParser(int argc, const char* const* argv, bool IgnoreUnknownArgs = false) : + m_argc(argc), m_argv(argv), m_startIndex(1), m_ignoreUnknownArgs(IgnoreUnknownArgs) { } diff --git a/src/windows/wsladiag/wsladiag.cpp b/src/windows/wsladiag/wsladiag.cpp index dd24d06b2..350f143ca 100644 --- a/src/windows/wsladiag/wsladiag.cpp +++ b/src/windows/wsladiag/wsladiag.cpp @@ -8,7 +8,8 @@ Module Name: Abstract: - Entry point for the wsladiag tool. Provides diagnostic commands for WSLA sessions. + Entry point for the wsladiag tool, performs WSL runtime initialization and parses list/shell/help. + --*/ @@ -21,6 +22,7 @@ Module Name: #include "ExecutionContext.h" #include #include +#include using namespace wsl::shared; namespace wslutil = wsl::windows::common::wslutil; @@ -36,7 +38,7 @@ static int ReportError(const std::wstring& context, HRESULT hr) return 1; } -// Handler for `wsladiag shell ` command. +// Handler for `wsladiag shell [--verbose]` command - launches TTY-backed interactive shell. static int RunShellCommand(std::wstring_view commandLine) { std::wstring sessionName; diff --git a/test/windows/CMakeLists.txt b/test/windows/CMakeLists.txt index c700d66f4..1abe54e68 100644 --- a/test/windows/CMakeLists.txt +++ b/test/windows/CMakeLists.txt @@ -9,7 +9,8 @@ set(SOURCES PluginTests.cpp PolicyTests.cpp InstallerTests.cpp - WSLATests.cpp) + WSLATests.cpp + WsladiagTests.cpp) set(HEADERS Common.h diff --git a/test/windows/WSLATests.cpp b/test/windows/WSLATests.cpp index 2988b5d05..9cd91a1af 100644 --- a/test/windows/WSLATests.cpp +++ b/test/windows/WSLATests.cpp @@ -149,7 +149,7 @@ class WSLATests if (result.Code != expectedResult) { LogError( - "Comman didn't return expected code (%i). ExitCode: %i, Stdout: '%hs', Stderr: '%hs'", + "Command didn't return expected code (%i). ExitCode: %i, Stdout: '%hs', Stderr: '%hs'", expectedResult, result.Code, result.Output[1].c_str(), diff --git a/test/windows/WsladiagTests.cpp b/test/windows/WsladiagTests.cpp new file mode 100644 index 000000000..d1d989d27 --- /dev/null +++ b/test/windows/WsladiagTests.cpp @@ -0,0 +1,179 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WsladiagTests.cpp + +Abstract: + + This file contains smoke tests for wsladiag. + +--*/ + +#include "precomp.h" +#include "Common.h" +#include "Localization.h" +#include + +namespace WsladiagTests { +class WsladiagTests +{ + WSL_TEST_CLASS(WsladiagTests) + + // Use localized usage text at runtime + static std::wstring GetUsageText() + { + return std::wstring(wsl::shared::Localization::MessageWsladiagUsage()); + } + + // Test that wsladiag list command shows either sessions or "no sessions" message + TEST_METHOD(List_ShowsSessionsOrNoSessions) + { + auto [out, err, code] = RunWsladiag(L"list"); + VERIFY_ARE_EQUAL(0, code); + VERIFY_ARE_EQUAL(L"", NormalizeForCompare(err)); + + ValidateListOutput(out); + } + + // Test that wsladiag --help shows usage information + TEST_METHOD(Help_ShowsUsage) + { + ValidateWsladiagOutput(L"--help", 0, L"", GetUsageText()); + } + + // Test that wsladiag with no arguments shows usage information + TEST_METHOD(EmptyCommand_ShowsUsage) + { + ValidateWsladiagOutput(L"", 0, L"", GetUsageText()); + } + + // Test that -h and --help flags produce identical output + TEST_METHOD(Help_ShortAndLongFlags_Match) + { + auto [outH, errH, codeH] = RunWsladiag(L"-h"); + auto [outLong, errLong, codeLong] = RunWsladiag(L"--help"); + + VERIFY_ARE_EQUAL(0, codeH); + VERIFY_ARE_EQUAL(0, codeLong); + + VERIFY_ARE_EQUAL(L"", outH); + VERIFY_ARE_EQUAL(L"", outLong); + + VERIFY_ARE_EQUAL(NormalizeForCompare(errH), NormalizeForCompare(errLong)); + ValidateUsage(errH); + } + + // Test that unknown commands show error message and usage + TEST_METHOD(UnknownCommand_ShowsError) + { + const std::wstring verb = L"blah"; + const std::wstring errorMsg = std::wstring(wsl::shared::Localization::MessageWslaUnknownCommand(verb.c_str())); + const std::wstring usage = GetUsageText(); + + auto [out, err, code] = RunWsladiag(verb); + + VERIFY_ARE_EQUAL(1, code); + VERIFY_ARE_EQUAL(L"", NormalizeForCompare(out)); + + const auto nerr = NormalizeForCompare(err); + const auto nerrorMsg = NormalizeForCompare(errorMsg); + const auto nusage = NormalizeForCompare(usage); + + VERIFY_IS_TRUE(nerr.find(nerrorMsg) != std::wstring::npos); + VERIFY_IS_TRUE(nerr.find(nusage) != std::wstring::npos); + } + + // Test that shell command without session name shows error + TEST_METHOD(Shell_MissingName_ShowsError) + { + auto [out, err, code] = RunWsladiag(L"shell"); + VERIFY_ARE_EQUAL(1, code); + VERIFY_ARE_EQUAL(L"", out); + const std::wstring missingArgMsg = + std::wstring(wsl::shared::Localization::MessageMissingArgument(L"", L"wsladiag shell")); + VERIFY_IS_TRUE(NormalizeForCompare(err).find(NormalizeForCompare(missingArgMsg)) != std::wstring::npos); + } + + // Test shell command with invalid session name (silent mode) + TEST_METHOD(Shell_InvalidSessionName_Silent) + { + const auto expectedErr = + std::wstring(wsl::shared::Localization::MessageWslaSessionNotFound(L"DefinitelyNotARealSession")); + ValidateWsladiagOutput(L"shell DefinitelyNotARealSession", 1, L"", expectedErr); + } + + // Test shell command with invalid session name (verbose mode) + TEST_METHOD(Shell_InvalidSessionName_Verbose) + { + const std::wstring name = L"DefinitelyNotARealSession"; + const auto expectedErr = std::wstring(wsl::shared::Localization::MessageWslaSessionNotFound(name.c_str())); + ValidateWsladiagOutput(std::format(L"shell {} --verbose", name), 1, L"", expectedErr); + } + + // Build command line for wsladiag.exe with given arguments + static std::wstring BuildWsladiagCmd(const std::wstring& args) + { + const auto msiPathOpt = wsl::windows::common::wslutil::GetMsiPackagePath(); + VERIFY_IS_TRUE(msiPathOpt.has_value()); + + const auto exePath = std::filesystem::path(*msiPathOpt) / L"wsladiag.exe"; + const auto exe = exePath.wstring(); + + return args.empty() ? std::format(L"\"{}\"", exe) : std::format(L"\"{}\" {}", exe, args); + } + + // Execute wsladiag with given arguments and return output, error, and exit code + static std::tuple RunWsladiag(const std::wstring& args) + { + std::wstring cmd = BuildWsladiagCmd(args); + return LxsstuLaunchCommandAndCaptureOutputWithResult(cmd.data()); + } + + // Normalize for reliable comparisons: + // - Remove '\r' so CRLF -> LF + // - Trim trailing whitespace (usually just final newline) + static std::wstring NormalizeForCompare(std::wstring s) + { + s.erase(std::remove(s.begin(), s.end(), L'\r'), s.end()); + while (!s.empty() && iswspace(s.back())) + { + s.pop_back(); + } + return s; + } + + static void ValidateWsladiagOutput(const std::wstring& cmd, int expectedExitCode, const std::wstring& expectedStdout, const std::wstring& expectedStderr) + { + auto [out, err, code] = RunWsladiag(cmd); + VERIFY_ARE_EQUAL(expectedExitCode, code); + + VERIFY_ARE_EQUAL(NormalizeForCompare(expectedStdout), NormalizeForCompare(out)); + VERIFY_ARE_EQUAL(NormalizeForCompare(expectedStderr), NormalizeForCompare(err)); + } + + // Validate that list command output shows either no sessions message or session table + static void ValidateListOutput(const std::wstring& out) + { + const bool noSessions = out.find(std::wstring(wsl::shared::Localization::MessageWslaNoSessionsFound())) != std::wstring::npos; + const auto idHeader = std::wstring(wsl::shared::Localization::MessageWslaHeaderId()); + const auto pidHeader = std::wstring(wsl::shared::Localization::MessageWslaHeaderCreatorPid()); + const auto nameHeader = std::wstring(wsl::shared::Localization::MessageWslaHeaderDisplayName()); + + const bool hasTable = (out.find(idHeader) != std::wstring::npos) && (out.find(pidHeader) != std::wstring::npos) && + (out.find(nameHeader) != std::wstring::npos); + + VERIFY_IS_TRUE(noSessions || hasTable); + } + + // Validate that usage information contains expected command descriptions + static void ValidateUsage(const std::wstring& err) + { + const std::wstring nerr = NormalizeForCompare(err); + const std::wstring usage = NormalizeForCompare(GetUsageText()); + VERIFY_IS_TRUE(nerr.find(usage) != std::wstring::npos); + } +}; +} // namespace WsladiagTests \ No newline at end of file