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