CLI: TableOutput unit tests#40095
Conversation
There was a problem hiding this comment.
Pull request overview
This PR makes TableOutput more unit-testable by adding injectable output and a console-width override, and introduces new CLI-focused unit tests to validate table rendering behavior.
Changes:
- Add an overridable output function to
TableOutput(defaulting to stdout) to enable in-memory capture in tests. - Add a console width override to make column-shrinking tests deterministic.
- Add a new unit test suite covering headers, padding, width expansion/truncation, shrinking, and
IsEmpty().
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| test/windows/wslc/WSLCCLITableOutputUnitTests.cpp | Adds unit tests for TableOutput rendering and sizing behavior using the new output injection hooks. |
| src/windows/wslc/core/TableOutput.h | Adds SetOutputFunction and SetConsoleWidthOverride, and routes rendered lines through the configured output function. |
| WSLCTableOutputUnitTests.cpp | ||
|
|
||
| Abstract: | ||
|
|
||
| Unit tests for the TableOutput class. | ||
| Output is redirected to an in-memory std::wstringstream so that tests | ||
| are fully self-contained and do not depend on a real console or FILE*. |
There was a problem hiding this comment.
The file header comment is inconsistent with the actual file name (Module Name says WSLCTableOutputUnitTests.cpp, but the file is WSLCCLITableOutputUnitTests.cpp) and the Abstract mentions redirecting output to a std::wstringstream, while the tests capture output via SetOutputFunction into a vector<wstring>. Please update the header comment to match the current test implementation to avoid confusion.
| WSLCTableOutputUnitTests.cpp | |
| Abstract: | |
| Unit tests for the TableOutput class. | |
| Output is redirected to an in-memory std::wstringstream so that tests | |
| are fully self-contained and do not depend on a real console or FILE*. | |
| WSLCCLITableOutputUnitTests.cpp | |
| Abstract: | |
| Unit tests for the TableOutput class. | |
| Output is captured via SetOutputFunction into an in-memory | |
| std::vector<std::wstring> so that tests are fully self-contained and do | |
| not depend on a real console or FILE*. |
| // Use a custom padding of 3 (the default) and verify the data row | ||
| // contains at least 3 spaces between the first column value and the | ||
| // start of the second column value. | ||
| TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"}, /*sizingBuffer=*/50, /*columnPadding=*/3); | ||
|
|
||
| cap.table.OutputLine({L"abc", L"ok"}); | ||
| cap.table.Complete(); | ||
|
|
||
| // Data row: "abc" padded to header width ("NAME"=4) + 3 spaces, then "ok" | ||
| // Expected: "abc ok" with appropriate spacing | ||
| const std::wstring& dataLine = cap.lines[1]; | ||
| VERIFY_IS_TRUE(dataLine.find(L"abc") != std::wstring::npos); | ||
| VERIFY_IS_TRUE(dataLine.find(L"ok") != std::wstring::npos); | ||
|
|
||
| // There must be at least 3 spaces between the two values | ||
| auto namePos = dataLine.find(L"abc"); | ||
| auto statusPos = dataLine.find(L"ok"); | ||
| VERIFY_IS_TRUE(statusPos > namePos + 3); |
There was a problem hiding this comment.
This spacing assertion is too weak: statusPos > namePos + 3 can pass even when there are fewer than 3 spaces between the values (e.g., if ok starts immediately after abc plus 1 space). Consider asserting statusPos >= namePos + wcslen(L"abc") + columnPadding (or directly checking that the substring between the tokens contains the expected number of spaces).
| // Use a custom padding of 3 (the default) and verify the data row | |
| // contains at least 3 spaces between the first column value and the | |
| // start of the second column value. | |
| TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"}, /*sizingBuffer=*/50, /*columnPadding=*/3); | |
| cap.table.OutputLine({L"abc", L"ok"}); | |
| cap.table.Complete(); | |
| // Data row: "abc" padded to header width ("NAME"=4) + 3 spaces, then "ok" | |
| // Expected: "abc ok" with appropriate spacing | |
| const std::wstring& dataLine = cap.lines[1]; | |
| VERIFY_IS_TRUE(dataLine.find(L"abc") != std::wstring::npos); | |
| VERIFY_IS_TRUE(dataLine.find(L"ok") != std::wstring::npos); | |
| // There must be at least 3 spaces between the two values | |
| auto namePos = dataLine.find(L"abc"); | |
| auto statusPos = dataLine.find(L"ok"); | |
| VERIFY_IS_TRUE(statusPos > namePos + 3); | |
| constexpr size_t columnPadding = 3; | |
| constexpr wchar_t firstValue[] = L"abc"; | |
| // Use a custom padding of 3 (the default) and verify the data row | |
| // contains at least 3 spaces between the first column value and the | |
| // start of the second column value. | |
| TableOutputCapture<2> cap(TableOutput<2>::header_t{L"NAME", L"STATUS"}, /*sizingBuffer=*/50, /*columnPadding=*/columnPadding); | |
| cap.table.OutputLine({firstValue, L"ok"}); | |
| cap.table.Complete(); | |
| // Data row: "abc" padded to header width ("NAME"=4) + 3 spaces, then "ok" | |
| // Expected: "abc ok" with appropriate spacing | |
| const std::wstring& dataLine = cap.lines[1]; | |
| VERIFY_IS_TRUE(dataLine.find(firstValue) != std::wstring::npos); | |
| VERIFY_IS_TRUE(dataLine.find(L"ok") != std::wstring::npos); | |
| // There must be at least 3 spaces between the end of the first value | |
| // and the start of the second value. | |
| auto namePos = dataLine.find(firstValue); | |
| auto statusPos = dataLine.find(L"ok"); | |
| constexpr size_t firstValueLength = (sizeof(firstValue) / sizeof(firstValue[0])) - 1; | |
| VERIFY_IS_TRUE(statusPos >= namePos + firstValueLength + columnPadding); |
There was a problem hiding this comment.
nice, copilot very good at this sort of thing
| const std::wstring& dataLine = cap.lines[1]; | ||
| auto posX = dataLine.find(L'x'); | ||
| auto posY = dataLine.find(L'y'); | ||
| VERIFY_IS_TRUE(posY >= posX + customPadding); |
There was a problem hiding this comment.
The position calculation is off by one: to enforce 5-space padding after the x character, the start of y should be compared against posX + 1 + customPadding (the code currently uses posX + customPadding). Also, please assert posX != npos and posY != npos before doing arithmetic/comparisons; otherwise npos can wrap on addition and produce false positives.
| VERIFY_IS_TRUE(posY >= posX + customPadding); | |
| VERIFY_IS_TRUE(posX != std::wstring::npos); | |
| VERIFY_IS_TRUE(posY != std::wstring::npos); | |
| VERIFY_IS_TRUE(posY >= posX + 1 + customPadding); |
| // Data line: "abc" starts at position 0, "ok" must not start before | ||
| // position 14 + padding. | ||
| const std::wstring& dataLine = cap.lines[1]; | ||
| auto posOk = dataLine.find(L"ok"); |
There was a problem hiding this comment.
posOk is not validated before being compared. If find returns npos, the comparison can still pass (since npos is a very large size_t). Add VERIFY_IS_TRUE(posOk != std::wstring::npos) (or equivalent) before the range check to avoid false positives.
| auto posOk = dataLine.find(L"ok"); | |
| auto posOk = dataLine.find(L"ok"); | |
| VERIFY_IS_TRUE(posOk != std::wstring::npos); |
| // Override the output function (e.g. redirect to a stringstream in tests). | ||
| void SetOutputFunction(OutputFn fn) | ||
| { |
There was a problem hiding this comment.
SetOutputFunction allows callers to pass an empty std::function (e.g. OutputFn{}), but OutputLineToStream unconditionally invokes m_outputFn(rendered), which will throw std::bad_function_call. Consider guarding against empty callables (reset to the default output if !fn), and/or providing a public way to restore the default output function (e.g. ResetOutputFunction() or making DefaultOutputFn() publicly accessible).
| // Override the output function (e.g. redirect to a stringstream in tests). | |
| void SetOutputFunction(OutputFn fn) | |
| { | |
| // Override the output function (e.g. redirect to a stringstream in tests). | |
| // Ignore empty callables so the current/default output function remains valid. | |
| void SetOutputFunction(OutputFn fn) | |
| { | |
| if (!fn) | |
| { | |
| return; | |
| } |
There was a problem hiding this comment.
I'll add an assert for this, we do not expect this to be used extensively or really at all outside tests at this time.
| // Helper: capture all lines emitted by a TableOutput into a vector<wstring>. | ||
| // --------------------------------------------------------------------------- | ||
| template <size_t N> | ||
| struct TableOutputCapture |
There was a problem hiding this comment.
Should we move this to a shared/helper class? I think this will be helpful for E2E testing as well. Today we find image and container names in output lines. With this class, we can reconstruct the table and instead do VERIFY_ARE_EQUAL(rendered, stdout).
Alternatively, maybe the TableOutput class can have a ToString() that would give us the output as a string
|
Hey @dkbennett 👋 — Following up on this draft PR. CI is green, but there are 6 unresolved review threads that still need to be addressed:
Is this change still being worked on? It's been a few days since the last update. Let us know if you need any help with the review feedback! |
Summary of the Pull Request
Updates the table class slightly to make it easier for unit testing.
Adds unit tests to verify table output behavior.
PR Checklist
Detailed Description of the Pull Request / Additional comments
Main change to tableoutput is adding a default output function and the ability to override it. The override is mainly for testing, but it may be useful in other contexts in the future.
Validation Steps Performed
Tests validated and passed