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

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

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