master
cpp 565 lines 23.4 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WSLCCLIVTSupportUnitTests.cpp
8
9 Abstract:
10
11 This file contains unit tests for VT sequence construction and console mode helpers.
12
13 --*/
14
15 #include "precomp.h"
16 #include "windows/Common.h"
17 #include "VTSupport.h"
18
19 using namespace WEX::Logging;
20 using namespace WEX::Common;
21 using namespace WEX::TestExecution;
22 using namespace wsl::windows::common::vt;
23
24 namespace WSLCCLIVTSupportUnitTests {
25
26 // Creates a real console screen buffer that can be used as an output handle for console API tests.
27 // The buffer is not attached to the visible console window, so it does not affect the test runner output.
28 static wil::unique_hfile MakeScreenBuffer()
29 {
30 wil::unique_hfile handle{CreateConsoleScreenBuffer(
31 GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, CONSOLE_TEXTMODE_BUFFER, nullptr)};
32 THROW_LAST_ERROR_IF(!handle);
33 return handle;
34 }
35
36 class WSLCCLIVTSupportUnitTests
37 {
38 WSLC_TEST_CLASS(WSLCCLIVTSupportUnitTests)
39
40 TEST_METHOD(VT_Sequence)
41 {
42 // Constant sequence round-trips correctly.
43 const Sequence constant{L"\x1b[0m"};
44 VERIFY_ARE_EQUAL(L"\x1b[0m", constant);
45
46 // Default-constructed ConstructedSequence is empty.
47 ConstructedSequence empty;
48 VERIFY_IS_TRUE(empty.Get().empty());
49
50 // Construct from string.
51 ConstructedSequence seq{L"\x1b[1m"};
52 VERIFY_ARE_EQUAL(L"\x1b[1m", seq);
53
54 // Append combines sequences.
55 seq.Append(Sequence{L"\x1b[91m"});
56 VERIFY_ARE_EQUAL(L"\x1b[1m\x1b[91m", seq);
57
58 // Appending an empty sequence is a no-op.
59 seq.Append(Sequence{});
60 VERIFY_ARE_EQUAL(L"\x1b[1m\x1b[91m", seq);
61
62 // Clear resets to empty.
63 seq.Clear();
64 VERIFY_IS_TRUE(seq.Get().empty());
65
66 // Copy construction.
67 ConstructedSequence original{L"\x1b[1m"};
68 ConstructedSequence copy{original};
69 VERIFY_ARE_EQUAL(original.Get(), copy.Get());
70
71 // Move construction.
72 ConstructedSequence moved{std::move(original)};
73 VERIFY_ARE_EQUAL(L"\x1b[1m", moved);
74
75 // Copy assignment.
76 ConstructedSequence assigned;
77 assigned = copy;
78 VERIFY_ARE_EQUAL(copy.Get(), assigned.Get());
79
80 // Move assignment.
81 ConstructedSequence moveAssigned;
82 moveAssigned = std::move(moved);
83 VERIFY_ARE_EQUAL(L"\x1b[1m", moveAssigned);
84
85 // SGR Construction.
86 VERIFY_ARE_EQUAL(L"\x1b[1;31m", Sgr({1, 31}));
87 VERIFY_ARE_EQUAL(L"\x1b[0m", Sgr({0}));
88 }
89
90 TEST_METHOD(VT_CursorSequences)
91 {
92 VERIFY_ARE_EQUAL(L"\x1b[3A", Cursor::Up(3));
93 VERIFY_ARE_EQUAL(L"\x1b[5B", Cursor::Down(5));
94 VERIFY_ARE_EQUAL(L"\x1b[2C", Cursor::Forward(2));
95 VERIFY_ARE_EQUAL(L"\x1b[1D", Cursor::Backward(1));
96 VERIFY_ARE_EQUAL(L"\x1b[5;10H", Cursor::MoveTo(5, 10));
97 VERIFY_ARE_EQUAL(L"\x1b[H", Cursor::Home);
98
99 // cells == 0 is a no-op — returns an empty sequence rather than
100 // emitting ESC[0X, which most terminals treat as "move 1 cell".
101 VERIFY_IS_TRUE(Cursor::Up(0).Get().empty());
102 VERIFY_IS_TRUE(Cursor::Down(0).Get().empty());
103 VERIFY_IS_TRUE(Cursor::Forward(0).Get().empty());
104 VERIFY_IS_TRUE(Cursor::Backward(0).Get().empty());
105
106 VERIFY_THROWS_SPECIFIC(
107 Cursor::Up(-1), wil::ResultException, [](const wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; });
108 VERIFY_THROWS_SPECIFIC(Cursor::MoveTo(0, 1), wil::ResultException, [](const wil::ResultException& e) {
109 return e.GetErrorCode() == E_INVALIDARG;
110 });
111 VERIFY_THROWS_SPECIFIC(Cursor::MoveTo(1, 0), wil::ResultException, [](const wil::ResultException& e) {
112 return e.GetErrorCode() == E_INVALIDARG;
113 });
114
115 VERIFY_ARE_EQUAL(L"\x1b[?2004h", Cursor::BracketedPasteOn);
116 VERIFY_ARE_EQUAL(L"\x1b[?2004l", Cursor::BracketedPasteOff);
117 }
118
119 TEST_METHOD(VT_TextFormatSequences)
120 {
121 VERIFY_ARE_EQUAL(L"\x1b[0m", Format::Default);
122 VERIFY_ARE_EQUAL(L"\x1b[7m", Format::Negative);
123 VERIFY_ARE_EQUAL(L"\x1b[1m", Format::Bright);
124 VERIFY_ARE_EQUAL(L"\x1b[2m", Format::Dim);
125 VERIFY_ARE_EQUAL(L"\x1b[22m", Format::Normal);
126 VERIFY_ARE_EQUAL(L"\x1b[3m", Format::Italic);
127 VERIFY_ARE_EQUAL(L"\x1b[23m", Format::NoItalic);
128 VERIFY_ARE_EQUAL(L"\x1b[4m", Format::Underline);
129 VERIFY_ARE_EQUAL(L"\x1b[24m", Format::NoUnderline);
130
131 VERIFY_ARE_EQUAL(L"\x1b[30m", Format::Fg::Black);
132 VERIFY_ARE_EQUAL(L"\x1b[31m", Format::Fg::Red);
133 VERIFY_ARE_EQUAL(L"\x1b[32m", Format::Fg::Green);
134 VERIFY_ARE_EQUAL(L"\x1b[33m", Format::Fg::Yellow);
135 VERIFY_ARE_EQUAL(L"\x1b[34m", Format::Fg::Blue);
136 VERIFY_ARE_EQUAL(L"\x1b[35m", Format::Fg::Magenta);
137 VERIFY_ARE_EQUAL(L"\x1b[36m", Format::Fg::Cyan);
138 VERIFY_ARE_EQUAL(L"\x1b[37m", Format::Fg::White);
139
140 VERIFY_ARE_EQUAL(L"\x1b[90m", Format::Fg::BrightBlack);
141 VERIFY_ARE_EQUAL(L"\x1b[91m", Format::Fg::BrightRed);
142 VERIFY_ARE_EQUAL(L"\x1b[92m", Format::Fg::BrightGreen);
143 VERIFY_ARE_EQUAL(L"\x1b[93m", Format::Fg::BrightYellow);
144 VERIFY_ARE_EQUAL(L"\x1b[94m", Format::Fg::BrightBlue);
145 VERIFY_ARE_EQUAL(L"\x1b[95m", Format::Fg::BrightMagenta);
146 VERIFY_ARE_EQUAL(L"\x1b[96m", Format::Fg::BrightCyan);
147 VERIFY_ARE_EQUAL(L"\x1b[97m", Format::Fg::BrightWhite);
148
149 VERIFY_ARE_EQUAL(L"\x1b[38;2;255;128;0m", Format::Fg::Extended(Format::Color{255, 128, 0}));
150
151 VERIFY_ARE_EQUAL(L"\x1b[40m", Format::Bg::Black);
152 VERIFY_ARE_EQUAL(L"\x1b[41m", Format::Bg::Red);
153 VERIFY_ARE_EQUAL(L"\x1b[42m", Format::Bg::Green);
154 VERIFY_ARE_EQUAL(L"\x1b[43m", Format::Bg::Yellow);
155 VERIFY_ARE_EQUAL(L"\x1b[44m", Format::Bg::Blue);
156 VERIFY_ARE_EQUAL(L"\x1b[45m", Format::Bg::Magenta);
157 VERIFY_ARE_EQUAL(L"\x1b[46m", Format::Bg::Cyan);
158 VERIFY_ARE_EQUAL(L"\x1b[47m", Format::Bg::White);
159
160 VERIFY_ARE_EQUAL(L"\x1b[100m", Format::Bg::BrightBlack);
161 VERIFY_ARE_EQUAL(L"\x1b[101m", Format::Bg::BrightRed);
162 VERIFY_ARE_EQUAL(L"\x1b[102m", Format::Bg::BrightGreen);
163 VERIFY_ARE_EQUAL(L"\x1b[103m", Format::Bg::BrightYellow);
164 VERIFY_ARE_EQUAL(L"\x1b[104m", Format::Bg::BrightBlue);
165 VERIFY_ARE_EQUAL(L"\x1b[105m", Format::Bg::BrightMagenta);
166 VERIFY_ARE_EQUAL(L"\x1b[106m", Format::Bg::BrightCyan);
167 VERIFY_ARE_EQUAL(L"\x1b[107m", Format::Bg::BrightWhite);
168
169 VERIFY_ARE_EQUAL(L"\x1b[48;2;0;64;192m", Format::Bg::Extended(Format::Color{0, 64, 192}));
170
171 VERIFY_ARE_EQUAL(
172 L"\x1b]8;;https://example.com\x1b\\Click here\x1b]8;;\x1b\\",
173 Format::Hyperlink(L"Click here", L"https://example.com"));
174 }
175
176 TEST_METHOD(VT_EraseSequences)
177 {
178 VERIFY_ARE_EQUAL(L"\x1b[K", Erase::LineForward);
179 VERIFY_ARE_EQUAL(L"\x1b[1K", Erase::LineBackward);
180 VERIFY_ARE_EQUAL(L"\x1b[2K", Erase::LineEntirely);
181 VERIFY_ARE_EQUAL(L"\x1b[J", Erase::ScreenForward);
182 VERIFY_ARE_EQUAL(L"\x1b[1J", Erase::ScreenBackward);
183 VERIFY_ARE_EQUAL(L"\x1b[2J", Erase::ScreenEntirely);
184 }
185
186 TEST_METHOD(VT_ProgressSequences)
187 {
188 VERIFY_ARE_EQUAL(L"\x1b]9;4;0;0\x1b\\", Progress::Construct(Progress::State::None));
189 VERIFY_ARE_EQUAL(L"\x1b]9;4;3;0\x1b\\", Progress::Construct(Progress::State::Indeterminate));
190 VERIFY_ARE_EQUAL(L"\x1b]9;4;1;50\x1b\\", Progress::Construct(Progress::State::Normal, 50u));
191 VERIFY_ARE_EQUAL(L"\x1b]9;4;2;75\x1b\\", Progress::Construct(Progress::State::Error, 75u));
192 VERIFY_ARE_EQUAL(L"\x1b]9;4;4;25\x1b\\", Progress::Construct(Progress::State::Paused, 25u));
193
194 VERIFY_THROWS_SPECIFIC(Progress::Construct(Progress::State::Normal), wil::ResultException, [](const wil::ResultException& e) {
195 return e.GetErrorCode() == E_INVALIDARG;
196 });
197 VERIFY_THROWS_SPECIFIC(Progress::Construct(Progress::State::Normal, 101u), wil::ResultException, [](const wil::ResultException& e) {
198 return e.GetErrorCode() == E_BOUNDS;
199 });
200 }
201
202 TEST_METHOD(VT_IsColor)
203 {
204 // Named SGR sequences are color.
205 VERIFY_IS_TRUE(Format::Bright.IsColor());
206 VERIFY_IS_TRUE(Format::Dim.IsColor());
207 VERIFY_IS_TRUE(Format::Fg::BrightRed.IsColor());
208 VERIFY_IS_TRUE(Format::Default.IsColor());
209
210 // Constructed multi-param SGR is color.
211 VERIFY_IS_TRUE(Sgr({1, 31}).IsColor());
212
213 // OSC 8 hyperlink is color-adjacent.
214 VERIFY_IS_TRUE(Format::Hyperlink(L"text", L"https://example.com").IsColor());
215
216 // Cursor movement is structural — not color.
217 VERIFY_IS_FALSE(Cursor::Up(1).IsColor());
218 VERIFY_IS_FALSE(Cursor::Home.IsColor());
219
220 // Erase is structural — not color.
221 VERIFY_IS_FALSE(Erase::LineForward.IsColor());
222 VERIFY_IS_FALSE(Erase::ScreenForward.IsColor());
223
224 // Progress is structural — not color.
225 VERIFY_IS_FALSE(Progress::Construct(Progress::State::Normal, 50u).IsColor());
226 }
227
228 TEST_METHOD(VT_StringConcatenation)
229 {
230 // Sequence + Sequence
231 VERIFY_ARE_EQUAL(std::wstring{L"\x1b[1m\x1b[0m"}, Format::Bright + Format::Default);
232
233 // Sequence + string literal
234 VERIFY_ARE_EQUAL(std::wstring{L"\x1b[1mhello"}, Format::Bright + L"hello");
235
236 // string literal + Sequence
237 VERIFY_ARE_EQUAL(std::wstring{L"hello\x1b[0m"}, L"hello" + Format::Default);
238
239 // Sequence + std::wstring
240 VERIFY_ARE_EQUAL(std::wstring{L"\x1b[1mhello"}, Format::Bright + std::wstring{L"hello"});
241
242 // std::wstring + Sequence
243 VERIFY_ARE_EQUAL(std::wstring{L"world\x1b[0m"}, std::wstring{L"world"} + Format::Default);
244
245 // Chained: Sequence + Sequence + literal — verifies operator+ associativity.
246 VERIFY_ARE_EQUAL(std::wstring{L"\x1b[?2004h\x1b[91mroot@ "}, Cursor::BracketedPasteOn + Format::Fg::BrightRed + L"root@ ");
247
248 // In-place wide append.
249 std::wstring buf;
250 buf += Format::Default;
251 buf += std::wstring{L"world"};
252 VERIFY_ARE_EQUAL(std::wstring{L"\x1b[0mworld"}, buf);
253 }
254
255 TEST_METHOD(VT_ChangeTerminalMode)
256 {
257 auto buffer = MakeScreenBuffer();
258 HANDLE h = buffer.get();
259
260 // Capture the original cursor visibility.
261 CONSOLE_CURSOR_INFO original{};
262 VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleCursorInfo(h, &original));
263
264 {
265 // Hide the cursor.
266 ChangeTerminalMode hide{h, false};
267 VERIFY_IS_TRUE(hide.IsConsole());
268
269 CONSOLE_CURSOR_INFO info{};
270 VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleCursorInfo(h, &info));
271 VERIFY_IS_FALSE(!!info.bVisible);
272 }
273
274 // Destructor must restore the original visibility.
275 CONSOLE_CURSOR_INFO restored{};
276 VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleCursorInfo(h, &restored));
277 VERIFY_ARE_EQUAL(original.bVisible, restored.bVisible);
278
279 {
280 // Show the cursor explicitly.
281 ChangeTerminalMode show{h, true};
282 CONSOLE_CURSOR_INFO info{};
283 VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleCursorInfo(h, &info));
284 VERIFY_IS_TRUE(!!info.bVisible);
285 }
286
287 // Non-console handle (a pipe) is silently ignored — IsConsole() returns false.
288 wil::unique_handle readPipe, writePipe;
289 VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&readPipe, &writePipe, nullptr, 0));
290 ChangeTerminalMode nonConsole{readPipe.get(), false};
291 VERIFY_IS_FALSE(nonConsole.IsConsole());
292 }
293
294 TEST_METHOD(VT_ChangeTerminalMode_RedirectedHandles)
295 {
296 wil::unique_hfile file{
297 CreateFileW(L"NUL", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr)};
298 VERIFY_IS_TRUE(!!file);
299 {
300 ChangeTerminalMode mode{file.get(), false};
301 VERIFY_IS_FALSE(mode.IsConsole());
302 }
303
304 // NULL handle.
305 {
306 ChangeTerminalMode mode{nullptr, false};
307 VERIFY_IS_FALSE(mode.IsConsole());
308 }
309
310 // INVALID_HANDLE_VALUE.
311 {
312 ChangeTerminalMode mode{INVALID_HANDLE_VALUE, false};
313 VERIFY_IS_FALSE(mode.IsConsole());
314 }
315 }
316
317 TEST_METHOD(VT_EnableVirtualTerminal)
318 {
319 auto buffer = MakeScreenBuffer();
320 HANDLE h = buffer.get();
321
322 DWORD baseline{};
323 VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &baseline));
324 VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(h, baseline & ~ENABLE_VIRTUAL_TERMINAL_PROCESSING));
325
326 {
327 EnableVirtualTerminal vt{h};
328 VERIFY_IS_TRUE(vt.IsVTEnabled());
329
330 DWORD mode{};
331 VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &mode));
332 VERIFY_IS_TRUE(!!(mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING));
333 }
334
335 DWORD restored{};
336 VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &restored));
337 VERIFY_IS_FALSE(!!(restored & ENABLE_VIRTUAL_TERMINAL_PROCESSING));
338
339 // With DISABLE_NEWLINE_AUTO_RETURN requested.
340 {
341 EnableVirtualTerminal vtWithNewline{h, EnableVirtualTerminal::Mode::Output, true};
342 VERIFY_IS_TRUE(vtWithNewline.IsVTEnabled());
343
344 DWORD mode{};
345 VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &mode));
346 VERIFY_IS_TRUE(!!(mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING));
347 }
348
349 // Non-console handles are silently ignored for both modes.
350 wil::unique_handle readPipe, writePipe;
351 VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&readPipe, &writePipe, nullptr, 0));
352 VERIFY_IS_FALSE(EnableVirtualTerminal(readPipe.get(), EnableVirtualTerminal::Mode::Output).IsVTEnabled());
353 VERIFY_IS_FALSE(EnableVirtualTerminal(readPipe.get(), EnableVirtualTerminal::Mode::Input).IsVTEnabled());
354 }
355
356 TEST_METHOD(VT_EnableVirtualTerminal_InputMode)
357 {
358 // CONIN$ requires an attached console. CI environments that run without one
359 // (e.g. headless agents) cannot exercise this path; skip rather than fail.
360 wil::unique_hfile conin{CreateFileW(
361 L"CONIN$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr)};
362 if (!conin)
363 {
364 LogSkipped("Skipping input-mode VT test: CONIN$ is not available (no attached console)");
365 return;
366 }
367
368 DWORD inputBaseline{};
369 VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(conin.get(), &inputBaseline));
370 {
371 EnableVirtualTerminal vt{conin.get(), EnableVirtualTerminal::Mode::Input};
372 VERIFY_IS_TRUE(vt.IsVTEnabled());
373
374 DWORD mode{};
375 VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(conin.get(), &mode));
376 VERIFY_IS_TRUE(!!(mode & ENABLE_VIRTUAL_TERMINAL_INPUT));
377 VERIFY_IS_FALSE(!!(mode & ENABLE_LINE_INPUT));
378 VERIFY_IS_FALSE(!!(mode & ENABLE_ECHO_INPUT));
379 }
380
381 DWORD inputRestored{};
382 VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(conin.get(), &inputRestored));
383 VERIFY_ARE_EQUAL(inputBaseline, inputRestored);
384 }
385
386 TEST_METHOD(VT_EnableVirtualTerminal_RedirectedHandles)
387 {
388 wil::unique_hfile file{
389 CreateFileW(L"NUL", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr)};
390 VERIFY_IS_TRUE(!!file);
391
392 VERIFY_IS_FALSE(EnableVirtualTerminal(file.get(), EnableVirtualTerminal::Mode::Output).IsVTEnabled());
393 VERIFY_IS_FALSE(EnableVirtualTerminal(file.get(), EnableVirtualTerminal::Mode::Input).IsVTEnabled());
394 VERIFY_IS_FALSE(EnableVirtualTerminal(nullptr, EnableVirtualTerminal::Mode::Output).IsVTEnabled());
395 VERIFY_IS_FALSE(EnableVirtualTerminal(nullptr, EnableVirtualTerminal::Mode::Input).IsVTEnabled());
396 VERIFY_IS_FALSE(EnableVirtualTerminal(INVALID_HANDLE_VALUE, EnableVirtualTerminal::Mode::Output).IsVTEnabled());
397 VERIFY_IS_FALSE(EnableVirtualTerminal(INVALID_HANDLE_VALUE, EnableVirtualTerminal::Mode::Input).IsVTEnabled());
398 }
399
400 TEST_METHOD(VT_EnableVirtualTerminal_AlreadyEnabled_Output)
401 {
402 // Regression: when VT_PROC is already set on the console, EnableVirtualTerminal
403 // must report VT as enabled (so callers gate VT output correctly) AND must not
404 // claim restore ownership — destruction must leave the pre-existing mode alone.
405 auto buffer = MakeScreenBuffer();
406 HANDLE h = buffer.get();
407
408 DWORD baseline{};
409 VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &baseline));
410 const DWORD scopeExitRestore = baseline;
411 auto restoreBaseline = wil::scope_exit([&] { ::SetConsoleMode(h, scopeExitRestore); });
412
413 // Pre-enable VT processing so the constructor's tryEnable() short-circuits
414 // on the "flags already set" path.
415 const DWORD preEnabled = baseline | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
416 VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(h, preEnabled));
417
418 {
419 EnableVirtualTerminal vt{h};
420 VERIFY_IS_TRUE(vt.IsVTEnabled(), L"VT must be reported as enabled when it was already enabled on the handle");
421
422 DWORD mode{};
423 VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &mode));
424 VERIFY_ARE_EQUAL(preEnabled, mode, L"Constructor must not change the mode when the requested flags are already set");
425 }
426
427 // Destructor must not have restored anything — the pre-existing VT_PROC
428 // bit must still be set after the EnableVirtualTerminal instance goes
429 // out of scope.
430 DWORD afterScope{};
431 VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &afterScope));
432 VERIFY_ARE_EQUAL(preEnabled, afterScope, L"Destructor must not restore mode when constructor did not change it");
433 }
434
435 TEST_METHOD(VT_EnableVirtualTerminal_AlreadyEnabled_OutputWithDisableNewlineAutoReturn)
436 {
437 // Same regression as above, exercising the disableNewlineAutoReturn=true path
438 // where the constructor tries (VT_PROC | DISABLE_NEWLINE_AUTO_RETURN) first.
439 auto buffer = MakeScreenBuffer();
440 HANDLE h = buffer.get();
441
442 DWORD baseline{};
443 VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &baseline));
444 const DWORD scopeExitRestore = baseline;
445 auto restoreBaseline = wil::scope_exit([&] { ::SetConsoleMode(h, scopeExitRestore); });
446
447 const DWORD preEnabled = baseline | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
448 if (!SetConsoleMode(h, preEnabled))
449 {
450 // DISABLE_NEWLINE_AUTO_RETURN is not supported on all conhost builds; skip
451 // rather than fail in environments where it cannot be set.
452 LogSkipped("Skipping DISABLE_NEWLINE_AUTO_RETURN test: SetConsoleMode rejected the flag");
453 return;
454 }
455
456 {
457 EnableVirtualTerminal vt{h, EnableVirtualTerminal::Mode::Output, true};
458 VERIFY_IS_TRUE(vt.IsVTEnabled());
459
460 DWORD mode{};
461 VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &mode));
462 VERIFY_ARE_EQUAL(preEnabled, mode);
463 }
464
465 DWORD afterScope{};
466 VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(h, &afterScope));
467 VERIFY_ARE_EQUAL(preEnabled, afterScope);
468 }
469
470 TEST_METHOD(VT_EnableVirtualTerminal_AlreadyEnabled_Input)
471 {
472 // Regression: when CONIN$ is already in the exact target mode
473 // (ENABLE_VIRTUAL_TERMINAL_INPUT + ENABLE_EXTENDED_FLAGS, no ENABLE_LINE_INPUT,
474 // no ENABLE_ECHO_INPUT), the constructor's "no change needed" early-return
475 // must still report IsVTEnabled()==true and must not touch the mode.
476 wil::unique_hfile conin{CreateFileW(
477 L"CONIN$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr)};
478 if (!conin)
479 {
480 LogSkipped("Skipping input-mode VT already-enabled test: CONIN$ is not available (no attached console)");
481 return;
482 }
483
484 DWORD baseline{};
485 VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(conin.get(), &baseline));
486 const DWORD scopeExitRestore = baseline;
487 auto restoreBaseline = wil::scope_exit([&] { ::SetConsoleMode(conin.get(), scopeExitRestore); });
488
489 // Pre-configure CONIN$ to exactly match what EnableVirtualTerminal would set,
490 // so the constructor takes the newMode == current early-return path.
491 const DWORD preEnabled = (baseline & ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT)) | ENABLE_EXTENDED_FLAGS | ENABLE_VIRTUAL_TERMINAL_INPUT;
492 VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(conin.get(), preEnabled));
493
494 {
495 EnableVirtualTerminal vt{conin.get(), EnableVirtualTerminal::Mode::Input};
496 VERIFY_IS_TRUE(vt.IsVTEnabled(), L"VT input must be reported as enabled when CONIN$ was already in the target mode");
497
498 DWORD mode{};
499 VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(conin.get(), &mode));
500 VERIFY_ARE_EQUAL(preEnabled, mode, L"Constructor must not change input mode when it already matches the target");
501 }
502
503 DWORD afterScope{};
504 VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleMode(conin.get(), &afterScope));
505 VERIFY_ARE_EQUAL(preEnabled, afterScope, L"Destructor must not restore input mode when constructor did not change it");
506 }
507
508 TEST_METHOD(VT_PrimaryDeviceAttributes)
509 {
510 // Clean DA1 response: conformance level 62, extensions Columns132 (1) and Sixel (4).
511 {
512 std::wostringstream out;
513 std::wistringstream in{L"\x1b[?62;1;4c"};
514 PrimaryDeviceAttributes da{out, in};
515
516 VERIFY_IS_TRUE(da.Supports(PrimaryDeviceAttributes::Extension::Columns132));
517 VERIFY_IS_TRUE(da.Supports(PrimaryDeviceAttributes::Extension::Sixel));
518 VERIFY_IS_FALSE(da.Supports(PrimaryDeviceAttributes::Extension::PrinterPort));
519 // DA1 request must have been written to the output stream.
520 VERIFY_ARE_EQUAL(std::wstring{L"\x1b[0c"}, out.str());
521 }
522
523 // Trailing plain text (e.g. queued user input) must not break parsing.
524 {
525 std::wostringstream out;
526 std::wistringstream in{L"\x1b[?62;1;4chello"};
527 PrimaryDeviceAttributes da{out, in};
528
529 VERIFY_IS_TRUE(da.Supports(PrimaryDeviceAttributes::Extension::Columns132));
530 VERIFY_IS_TRUE(da.Supports(PrimaryDeviceAttributes::Extension::Sixel));
531 }
532
533 // Trailing VT sequence must not corrupt suffix search or result extraction.
534 {
535 std::wostringstream out;
536 std::wistringstream in{L"\x1b[?62;6c\x1b[0m"};
537 PrimaryDeviceAttributes da{out, in};
538
539 VERIFY_IS_TRUE(da.Supports(PrimaryDeviceAttributes::Extension::SelectiveErase));
540 VERIFY_IS_FALSE(da.Supports(PrimaryDeviceAttributes::Extension::Columns132));
541 }
542
543 // Empty/malformed response — should not throw, extensions remain unset.
544 {
545 std::wostringstream out;
546 std::wistringstream in{L""};
547 PrimaryDeviceAttributes da{out, in};
548
549 VERIFY_IS_FALSE(da.Supports(PrimaryDeviceAttributes::Extension::Columns132));
550 }
551 }
552
553 TEST_METHOD(VT_PrimaryDeviceAttributes_Empty)
554 {
555 // Empty/malformed response — should not throw, extensions remain unset.
556 std::wostringstream out;
557 std::wistringstream in{L""};
558
559 PrimaryDeviceAttributes da{out, in};
560
561 VERIFY_IS_FALSE(da.Supports(PrimaryDeviceAttributes::Extension::Columns132));
562 }
563 };
564
565 } // namespace WSLCCLIVTSupportUnitTests