1//===-- EditlineTest.cpp --------------------------------------------------===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8
9#include "lldb/Host/Config.h"
10
11#if LLDB_ENABLE_LIBEDIT
12
13#define EDITLINE_TEST_DUMP_OUTPUT 0
14
15#include <stdio.h>
16#include <unistd.h>
17
18#include "gmock/gmock.h"
19#include "gtest/gtest.h"
20#include <memory>
21#include <thread>
22
23#include "TestingSupport/SubsystemRAII.h"
24#include "lldb/Host/Editline.h"
25#include "lldb/Host/FileSystem.h"
26#include "lldb/Host/Pipe.h"
27#include "lldb/Host/PseudoTerminal.h"
28#include "lldb/Utility/Status.h"
29#include "lldb/Utility/StringList.h"
30
31using namespace lldb_private;
32
33namespace {
34const size_t TIMEOUT_MILLIS = 5000;
35}
36
37class FilePointer {
38public:
39 FilePointer() = delete;
40
41 FilePointer(const FilePointer &) = delete;
42
43 FilePointer(FILE *file_p) : _file_p(file_p) {}
44
45 ~FilePointer() {
46 if (_file_p != nullptr) {
47 const int close_result = fclose(stream: _file_p);
48 EXPECT_EQ(0, close_result);
49 }
50 }
51
52 operator FILE *() { return _file_p; }
53
54private:
55 FILE *_file_p;
56};
57
58/**
59 Wraps an Editline class, providing a simple way to feed
60 input (as if from the keyboard) and receive output from Editline.
61 */
62class EditlineAdapter {
63public:
64 EditlineAdapter();
65
66 void CloseInput();
67
68 bool IsValid() const { return _editline_sp != nullptr; }
69
70 lldb_private::Editline &GetEditline() { return *_editline_sp; }
71
72 bool SendLine(const std::string &line);
73
74 bool SendLines(const std::vector<std::string> &lines);
75
76 bool GetLine(std::string &line, bool &interrupted, size_t timeout_millis);
77
78 bool GetLines(lldb_private::StringList &lines, bool &interrupted,
79 size_t timeout_millis);
80
81 void ConsumeAllOutput();
82
83private:
84 bool IsInputComplete(lldb_private::Editline *editline,
85 lldb_private::StringList &lines);
86
87 std::recursive_mutex output_mutex;
88 std::unique_ptr<lldb_private::Editline> _editline_sp;
89
90 PseudoTerminal _pty;
91 int _pty_primary_fd = -1;
92 int _pty_secondary_fd = -1;
93
94 std::unique_ptr<FilePointer> _el_secondary_file;
95};
96
97EditlineAdapter::EditlineAdapter()
98 : _editline_sp(), _pty(), _el_secondary_file() {
99 lldb_private::Status error;
100
101 // Open the first primary pty available.
102 EXPECT_THAT_ERROR(_pty.OpenFirstAvailablePrimary(O_RDWR), llvm::Succeeded());
103
104 // Grab the primary fd. This is a file descriptor we will:
105 // (1) write to when we want to send input to editline.
106 // (2) read from when we want to see what editline sends back.
107 _pty_primary_fd = _pty.GetPrimaryFileDescriptor();
108
109 // Open the corresponding secondary pty.
110 EXPECT_THAT_ERROR(_pty.OpenSecondary(O_RDWR), llvm::Succeeded());
111 _pty_secondary_fd = _pty.GetSecondaryFileDescriptor();
112
113 _el_secondary_file.reset(p: new FilePointer(fdopen(fd: _pty_secondary_fd, modes: "rw")));
114 EXPECT_FALSE(nullptr == *_el_secondary_file);
115 if (*_el_secondary_file == nullptr)
116 return;
117
118 // Create an Editline instance.
119 _editline_sp.reset(p: new lldb_private::Editline(
120 "gtest editor", *_el_secondary_file, *_el_secondary_file,
121 *_el_secondary_file, output_mutex));
122 _editline_sp->SetPrompt("> ");
123
124 // Hookup our input complete callback.
125 auto input_complete_cb = [this](Editline *editline, StringList &lines) {
126 return this->IsInputComplete(editline, lines);
127 };
128 _editline_sp->SetIsInputCompleteCallback(input_complete_cb);
129}
130
131void EditlineAdapter::CloseInput() {
132 if (_el_secondary_file != nullptr)
133 _el_secondary_file.reset(p: nullptr);
134}
135
136bool EditlineAdapter::SendLine(const std::string &line) {
137 // Ensure we're valid before proceeding.
138 if (!IsValid())
139 return false;
140
141 // Write the line out to the pipe connected to editline's input.
142 ssize_t input_bytes_written =
143 ::write(fd: _pty_primary_fd, buf: line.c_str(),
144 n: line.length() * sizeof(std::string::value_type));
145
146 const char *eoln = "\n";
147 const size_t eoln_length = strlen(s: eoln);
148 input_bytes_written =
149 ::write(fd: _pty_primary_fd, buf: eoln, n: eoln_length * sizeof(char));
150
151 EXPECT_NE(-1, input_bytes_written) << strerror(errno);
152 EXPECT_EQ(eoln_length * sizeof(char), size_t(input_bytes_written));
153 return eoln_length * sizeof(char) == size_t(input_bytes_written);
154}
155
156bool EditlineAdapter::SendLines(const std::vector<std::string> &lines) {
157 for (auto &line : lines) {
158#if EDITLINE_TEST_DUMP_OUTPUT
159 printf("<stdin> sending line \"%s\"\n", line.c_str());
160#endif
161 if (!SendLine(line))
162 return false;
163 }
164 return true;
165}
166
167// We ignore the timeout for now.
168bool EditlineAdapter::GetLine(std::string &line, bool &interrupted,
169 size_t /* timeout_millis */) {
170 // Ensure we're valid before proceeding.
171 if (!IsValid())
172 return false;
173
174 _editline_sp->GetLine(line, interrupted);
175 return true;
176}
177
178bool EditlineAdapter::GetLines(lldb_private::StringList &lines,
179 bool &interrupted, size_t /* timeout_millis */) {
180 // Ensure we're valid before proceeding.
181 if (!IsValid())
182 return false;
183
184 _editline_sp->GetLines(first_line_number: 1, lines, interrupted);
185 return true;
186}
187
188bool EditlineAdapter::IsInputComplete(lldb_private::Editline *editline,
189 lldb_private::StringList &lines) {
190 // We'll call ourselves complete if we've received a balanced set of braces.
191 int start_block_count = 0;
192 int brace_balance = 0;
193
194 for (const std::string &line : lines) {
195 for (auto ch : line) {
196 if (ch == '{') {
197 ++start_block_count;
198 ++brace_balance;
199 } else if (ch == '}')
200 --brace_balance;
201 }
202 }
203
204 return (start_block_count > 0) && (brace_balance == 0);
205}
206
207void EditlineAdapter::ConsumeAllOutput() {
208 FilePointer output_file(fdopen(fd: _pty_primary_fd, modes: "r"));
209
210 int ch;
211 while ((ch = fgetc(stream: output_file)) != EOF) {
212#if EDITLINE_TEST_DUMP_OUTPUT
213 char display_str[] = {0, 0, 0};
214 switch (ch) {
215 case '\t':
216 display_str[0] = '\\';
217 display_str[1] = 't';
218 break;
219 case '\n':
220 display_str[0] = '\\';
221 display_str[1] = 'n';
222 break;
223 case '\r':
224 display_str[0] = '\\';
225 display_str[1] = 'r';
226 break;
227 default:
228 display_str[0] = ch;
229 break;
230 }
231 printf("<stdout> 0x%02x (%03d) (%s)\n", ch, ch, display_str);
232// putc(ch, stdout);
233#endif
234 }
235}
236
237class EditlineTestFixture : public ::testing::Test {
238 SubsystemRAII<FileSystem> subsystems;
239 EditlineAdapter _el_adapter;
240 std::shared_ptr<std::thread> _sp_output_thread;
241
242public:
243 static void SetUpTestCase() {
244 // We need a TERM set properly for editline to work as expected.
245 setenv(name: "TERM", value: "vt100", replace: 1);
246 }
247
248 void SetUp() override {
249 // Validate the editline adapter.
250 EXPECT_TRUE(_el_adapter.IsValid());
251 if (!_el_adapter.IsValid())
252 return;
253
254 // Dump output.
255 _sp_output_thread =
256 std::make_shared<std::thread>(args: [&] { _el_adapter.ConsumeAllOutput(); });
257 }
258
259 void TearDown() override {
260 _el_adapter.CloseInput();
261 if (_sp_output_thread)
262 _sp_output_thread->join();
263 }
264
265 EditlineAdapter &GetEditlineAdapter() { return _el_adapter; }
266};
267
268TEST_F(EditlineTestFixture, EditlineReceivesSingleLineText) {
269 // Send it some text via our virtual keyboard.
270 const std::string input_text("Hello, world");
271 EXPECT_TRUE(GetEditlineAdapter().SendLine(input_text));
272
273 // Verify editline sees what we put in.
274 std::string el_reported_line;
275 bool input_interrupted = false;
276 const bool received_line = GetEditlineAdapter().GetLine(
277 line&: el_reported_line, interrupted&: input_interrupted, TIMEOUT_MILLIS);
278
279 EXPECT_TRUE(received_line);
280 EXPECT_FALSE(input_interrupted);
281 EXPECT_EQ(input_text, el_reported_line);
282}
283
284TEST_F(EditlineTestFixture, EditlineReceivesMultiLineText) {
285 // Send it some text via our virtual keyboard.
286 std::vector<std::string> input_lines;
287 input_lines.push_back(x: "int foo()");
288 input_lines.push_back(x: "{");
289 input_lines.push_back(x: "printf(\"Hello, world\");");
290 input_lines.push_back(x: "}");
291 input_lines.push_back(x: "");
292
293 EXPECT_TRUE(GetEditlineAdapter().SendLines(input_lines));
294
295 // Verify editline sees what we put in.
296 lldb_private::StringList el_reported_lines;
297 bool input_interrupted = false;
298
299 EXPECT_TRUE(GetEditlineAdapter().GetLines(el_reported_lines,
300 input_interrupted, TIMEOUT_MILLIS));
301 EXPECT_FALSE(input_interrupted);
302
303 // Without any auto indentation support, our output should directly match our
304 // input.
305 std::vector<std::string> reported_lines;
306 for (const std::string &line : el_reported_lines)
307 reported_lines.push_back(x: line);
308
309 EXPECT_THAT(reported_lines, testing::ContainerEq(input_lines));
310}
311
312#endif
313

source code of lldb/unittests/Editline/EditlineTest.cpp