mirror of
https://github.com/aphrody-labs/WSL.git
synced 2026-10-17 20:56:23 +00:00
Add a --progress flag to `wslc build` that controls how build output is rendered, accepting "auto", "tty", "plain", or "quiet". The value is converted to a typed ProgressMode at parse time rather than merely validated, so invalid input is rejected up front with a localized error listing the accepted values. "auto" is the default and resolves to "tty" when progress output is an interactive VT console and "plain" otherwise, matching the previous behavior. "tty" renders the scrolling status window in place, "plain" emits each line sequentially without cursor movement or redraws so the output stays readable when redirected to a file or CI log, and "quiet" suppresses progress entirely while still replaying the failing step's log and the error on failure so diagnosability is preserved. Adds parser, argument, callback, and end-to-end coverage for each mode. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
165 lines
6.5 KiB
C++
165 lines
6.5 KiB
C++
/*++
|
|
|
|
Copyright (c) Microsoft. All rights reserved.
|
|
|
|
Module Name:
|
|
|
|
WSLCCLIBuildImageCallbackUnitTests.cpp
|
|
|
|
Abstract:
|
|
|
|
Unit tests for BuildImageCallback progress rendering.
|
|
|
|
--*/
|
|
|
|
#include "precomp.h"
|
|
#include "windows/Common.h"
|
|
#include "WSLCCLITestHelpers.h"
|
|
|
|
#include "BuildImageCallback.h"
|
|
#include "ContainerModel.h"
|
|
#include "Terminal.h"
|
|
|
|
using namespace wsl::windows::wslc;
|
|
using namespace wsl::windows::wslc::models;
|
|
using namespace wsl::windows::wslc::services;
|
|
using namespace WSLCTestHelpers;
|
|
using namespace WEX::Logging;
|
|
using namespace WEX::Common;
|
|
using namespace WEX::TestExecution;
|
|
|
|
namespace WSLCCLIBuildImageCallbackUnitTests {
|
|
|
|
namespace {
|
|
|
|
// Drives a callback through a representative build: a step header, multi-line log output,
|
|
// a \r-based in-place progress update, and a pull progress entry. Returns everything the
|
|
// terminal emitted, captured after the callback is destroyed so its final frame is included.
|
|
std::wstring RunBuild(ProgressMode mode, bool vtEnabled, bool verbose = false)
|
|
{
|
|
CaptureTerminal capture(vtEnabled);
|
|
wil::unique_event cancelEvent;
|
|
cancelEvent.create(wil::EventOptions::ManualReset);
|
|
|
|
{
|
|
BuildImageCallback callback(capture.terminal, cancelEvent.get(), verbose, mode);
|
|
|
|
VERIFY_SUCCEEDED(callback.OnProgress("#1 [1/2] FROM debian:latest\n", "", 0, 0));
|
|
VERIFY_SUCCEEDED(callback.OnProgress("first log line\nsecond log line\n", "log", 0, 0));
|
|
VERIFY_SUCCEEDED(callback.OnProgress("downloading 10%\rdownloading 80%\r", "log", 0, 0));
|
|
VERIFY_SUCCEEDED(callback.OnProgress("sha256:abc downloading", "sha256:abc", 50, 100));
|
|
VERIFY_SUCCEEDED(callback.OnProgress("#2 [2/2] RUN echo hi\n", "", 0, 0));
|
|
}
|
|
|
|
return capture.captured();
|
|
}
|
|
|
|
// Drives the same build as RunBuild, but destroys the callback while an exception is in flight,
|
|
// which is the only condition under which the destructor replays captured output.
|
|
std::wstring RunFailingBuild(ProgressMode mode, bool vtEnabled)
|
|
{
|
|
CaptureTerminal capture(vtEnabled);
|
|
wil::unique_event cancelEvent;
|
|
cancelEvent.create(wil::EventOptions::ManualReset);
|
|
|
|
try
|
|
{
|
|
BuildImageCallback callback(capture.terminal, cancelEvent.get(), false, mode);
|
|
|
|
VERIFY_SUCCEEDED(callback.OnProgress("#1 [1/2] FROM debian:latest\n", "", 0, 0));
|
|
VERIFY_SUCCEEDED(callback.OnProgress("sha256:abc downloading", "sha256:abc", 50, 100));
|
|
VERIFY_SUCCEEDED(callback.OnProgress("#2 [2/2] RUN exit 7\n", "", 0, 0));
|
|
VERIFY_SUCCEEDED(callback.OnProgress(" | about-to-fail\n", "log", 0, 0));
|
|
VERIFY_SUCCEEDED(callback.OnProgress("process \"/bin/sh -c exit 7\" did not complete successfully: exit code: 7\n", "", 0, 0));
|
|
|
|
THROW_HR(E_FAIL);
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
|
|
return capture.captured();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
class WSLCCLIBuildImageCallbackUnitTests
|
|
{
|
|
WSLC_TEST_CLASS(WSLCCLIBuildImageCallbackUnitTests)
|
|
|
|
TEST_CLASS_SETUP(TestClassSetup)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
TEST_CLASS_CLEANUP(TestClassCleanup)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// plain must never redraw. Even when a VT console is attached (the case an E2E test with
|
|
// redirected output cannot reach), the output must be append-only: no cursor movement, no
|
|
// erases, and no escape sequences of any kind.
|
|
TEST_METHOD(BuildImageCallback_PlainOnVtConsole_EmitsNoEscapeSequences)
|
|
{
|
|
const auto output = RunBuild(ProgressMode::Plain, true);
|
|
|
|
VERIFY_IS_TRUE(output.find(L'\x1b') == std::wstring::npos, L"plain mode must not emit any VT escape sequence");
|
|
VERIFY_IS_TRUE(output.find(L"#1 [1/2] FROM debian:latest") != std::wstring::npos, L"build steps must still be reported");
|
|
VERIFY_IS_TRUE(output.find(L"#2 [2/2] RUN echo hi") != std::wstring::npos, L"build steps must still be reported");
|
|
}
|
|
|
|
// Control: the same input in tty mode does emit cursor control. Without this, the test above
|
|
// could pass simply because the rendering path was never exercised.
|
|
TEST_METHOD(BuildImageCallback_TtyOnVtConsole_EmitsEscapeSequences)
|
|
{
|
|
const auto output = RunBuild(ProgressMode::Tty, true);
|
|
|
|
VERIFY_IS_TRUE(output.find(L'\x1b') != std::wstring::npos, L"tty mode is expected to render in place using VT sequences");
|
|
}
|
|
|
|
// plain must produce the same escape-free output regardless of whether a console is attached,
|
|
// so redirecting a plain build to a file yields exactly what was shown on screen.
|
|
TEST_METHOD(BuildImageCallback_PlainMatchesRedirectedOutput)
|
|
{
|
|
const auto onConsole = RunBuild(ProgressMode::Plain, true);
|
|
const auto redirected = RunBuild(ProgressMode::Plain, false);
|
|
|
|
VERIFY_ARE_EQUAL(redirected, onConsole);
|
|
}
|
|
|
|
// quiet suppresses progress entirely on success, and must not emit cursor control either.
|
|
// This also covers the success side of replay: nothing captured may reach the terminal.
|
|
TEST_METHOD(BuildImageCallback_QuietOnVtConsole_EmitsNothing)
|
|
{
|
|
const auto output = RunBuild(ProgressMode::Quiet, true);
|
|
|
|
VERIFY_ARE_EQUAL(std::wstring{L""}, output);
|
|
}
|
|
|
|
// quiet prints nothing while the build runs, but a failure must still explain what went wrong.
|
|
// The step that failed and the error itself are reported with an empty id rather than "log", so
|
|
// they have to be captured too or the replay only shows unattributed log output.
|
|
TEST_METHOD(BuildImageCallback_QuietReplaysFailingStepAndError)
|
|
{
|
|
const auto output = RunFailingBuild(ProgressMode::Quiet, true);
|
|
|
|
VERIFY_IS_TRUE(output.find(L"#2 [2/2] RUN exit 7") != std::wstring::npos, L"quiet must replay the step that failed");
|
|
VERIFY_IS_TRUE(
|
|
output.find(L"did not complete successfully: exit code: 7") != std::wstring::npos,
|
|
L"quiet must replay the build error");
|
|
VERIFY_IS_TRUE(output.find(L"about-to-fail") != std::wstring::npos, L"quiet must replay log output");
|
|
}
|
|
|
|
// Pull progress is rewritten in place and is the one message sent without a trailing newline,
|
|
// so replaying it would emit a partial line into an otherwise line-oriented transcript.
|
|
TEST_METHOD(BuildImageCallback_QuietReplayOmitsPullProgress)
|
|
{
|
|
const auto output = RunFailingBuild(ProgressMode::Quiet, true);
|
|
|
|
VERIFY_IS_TRUE(output.find(L"sha256:abc downloading") == std::wstring::npos, L"quiet must not replay pull progress");
|
|
}
|
|
};
|
|
|
|
} // namespace WSLCCLIBuildImageCallbackUnitTests
|