xref: /llvm-project/lldb/unittests/Editline/EditlineTest.cpp (revision 2841cdbfda92b3fb5fbec726f3376b198106d496)
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 
31 using namespace lldb_private;
32 
33 namespace {
34 const size_t TIMEOUT_MILLIS = 5000;
35 }
36 
37 class FilePointer {
38 public:
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(_file_p);
48       EXPECT_EQ(0, close_result);
49     }
50   }
51 
52   operator FILE *() { return _file_p; }
53 
54 private:
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  */
62 class EditlineAdapter {
63 public:
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 
83 private:
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 
97 EditlineAdapter::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(new FilePointer(fdopen(_pty_secondary_fd, "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(new lldb_private::Editline(
120       "gtest editor", *_el_secondary_file, *_el_secondary_file,
121       *_el_secondary_file, /*color=*/false, 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 
131 void EditlineAdapter::CloseInput() {
132   if (_el_secondary_file != nullptr)
133     _el_secondary_file.reset(nullptr);
134 }
135 
136 bool 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(_pty_primary_fd, line.c_str(),
144               line.length() * sizeof(std::string::value_type));
145 
146   const char *eoln = "\n";
147   const size_t eoln_length = strlen(eoln);
148   input_bytes_written =
149       ::write(_pty_primary_fd, eoln, 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 
156 bool 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.
168 bool 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 
178 bool 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(1, lines, interrupted);
185   return true;
186 }
187 
188 bool 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 
207 void EditlineAdapter::ConsumeAllOutput() {
208   FilePointer output_file(fdopen(_pty_primary_fd, "r"));
209 
210   int ch;
211   while ((ch = fgetc(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 
237 class EditlineTestFixture : public ::testing::Test {
238   SubsystemRAII<FileSystem> subsystems;
239   EditlineAdapter _el_adapter;
240   std::shared_ptr<std::thread> _sp_output_thread;
241 
242 public:
243   static void SetUpTestCase() {
244     // We need a TERM set properly for editline to work as expected.
245     setenv("TERM", "vt100", 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>([&] { _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 
268 TEST_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       el_reported_line, 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 
284 TEST_F(EditlineTestFixture, EditlineReceivesMultiLineText) {
285   // Send it some text via our virtual keyboard.
286   std::vector<std::string> input_lines;
287   input_lines.push_back("int foo()");
288   input_lines.push_back("{");
289   input_lines.push_back("printf(\"Hello, world\");");
290   input_lines.push_back("}");
291   input_lines.push_back("");
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(line);
308 
309   EXPECT_THAT(reported_lines, testing::ContainerEq(input_lines));
310 }
311 
312 #endif
313