//===-- 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/Core/Error.h" #include "lldb/Core/StringList.h" #include "lldb/Host/Editline.h" #include "lldb/Host/Pipe.h" #include "lldb/Utility/PseudoTerminal.h" 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; lldb_utility::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::Error 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