master
h 277 lines 8.71 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WSLCTestHelpers.h
8
9 Abstract:
10
11 Helper utilities for WSLC CLI unit tests.
12
13 --*/
14
15 #pragma once
16
17 #include <fcntl.h>
18 #include <io.h>
19 #include <algorithm>
20 #include <memory>
21 #include <string>
22 #include <thread>
23 #include <vector>
24 #include <Windows.h>
25 #include <WexTestClass.h>
26 #include <wil/resource.h>
27 #include <wslutil.h>
28 #include "windows/Common.h"
29 #include "Invocation.h"
30 #include "OutputChannel.h"
31 #include "Terminal.h"
32 #include "TableOutput.h"
33
34 namespace WSLCTestHelpers {
35
36 inline wsl::windows::wslc::Invocation CreateInvocationFromCommandLine(const std::wstring& commandLine)
37 {
38 // Simulate creation of Arvc/Argc from command line as Windows does.
39 int argc = 0;
40 wil::unique_hlocal_ptr<LPWSTR[]> argv;
41 argv.reset(CommandLineToArgvW(commandLine.c_str(), &argc));
42 VERIFY_IS_NOT_NULL(argv.get());
43 VERIFY_IS_GREATER_THAN(argc, 0);
44
45 // Convert to vector for Invocation, skipping argv[0] (executable path)
46 // This is what we do in wmain() to populate Invocation input vector.
47 std::vector<std::wstring> args;
48 for (int i = 1; i < argc; ++i) // Skip argv[0]
49 {
50 args.push_back(argv[i]);
51 }
52
53 return wsl::windows::wslc::Invocation(std::move(args));
54 }
55
56 // Helper function to convert wstring to UTF-8 string for TAEF logging
57 inline std::string WStringToUTF8(const std::wstring& wstr)
58 {
59 if (wstr.empty())
60 {
61 return std::string();
62 }
63
64 int size_needed = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), static_cast<int>(wstr.size()), nullptr, 0, nullptr, nullptr);
65 std::string result(size_needed, 0);
66 WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), static_cast<int>(wstr.size()), &result[0], size_needed, nullptr, nullptr);
67 return result;
68 }
69
70 // Convenience wrapper for Log::Comment with wstring
71 inline void LogComment(const std::wstring& message)
72 {
73 WEX::Logging::Log::Comment(reinterpret_cast<const char8_t*>(WStringToUTF8(message).c_str()));
74 }
75
76 // RAII pipe pair for capturing FILE* output in tests.
77 // file() is passed to OutputChannel/Terminal; captured() drains the read end after flush.
78 struct CapturePipe
79 {
80 CapturePipe()
81 {
82 // ReadPipeOverlapped=true so PartialHandleRead's InterruptableRead can be
83 // interrupted by m_exitEvent during teardown if fclose hasn't run yet.
84 auto [r, w] = wsl::windows::common::wslutil::OpenAnonymousPipe(0, true, false);
85 wil::unique_handle writeHandle{w.release()};
86 m_file = FileFromHandle(writeHandle, "w");
87
88 const int fd = _fileno(m_file.get());
89 WI_VERIFY(_setmode(fd, _O_U8TEXT) != -1);
90
91 // Disable CRT buffering so each fwprintf is a single write. Prevents
92 // _O_U8TEXT from splitting VT escape sequences across buffer flushes.
93 setvbuf(m_file.get(), nullptr, _IONBF, 0);
94
95 // CapturePipe owns the read pipe; PartialHandleRead borrows it via .get().
96 // m_readPipe is declared before m_reader so destruction order tears the reader
97 // down first (joining its thread) and only then closes the handle it was reading.
98 m_readPipe = std::move(r);
99 m_reader = std::make_unique<PartialHandleRead>(m_readPipe.get());
100 }
101
102 NON_COPYABLE(CapturePipe);
103 NON_MOVABLE(CapturePipe);
104
105 FILE* file() const
106 {
107 return m_file.get();
108 }
109
110 std::wstring captured()
111 {
112 m_file.reset();
113
114 m_reader->ExpectClosed();
115 std::wstring result = wsl::shared::string::MultiByteToWide(m_reader->GetData());
116
117 // _O_U8TEXT prepends a UTF-8 BOM on some streams; strip it if present.
118 if (!result.empty() && result[0] == L'\xFEFF')
119 {
120 result.erase(0, 1);
121 }
122
123 // _O_U8TEXT translates \n to \r\n; strip \r so tests compare plain newlines.
124 result.erase(std::remove(result.begin(), result.end(), L'\r'), result.end());
125 return result;
126 }
127
128 private:
129 wil::unique_file m_file;
130 wil::unique_hfile m_readPipe;
131 std::unique_ptr<PartialHandleRead> m_reader;
132 };
133
134 // RAII pipe preloaded with input for tests. A background thread feeds the content
135 // (UTF-8) into the pipe while the test drains the read end, mirroring how real stdin
136 // is filled by a separate producer. A synchronous write of the whole content on the
137 // reading thread would deadlock once the content exceeds the pipe buffer
138 // (OpenAnonymousPipe defaults to 4096 bytes), so the feeder runs concurrently and
139 // closes the write end when done, letting file() read the content then hit EOF. The
140 // read FILE* is configured like real stdin (_O_U8TEXT) and passed to
141 // InputChannel/Terminal.
142 struct InputPipe
143 {
144 explicit InputPipe(const std::wstring& content)
145 {
146 auto [r, w] = wsl::windows::common::wslutil::OpenAnonymousPipe(0, false, false);
147
148 wil::unique_handle readHandle{r.release()};
149 m_file = FileFromHandle(readHandle, "r");
150
151 const int fd = _fileno(m_file.get());
152 WI_VERIFY(_setmode(fd, _O_U8TEXT) != -1);
153
154 // Feed the content from a background thread so the reader can drain while the
155 // writer fills. Closing the write end signals EOF. No VERIFY/THROW macros run
156 // here since this executes on a separate thread; a broken pipe (the reader
157 // closed early) simply stops the feed.
158 m_writer = std::thread([writeEnd = std::move(w), utf8 = WStringToUTF8(content)]() mutable {
159 size_t offset = 0;
160 while (offset < utf8.size())
161 {
162 DWORD written = 0;
163 if (!WriteFile(writeEnd.get(), utf8.data() + offset, static_cast<DWORD>(utf8.size() - offset), &written, nullptr))
164 {
165 break;
166 }
167
168 offset += written;
169 }
170
171 writeEnd.reset();
172 });
173 }
174
175 ~InputPipe()
176 {
177 // Close the read end first so a feeder still blocked on a full pipe unblocks
178 // with a broken pipe, then join it.
179 m_file.reset();
180 if (m_writer.joinable())
181 {
182 m_writer.join();
183 }
184 }
185
186 NON_COPYABLE(InputPipe);
187 NON_MOVABLE(InputPipe);
188
189 FILE* file() const
190 {
191 return m_file.get();
192 }
193
194 private:
195 wil::unique_file m_file;
196 std::thread m_writer;
197 };
198
199 // Terminal wired to a single capture pipe for full output capture.
200 // VT is disabled (not a console handle), so error output stays in the same pipe.
201 struct CaptureTerminal
202 {
203 CapturePipe pipe;
204 wsl::windows::wslc::Terminal terminal;
205
206 explicit CaptureTerminal(bool vtEnabled = false) : terminal(pipe.file(), vtEnabled, pipe.file(), vtEnabled)
207 {
208 }
209
210 std::wstring captured()
211 {
212 return pipe.captured();
213 }
214 };
215
216 // Helper: capture all lines emitted by a TableOutput into a vector<wstring>.
217 template <size_t N>
218 struct TableOutputCapture
219 {
220 CaptureTerminal capture;
221 wsl::windows::wslc::TableOutput<N> table;
222
223 // Header + optional config + optional VT flag.
224 explicit TableOutputCapture(
225 typename wsl::windows::wslc::TableOutput<N>::header_t&& header,
226 size_t sizingBuffer = 50,
227 size_t columnPadding = wsl::windows::wslc::TableOutput<N>::DefaultColumnPadding,
228 bool vtEnabled = false) :
229 capture(vtEnabled), table(capture.terminal, std::move(header), sizingBuffer, columnPadding)
230 {
231 table.SetConsoleWidthOverride(120);
232 }
233
234 // Header + column configs + optional VT flag.
235 explicit TableOutputCapture(
236 typename wsl::windows::wslc::TableOutput<N>::header_t&& header,
237 typename wsl::windows::wslc::TableOutput<N>::column_config_t&& configs,
238 bool vtEnabled = false) :
239 capture(vtEnabled),
240 table(capture.terminal, std::move(header), std::move(configs), 50, wsl::windows::wslc::TableOutput<N>::DefaultColumnPadding)
241 {
242 table.SetConsoleWidthOverride(120);
243 }
244
245 // Column definitions.
246 explicit TableOutputCapture(typename wsl::windows::wslc::TableOutput<N>::column_def_t&& defs, bool vtEnabled = false) :
247 capture(vtEnabled), table(capture.terminal, std::move(defs))
248 {
249 table.SetConsoleWidthOverride(120);
250 }
251
252 // Returns captured output split into lines.
253 std::vector<std::wstring> lines()
254 {
255 auto raw = capture.captured();
256 std::vector<std::wstring> result;
257 size_t pos = 0;
258 while (pos < raw.size())
259 {
260 auto nl = raw.find(L'\n', pos);
261 if (nl == std::wstring::npos)
262 {
263 result.emplace_back(raw.substr(pos));
264 break;
265 }
266 result.emplace_back(raw.substr(pos, nl - pos));
267 pos = nl + 1;
268 }
269 // Remove trailing empty entry from final newline.
270 if (!result.empty() && result.back().empty())
271 {
272 result.pop_back();
273 }
274 return result;
275 }
276 };
277 } // namespace WSLCTestHelpers