//===-- EditlineTest.cpp ----------------------------------------*- C++ -*-===// // // The LLVM Compiler Infrastructure // // This file is distributed under the University of Illinois Open Source // License. See LICENSE.TXT for details. // //===----------------------------------------------------------------------===// #ifndef LLDB_DISABLE_LIBEDIT #define EDITLINE_TEST_DUMP_OUTPUT 0 #include #include #include #include #include "gtest/gtest.h" #include "lldb/Host/Editline.h" #include "lldb/Host/Pipe.h" #include "lldb/Host/PseudoTerminal.h" #include "lldb/Utility/Status.h" #include "lldb/Utility/StringList.h" using namespace lldb_private; namespace { const size_t TIMEOUT_MILLIS = 5000; } class FilePointer { public: FilePointer() = delete; FilePointer(const FilePointer &) = delete; FilePointer(FILE *file_p) : _file_p(file_p) {} ~FilePointer() { if (_file_p != nullptr) { const int close_result = fclose(_file_p); EXPECT_EQ(0, close_result); } } operator FILE *() { return _file_p; } private: FILE *_file_p; }; /** Wraps an Editline class, providing a simple way to feed input (as if from the keyboard) and receive output from Editline. */ class EditlineAdapter { public: EditlineAdapter(); void CloseInput(); bool IsValid() const { return _editline_sp.get() != nullptr; } lldb_private::Editline &GetEditline() { return *_editline_sp; } bool SendLine(const std::string &line); bool SendLines(const std::vector &lines); bool GetLine(std::string &line, bool &interrupted, size_t timeout_millis); bool GetLines(lldb_private::StringList &lines, bool &interrupted, size_t timeout_millis); void ConsumeAllOutput(); private: static bool IsInputComplete(lldb_private::Editline *editline, lldb_private::StringList &lines, void *baton); std::unique_ptr _editline_sp; PseudoTerminal _pty; int _pty_master_fd; int _pty_slave_fd; std::unique_ptr _el_slave_file; }; EditlineAdapter::EditlineAdapter() : _editline_sp(), _pty(), _pty_master_fd(-1), _pty_slave_fd(-1), _el_slave_file() { lldb_private::Status error; // Open the first master pty available. char error_string[256]; error_string[0] = '\0'; if (!_pty.OpenFirstAvailableMaster(O_RDWR, error_string, sizeof(error_string))) { fprintf(stderr, "failed to open first available master pty: '%s'\n", error_string); return; } // Grab the master fd. This is a file descriptor we will: // (1) write to when we want to send input to editline. // (2) read from when we want to see what editline sends back. _pty_master_fd = _pty.GetMasterFileDescriptor(); // Open the corresponding slave pty. if (!_pty.OpenSlave(O_RDWR, error_string, sizeof(error_string))) { fprintf(stderr, "failed to open slave pty: '%s'\n", error_string); return; } _pty_slave_fd = _pty.GetSlaveFileDescriptor(); _el_slave_file.reset(new FilePointer(fdopen(_pty_slave_fd, "rw"))); EXPECT_FALSE(nullptr == *_el_slave_file); if (*_el_slave_file == nullptr) return; // Create an Editline instance. _editline_sp.reset(new lldb_private::Editline("gtest editor", *_el_slave_file, *_el_slave_file, *_el_slave_file, false)); _editline_sp->SetPrompt("> "); // Hookup our input complete callback. _editline_sp->SetIsInputCompleteCallback(IsInputComplete, this); } void EditlineAdapter::CloseInput() { if (_el_slave_file != nullptr) _el_slave_file.reset(nullptr); } bool EditlineAdapter::SendLine(const std::string &line) { // Ensure we're valid before proceeding. if (!IsValid()) return false; // Write the line out to the pipe connected to editline's input. ssize_t input_bytes_written = ::write(_pty_master_fd, line.c_str(), line.length() * sizeof(std::string::value_type)); const char *eoln = "\n"; const size_t eoln_length = strlen(eoln); input_bytes_written = ::write(_pty_master_fd, eoln, eoln_length * sizeof(char)); EXPECT_NE(-1, input_bytes_written) << strerror(errno); EXPECT_EQ(eoln_length * sizeof(char), size_t(input_bytes_written)); return eoln_length * sizeof(char) == size_t(input_bytes_written); } bool EditlineAdapter::SendLines(const std::vector &lines) { for (auto &line : lines) { #if EDITLINE_TEST_DUMP_OUTPUT printf(" sending line \"%s\"\n", line.c_str()); #endif if (!SendLine(line)) return false; } return true; } // We ignore the timeout for now. bool EditlineAdapter::GetLine(std::string &line, bool &interrupted, size_t /* timeout_millis */) { // Ensure we're valid before proceeding. if (!IsValid()) return false; _editline_sp->GetLine(line, interrupted); return true; } bool EditlineAdapter::GetLines(lldb_private::StringList &lines, bool &interrupted, size_t /* timeout_millis */) { // Ensure we're valid before proceeding. if (!IsValid()) return false; _editline_sp->GetLines(1, lines, interrupted); return true; } bool EditlineAdapter::IsInputComplete(lldb_private::Editline *editline, lldb_private::StringList &lines, void *baton) { // We'll call ourselves complete if we've received a balanced set of braces. int start_block_count = 0; int brace_balance = 0; for (size_t i = 0; i < lines.GetSize(); ++i) { for (auto ch : lines[i]) { if (ch == '{') { ++start_block_count; ++brace_balance; } else if (ch == '}') --brace_balance; } } return (start_block_count > 0) && (brace_balance == 0); } void EditlineAdapter::ConsumeAllOutput() { FilePointer output_file(fdopen(_pty_master_fd, "r")); int ch; while ((ch = fgetc(output_file)) != EOF) { #if EDITLINE_TEST_DUMP_OUTPUT char display_str[] = {0, 0, 0}; switch (ch) { case '\t': display_str[0] = '\\'; display_str[1] = 't'; break; case '\n': display_str[0] = '\\'; display_str[1] = 'n'; break; case '\r': display_str[0] = '\\'; display_str[1] = 'r'; break; default: display_str[0] = ch; break; } printf(" 0x%02x (%03d) (%s)\n", ch, ch, display_str); // putc(ch, stdout); #endif } } class EditlineTestFixture : public ::testing::Test { private: EditlineAdapter _el_adapter; std::shared_ptr _sp_output_thread; public: void SetUp() { // We need a TERM set properly for editline to work as expected. setenv("TERM", "vt100", 1); // Validate the editline adapter. EXPECT_TRUE(_el_adapter.IsValid()); if (!_el_adapter.IsValid()) return; // Dump output. _sp_output_thread.reset( new std::thread([&] { _el_adapter.ConsumeAllOutput(); })); } void TearDown() { _el_adapter.CloseInput(); if (_sp_output_thread) _sp_output_thread->join(); } EditlineAdapter &GetEditlineAdapter() { return _el_adapter; } }; TEST_F(EditlineTestFixture, EditlineReceivesSingleLineText) { // Send it some text via our virtual keyboard. const std::string input_text("Hello, world"); EXPECT_TRUE(GetEditlineAdapter().SendLine(input_text)); // Verify editline sees what we put in. std::string el_reported_line; bool input_interrupted = false; const bool received_line = GetEditlineAdapter().GetLine( el_reported_line, input_interrupted, TIMEOUT_MILLIS); EXPECT_TRUE(received_line); EXPECT_FALSE(input_interrupted); EXPECT_EQ(input_text, el_reported_line); } TEST_F(EditlineTestFixture, EditlineReceivesMultiLineText) { // Send it some text via our virtual keyboard. std::vector input_lines; input_lines.push_back("int foo()"); input_lines.push_back("{"); input_lines.push_back("printf(\"Hello, world\");"); input_lines.push_back("}"); input_lines.push_back(""); EXPECT_TRUE(GetEditlineAdapter().SendLines(input_lines)); // Verify editline sees what we put in. lldb_private::StringList el_reported_lines; bool input_interrupted = false; EXPECT_TRUE(GetEditlineAdapter().GetLines(el_reported_lines, input_interrupted, TIMEOUT_MILLIS)); EXPECT_FALSE(input_interrupted); // Without any auto indentation support, our output should directly match our // input. EXPECT_EQ(input_lines.size(), el_reported_lines.GetSize()); if (input_lines.size() == el_reported_lines.GetSize()) { for (size_t i = 0; i < input_lines.size(); ++i) EXPECT_EQ(input_lines[i], el_reported_lines[i]); } } #endif