-
Notifications
You must be signed in to change notification settings - Fork 1.7k
CLI: Add logging options with end-to-end tests #40314
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/wsl-for-apps
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -392,6 +392,26 @@ void ViewContainerLogs(CLIExecutionContext& context) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| auto& session = context.Data.Get<Data::Session>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| auto containerId = context.Args.Get<ArgType::ContainerId>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bool follow = context.Args.Contains(ArgType::Follow); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ContainerService::Logs(session, WideToMultiByte(containerId), follow); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bool timestamps = context.Args.Contains(ArgType::Timestamps); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ULONGLONG tail = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (context.Args.Contains(ArgType::Tail)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tail = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Tail>())); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ULONGLONG since = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (context.Args.Contains(ArgType::Since)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| since = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Since>())); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ULONGLONG until = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (context.Args.Contains(ArgType::Until)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| until = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Until>())); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+400
to
+412
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tail = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Tail>())); | |
| } | |
| ULONGLONG since = 0; | |
| if (context.Args.Contains(ArgType::Since)) | |
| { | |
| since = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Since>())); | |
| } | |
| ULONGLONG until = 0; | |
| if (context.Args.Contains(ArgType::Until)) | |
| { | |
| until = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Until>())); | |
| tail = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Tail>(), L"tail"); | |
| } | |
| ULONGLONG since = 0; | |
| if (context.Args.Contains(ArgType::Since)) | |
| { | |
| since = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Since>(), L"since"); | |
| } | |
| ULONGLONG until = 0; | |
| if (context.Args.Contains(ArgType::Until)) | |
| { | |
| until = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Until>(), L"until"); |
Copilot
AI
Apr 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
--since is parsed with std::stoull, which can throw and produce a non-user-friendly error path. Prefer validation::GetIntegerFromString<ULONGLONG> (or a dedicated validator in Argument::Validate) so invalid timestamps are rejected with a consistent CLI ArgumentException message.
| tail = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Tail>())); | |
| } | |
| ULONGLONG since = 0; | |
| if (context.Args.Contains(ArgType::Since)) | |
| { | |
| since = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Since>())); | |
| } | |
| ULONGLONG until = 0; | |
| if (context.Args.Contains(ArgType::Until)) | |
| { | |
| until = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Until>())); | |
| tail = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Tail>()); | |
| } | |
| ULONGLONG since = 0; | |
| if (context.Args.Contains(ArgType::Since)) | |
| { | |
| since = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Since>()); | |
| } | |
| ULONGLONG until = 0; | |
| if (context.Args.Contains(ArgType::Until)) | |
| { | |
| until = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Until>()); |
Copilot
AI
Apr 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as --since: parsing --until via std::stoull can throw and skip the CLI's normal argument-error handling. Use the repo's integer parsing/validation helpers so bad values surface as a clear user-facing argument error.
| tail = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Tail>())); | |
| } | |
| ULONGLONG since = 0; | |
| if (context.Args.Contains(ArgType::Since)) | |
| { | |
| since = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Since>())); | |
| } | |
| ULONGLONG until = 0; | |
| if (context.Args.Contains(ArgType::Until)) | |
| { | |
| until = std::stoull(WideToMultiByte(context.Args.Get<ArgType::Until>())); | |
| tail = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Tail>()); | |
| } | |
| ULONGLONG since = 0; | |
| if (context.Args.Contains(ArgType::Since)) | |
| { | |
| since = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Since>()); | |
| } | |
| ULONGLONG until = 0; | |
| if (context.Args.Contains(ArgType::Until)) | |
| { | |
| until = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Until>()); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| /*++ | ||
|
|
||
| Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| Module Name: | ||
|
|
||
| WSLCE2EContainerLogsTests.cpp | ||
|
|
||
| Abstract: | ||
|
|
||
| This file contains end-to-end tests for WSLC. | ||
| --*/ | ||
|
|
||
| #include "precomp.h" | ||
| #include "windows/Common.h" | ||
| #include "WSLCExecutor.h" | ||
| #include "WSLCE2EHelpers.h" | ||
|
|
||
| namespace WSLCE2ETests { | ||
| using namespace wsl::shared; | ||
|
|
||
| class WSLCE2EContainerLogsTests | ||
| { | ||
| WSLC_TEST_CLASS(WSLCE2EContainerLogsTests) | ||
|
|
||
| TEST_CLASS_SETUP(ClassSetup) | ||
| { | ||
| EnsureImageIsLoaded(DebianImage); | ||
| return true; | ||
| } | ||
|
|
||
| TEST_CLASS_CLEANUP(ClassCleanup) | ||
| { | ||
| EnsureContainerDoesNotExist(WslcContainerName); | ||
| EnsureImageIsDeleted(DebianImage); | ||
| return true; | ||
| } | ||
|
|
||
| TEST_METHOD_SETUP(MethodSetup) | ||
| { | ||
| EnsureContainerDoesNotExist(WslcContainerName); | ||
| return true; | ||
| } | ||
|
|
||
| WSLC_TEST_METHOD(WSLCE2E_Container_Logs_HelpCommand) | ||
| { | ||
| auto result = RunWslc(L"container logs --help"); | ||
| result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"", .ExitCode = 0}); | ||
| } | ||
|
|
||
| WSLC_TEST_METHOD(WSLCE2E_Container_Logs_MissingContainerId) | ||
| { | ||
| auto result = RunWslc(L"container logs"); | ||
| result.Verify({.Stdout = GetHelpMessage(), .Stderr = L"Required argument not provided: 'container-id'\r\n", .ExitCode = 1}); | ||
| } | ||
|
|
||
| WSLC_TEST_METHOD(WSLCE2E_Container_Logs_Success) | ||
| { | ||
| auto result = RunWslc(std::format( | ||
| L"container run --name {} {} sh -c \"echo hello && echo world\"", WslcContainerName, DebianImage.NameAndTag())); | ||
| result.Verify({.Stderr = L"", .ExitCode = 0}); | ||
|
|
||
| result = RunWslc(std::format(L"container logs {}", WslcContainerName)); | ||
| result.Verify({.Stderr = L"", .ExitCode = 0}); | ||
| auto lines = result.GetStdoutLines(); | ||
| VERIFY_ARE_NOT_EQUAL(lines.end(), std::find(lines.begin(), lines.end(), L"hello")); | ||
| VERIFY_ARE_NOT_EQUAL(lines.end(), std::find(lines.begin(), lines.end(), L"world")); | ||
| } | ||
|
|
||
| WSLC_TEST_METHOD(WSLCE2E_Container_Logs_TailOption) | ||
| { | ||
| auto result = RunWslc(std::format( | ||
| L"container run --name {} {} sh -c \"echo line1 && echo line2 && echo line3\"", WslcContainerName, DebianImage.NameAndTag())); | ||
| result.Verify({.Stderr = L"", .ExitCode = 0}); | ||
|
|
||
| result = RunWslc(std::format(L"container logs --tail 1 {}", WslcContainerName)); | ||
| result.Verify({.Stderr = L"", .ExitCode = 0}); | ||
| auto lines = result.GetStdoutLines(); | ||
| VERIFY_ARE_EQUAL(1u, lines.size()); | ||
| VERIFY_ARE_EQUAL(L"line3", lines[0]); | ||
| } | ||
|
|
||
| WSLC_TEST_METHOD(WSLCE2E_Container_Logs_TimestampsOption) | ||
| { | ||
| auto result = | ||
| RunWslc(std::format(L"container run --name {} {} sh -c \"echo hello\"", WslcContainerName, DebianImage.NameAndTag())); | ||
| result.Verify({.Stderr = L"", .ExitCode = 0}); | ||
|
|
||
| result = RunWslc(std::format(L"container logs --timestamps {}", WslcContainerName)); | ||
| result.Verify({.Stderr = L"", .ExitCode = 0}); | ||
|
|
||
| auto lines = result.GetStdoutLines(); | ||
| VERIFY_IS_TRUE(lines.size() >= 1u); | ||
| // Timestamps are prepended in ISO 8601 / RFC 3339 format, verify by checking for 'T' separator | ||
| VERIFY_IS_TRUE(lines[0].find(L"T") != std::wstring::npos); | ||
| VERIFY_IS_TRUE(lines[0].find(L"hello") != std::wstring::npos); | ||
| } | ||
|
|
||
| WSLC_TEST_METHOD(WSLCE2E_Container_Logs_FollowOption) | ||
| { | ||
| // Run a detached container that outputs lines with a delay between them | ||
| auto result = RunWslc(std::format( | ||
| L"container run -d --name {} {} sh -c \"echo first && sleep 2 && echo second\"", WslcContainerName, DebianImage.NameAndTag())); | ||
| result.Verify({.Stderr = L"", .ExitCode = 0}); | ||
|
|
||
| // Start following logs interactively — this blocks until the container exits | ||
| auto session = RunWslcInteractive(std::format(L"container logs --follow {}", WslcContainerName)); | ||
| VERIFY_IS_TRUE(session.IsRunning(), L"Follow session should be running"); | ||
|
|
||
| // Expect the first line of output | ||
| session.ExpectStdout("first\n"); | ||
|
|
||
| // Expect the second line which comes after the sleep | ||
| session.ExpectStdout("second\n"); | ||
|
|
||
| // The container exits after printing second, so follow should terminate | ||
| auto exitCode = session.Wait(30000); | ||
| VERIFY_ARE_EQUAL(0, exitCode); | ||
| } | ||
|
|
||
| private: | ||
| const std::wstring WslcContainerName = L"wslc-e2e-container-logs"; | ||
| const TestImage& DebianImage = DebianTestImage(); | ||
|
|
||
| std::wstring GetHelpMessage() const | ||
| { | ||
| std::wstringstream output; | ||
| output << GetWslcHeader() // | ||
| << GetDescription() // | ||
| << GetUsage() // | ||
| << GetAvailableCommands() // | ||
| << GetAvailableOptions(); | ||
| return output.str(); | ||
| } | ||
|
|
||
| std::wstring GetDescription() const | ||
| { | ||
| return Localization::WSLCCLI_ContainerLogsLongDesc() + L"\r\n\r\n"; | ||
| } | ||
|
|
||
| std::wstring GetUsage() const | ||
| { | ||
| return L"Usage: wslc container logs [<options>] <container-id>\r\n\r\n"; | ||
| } | ||
|
|
||
| std::wstring GetAvailableCommands() const | ||
| { | ||
| std::wstringstream commands; | ||
| commands << L"The following arguments are available:\r\n" // | ||
| << L" container-id Container name or id\r\n" // | ||
| << L"\r\n"; | ||
| return commands.str(); | ||
| } | ||
|
|
||
| std::wstring GetAvailableOptions() const | ||
| { | ||
| std::wstringstream options; | ||
| options << L"The following options are available:\r\n" // | ||
| << L" --session Specify the session to use\r\n" // | ||
| << L" -f,--follow Follow log output\r\n" // | ||
| << L" -n,--tail Number of lines to show from the end of the logs\r\n" // | ||
| << L" --since Show logs since timestamp (e.g. unix timestamp)\r\n" // | ||
| << L" --until Show logs before timestamp (e.g. unix timestamp)\r\n" // | ||
| << L" --timestamps Show timestamps in log output\r\n" // | ||
| << L" -?,--help Shows help about the selected command\r\n" // | ||
| << L"\r\n"; | ||
| return options.str(); | ||
| } | ||
| }; | ||
| } // namespace WSLCE2ETests |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This command adds
--since/--untilarguments, but the new E2E suite only covers--tail,--timestamps, and--follow. Add at least one end-to-end test that exercises--sinceand--untilfiltering (or validates their error handling) to ensure these options remain functional.