Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/shared/inc/CommandLine.h
Original file line number Diff line number Diff line change
Expand Up @@ -307,17 +307,17 @@ 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);
}

#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)
{
}

Expand Down
6 changes: 4 additions & 2 deletions src/windows/wsladiag/wsladiag.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.


--*/

Expand All @@ -21,6 +22,7 @@ Module Name:
#include "ExecutionContext.h"
#include <thread>
#include <format>
#include <iostream>

using namespace wsl::shared;
namespace wslutil = wsl::windows::common::wslutil;
Expand All @@ -36,7 +38,7 @@ static int ReportError(const std::wstring& context, HRESULT hr)
return 1;
}

// Handler for `wsladiag shell <SessionName>` command.
// Handler for `wsladiag shell <SessionName> [--verbose]` command - launches TTY-backed interactive shell.
static int RunShellCommand(std::wstring_view commandLine)
{
std::wstring sessionName;
Expand Down
3 changes: 2 additions & 1 deletion test/windows/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ set(SOURCES
PluginTests.cpp
PolicyTests.cpp
InstallerTests.cpp
WSLATests.cpp)
WSLATests.cpp
WsladiagTests.cpp)

set(HEADERS
Common.h
Expand Down
2 changes: 1 addition & 1 deletion test/windows/WSLATests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
179 changes: 179 additions & 0 deletions test/windows/WsladiagTests.cpp
Original file line number Diff line number Diff line change
@@ -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 <format>

namespace WsladiagTests {
class WsladiagTests
{
WSL_TEST_CLASS(WsladiagTests)
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test class uses WSL_TEST_CLASS macro which doesn't include wsladiag.exe in the BinaryUnderTest property list. While wslaservice.exe is included (which wsladiag depends on), the test is directly executing wsladiag.exe and should declare it as a binary under test. Consider adding a TEST_CLASS_PROPERTY for wsladiag.exe after the WSL_TEST_CLASS macro, similar to how other test classes declare their specific binaries.

Suggested change
WSL_TEST_CLASS(WsladiagTests)
WSL_TEST_CLASS(WsladiagTests)
TEST_CLASS_PROPERTY(L"BinaryUnderTest", L"wsladiag.exe");

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@OneBlue does BinaryUnderTest actually do something useful in WSL? Most docs I see on it seem related to internal infra.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've always assumed that TE would watch said binaries in case they crash and fail the test case if that happens, but I'm not actually not sure that it is the case


// 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to explicitly test that we get no list sessions output here, and then have another test to explicitly validate that list shows sessions when it should.

{
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"<SessionName>", 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does "silent mode" mean here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by silent mode, I meant the default non-verbose behavior. I’ll rename this to “no verbose output” to make it clearer.

{
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<std::wstring, std::wstring, int> 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might make more sense to check if the strings are exactly equal here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, I’ll update the test to check for exact string equality here.

}
};
} // namespace WsladiagTests