1
+/*++
2
+
3
+Copyright (c) Microsoft. All rights reserved.
4
+
5
+Module Name:
6
+
7
+ WSLCCLIBuildImageCallbackUnitTests.cpp
8
+
9
+Abstract:
10
+
11
+ Unit tests for BuildImageCallback progress rendering.
12
+
13
+--*/
14
+
15
+#include "precomp.h"
16
+#include "windows/Common.h"
17
+#include "WSLCCLITestHelpers.h"
18
+
19
+#include "BuildImageCallback.h"
20
+#include "ContainerModel.h"
21
+#include "Terminal.h"
22
+
23
+using namespace wsl::windows::wslc;
24
+using namespace wsl::windows::wslc::models;
25
+using namespace wsl::windows::wslc::services;
26
+using namespace WSLCTestHelpers;
27
+using namespace WEX::Logging;
28
+using namespace WEX::Common;
29
+using namespace WEX::TestExecution;
30
+
31
+namespace WSLCCLIBuildImageCallbackUnitTests {
32
+
33
+namespace {
34
+
35
+ // Drives a callback through a representative build: a step header, multi-line log output,
36
+ // a \r-based in-place progress update, and a pull progress entry. Returns everything the
37
+ // terminal emitted, captured after the callback is destroyed so its final frame is included.
38
+ std::wstring RunBuild(ProgressMode mode, bool vtEnabled, bool verbose = false)
39
+ {
40
+ CaptureTerminal capture(vtEnabled);
41
+ wil::unique_event cancelEvent;
42
+ cancelEvent.create(wil::EventOptions::ManualReset);
43
+
44
+ {
45
+ BuildImageCallback callback(capture.terminal, cancelEvent.get(), verbose, mode);
46
+
47
+ VERIFY_SUCCEEDED(callback.OnProgress("#1 [1/2] FROM debian:latest\n", "", 0, 0));
48
+ VERIFY_SUCCEEDED(callback.OnProgress("first log line\nsecond log line\n", "log", 0, 0));
49
+ VERIFY_SUCCEEDED(callback.OnProgress("downloading 10%\rdownloading 80%\r", "log", 0, 0));
50
+ VERIFY_SUCCEEDED(callback.OnProgress("sha256:abc downloading", "sha256:abc", 50, 100));
51
+ VERIFY_SUCCEEDED(callback.OnProgress("#2 [2/2] RUN echo hi\n", "", 0, 0));
52
+ }
53
+
54
+ return capture.captured();
55
+ }
56
+
57
+ // Drives the same build as RunBuild, but destroys the callback while an exception is in flight,
58
+ // which is the only condition under which the destructor replays captured output.
59
+ std::wstring RunFailingBuild(ProgressMode mode, bool vtEnabled)
60
+ {
61
+ CaptureTerminal capture(vtEnabled);
62
+ wil::unique_event cancelEvent;
63
+ cancelEvent.create(wil::EventOptions::ManualReset);
64
+
65
+ try
66
+ {
67
+ BuildImageCallback callback(capture.terminal, cancelEvent.get(), false, mode);
68
+
69
+ VERIFY_SUCCEEDED(callback.OnProgress("#1 [1/2] FROM debian:latest\n", "", 0, 0));
70
+ VERIFY_SUCCEEDED(callback.OnProgress("sha256:abc downloading", "sha256:abc", 50, 100));
71
+ VERIFY_SUCCEEDED(callback.OnProgress("#2 [2/2] RUN exit 7\n", "", 0, 0));
72
+ VERIFY_SUCCEEDED(callback.OnProgress(" | about-to-fail\n", "log", 0, 0));
73
+ VERIFY_SUCCEEDED(callback.OnProgress("process \"/bin/sh -c exit 7\" did not complete successfully: exit code: 7\n", "", 0, 0));
74
+
75
+ THROW_HR(E_FAIL);
76
+ }
77
+ catch (...)
78
+ {
79
+ }
80
+
81
+ return capture.captured();
82
+ }
83
+
84
+} // namespace
85
+
86
+class WSLCCLIBuildImageCallbackUnitTests
87
+{
88
+ WSLC_TEST_CLASS(WSLCCLIBuildImageCallbackUnitTests)
89
+
90
+ TEST_CLASS_SETUP(TestClassSetup)
91
+ {
92
+ return true;
93
+ }
94
+
95
+ TEST_CLASS_CLEANUP(TestClassCleanup)
96
+ {
97
+ return true;
98
+ }
99
+
100
+ // plain must never redraw. Even when a VT console is attached (the case an E2E test with
101
+ // redirected output cannot reach), the output must be append-only: no cursor movement, no
102
+ // erases, and no escape sequences of any kind.
103
+ TEST_METHOD(BuildImageCallback_PlainOnVtConsole_EmitsNoEscapeSequences)
104
+ {
105
+ const auto output = RunBuild(ProgressMode::Plain, true);
106
+
107
+ VERIFY_IS_TRUE(output.find(L'\x1b') == std::wstring::npos, L"plain mode must not emit any VT escape sequence");
108
+ VERIFY_IS_TRUE(output.find(L"#1 [1/2] FROM debian:latest") != std::wstring::npos, L"build steps must still be reported");
109
+ VERIFY_IS_TRUE(output.find(L"#2 [2/2] RUN echo hi") != std::wstring::npos, L"build steps must still be reported");
110
+ }
111
+
112
+ // Control: the same input in tty mode does emit cursor control. Without this, the test above
113
+ // could pass simply because the rendering path was never exercised.
114
+ TEST_METHOD(BuildImageCallback_TtyOnVtConsole_EmitsEscapeSequences)
115
+ {
116
+ const auto output = RunBuild(ProgressMode::Tty, true);
117
+
118
+ VERIFY_IS_TRUE(output.find(L'\x1b') != std::wstring::npos, L"tty mode is expected to render in place using VT sequences");
119
+ }
120
+
121
+ // plain must produce the same escape-free output regardless of whether a console is attached,
122
+ // so redirecting a plain build to a file yields exactly what was shown on screen.
123
+ TEST_METHOD(BuildImageCallback_PlainMatchesRedirectedOutput)
124
+ {
125
+ const auto onConsole = RunBuild(ProgressMode::Plain, true);
126
+ const auto redirected = RunBuild(ProgressMode::Plain, false);
127
+
128
+ VERIFY_ARE_EQUAL(redirected, onConsole);
129
+ }
130
+
131
+ // quiet suppresses progress entirely on success, and must not emit cursor control either.
132
+ // This also covers the success side of replay: nothing captured may reach the terminal.
133
+ TEST_METHOD(BuildImageCallback_QuietOnVtConsole_EmitsNothing)
134
+ {
135
+ const auto output = RunBuild(ProgressMode::Quiet, true);
136
+
137
+ VERIFY_ARE_EQUAL(std::wstring{L""}, output);
138
+ }
139
+
140
+ // quiet prints nothing while the build runs, but a failure must still explain what went wrong.
141
+ // The step that failed and the error itself are reported with an empty id rather than "log", so
142
+ // they have to be captured too or the replay only shows unattributed log output.
143
+ TEST_METHOD(BuildImageCallback_QuietReplaysFailingStepAndError)
144
+ {
145
+ const auto output = RunFailingBuild(ProgressMode::Quiet, true);
146
+
147
+ VERIFY_IS_TRUE(output.find(L"#2 [2/2] RUN exit 7") != std::wstring::npos, L"quiet must replay the step that failed");
148
+ VERIFY_IS_TRUE(
149
+ output.find(L"did not complete successfully: exit code: 7") != std::wstring::npos,
150
+ L"quiet must replay the build error");
151
+ VERIFY_IS_TRUE(output.find(L"about-to-fail") != std::wstring::npos, L"quiet must replay log output");
152
+ }
153
+
154
+ // Pull progress is rewritten in place and is the one message sent without a trailing newline,
155
+ // so replaying it would emit a partial line into an otherwise line-oriented transcript.
156
+ TEST_METHOD(BuildImageCallback_QuietReplayOmitsPullProgress)
157
+ {
158
+ const auto output = RunFailingBuild(ProgressMode::Quiet, true);
159
+
160
+ VERIFY_IS_TRUE(output.find(L"sha256:abc downloading") == std::wstring::npos, L"quiet must not replay pull progress");
161
+ }
162
+};
163
+
164
+} // namespace WSLCCLIBuildImageCallbackUnitTests