//===-- EditlineTest.cpp --------------------------------------------------===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// #include "lldb/Host/Config.h" #include "lldb/Host/File.h" #include "lldb/Host/HostInfo.h" #include "lldb/lldb-forward.h" #include "llvm/Testing/Support/Error.h" #if LLDB_ENABLE_LIBEDIT #define EDITLINE_TEST_DUMP_OUTPUT 0 #include #include #include "gmock/gmock.h" #include "gtest/gtest.h" #include #include #include "TestingSupport/SubsystemRAII.h" #include "lldb/Host/Editline.h" #include "lldb/Host/FileSystem.h" #include "lldb/Host/PseudoTerminal.h" #include "lldb/Host/StreamFile.h" #include "lldb/Utility/Status.h" #include "lldb/Utility/StringList.h" using namespace lldb_private; namespace { const size_t TIMEOUT_MILLIS = 5000; } /** 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 != 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: bool IsInputComplete(lldb_private::Editline *editline, lldb_private::StringList &lines); std::recursive_mutex output_mutex; std::unique_ptr _editline_sp; lldb::FileSP _el_primary_file; lldb::FileSP _el_secondary_file; }; EditlineAdapter::EditlineAdapter() : _editline_sp(), _el_secondary_file() { lldb_private::Status error; PseudoTerminal pty; // Open the first primary pty available. EXPECT_THAT_ERROR(pty.OpenFirstAvailablePrimary(O_RDWR), llvm::Succeeded()); // Open the corresponding secondary pty. EXPECT_THAT_ERROR(pty.OpenSecondary(O_RDWR), llvm::Succeeded()); // Grab the primary 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. _el_primary_file.reset( new NativeFile(pty.ReleasePrimaryFileDescriptor(), lldb_private::NativeFile::eOpenOptionReadWrite, true)); _el_secondary_file.reset( new NativeFile(pty.ReleaseSecondaryFileDescriptor(), lldb_private::NativeFile::eOpenOptionReadWrite, true)); lldb::LockableStreamFileSP output_stream_sp = std::make_shared(_el_secondary_file, output_mutex); lldb::LockableStreamFileSP error_stream_sp = std::make_shared(_el_secondary_file, output_mutex); // Create an Editline instance. _editline_sp.reset(new lldb_private::Editline( "gtest editor", _el_secondary_file->GetStream(), output_stream_sp, error_stream_sp, /*color=*/false)); _editline_sp->SetPrompt("> "); // Hookup our input complete callback. auto input_complete_cb = [this](Editline *editline, StringList &lines) { return this->IsInputComplete(editline, lines); }; _editline_sp->SetIsInputCompleteCallback(input_complete_cb); } void EditlineAdapter::CloseInput() { if (_el_secondary_file != nullptr) _el_secondary_file->Close(); } bool EditlineAdapter::SendLine(const std::string &line) { // Ensure we're valid before proceeding. if (!IsValid()) return false; std::string out = line + "\n"; // Write the line out to the pipe connected to editline's input. size_t num_bytes = out.length() * sizeof(std::string::value_type); EXPECT_THAT_ERROR(_el_primary_file->Write(out.c_str(), num_bytes).takeError(), llvm::Succeeded()); EXPECT_EQ(num_bytes, out.length() * sizeof(std::string::value_type)); return true; } 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) { // We'll call ourselves complete if we've received a balanced set of braces. int start_block_count = 0; int brace_balance = 0; for (const std::string &line : lines) { for (auto ch : line) { if (ch == '{') { ++start_block_count; ++brace_balance; } else if (ch == '}') --brace_balance; } } return (start_block_count > 0) && (brace_balance == 0); } void EditlineAdapter::ConsumeAllOutput() { FILE *output_file = _el_primary_file->GetStream(); 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 { SubsystemRAII subsystems; EditlineAdapter _el_adapter; std::shared_ptr _sp_output_thread; public: static void SetUpTestCase() { // We need a TERM set properly for editline to work as expected. setenv("TERM", "vt100", 1); } void SetUp() override { // Validate the editline adapter. EXPECT_TRUE(_el_adapter.IsValid()); if (!_el_adapter.IsValid()) return; // Dump output. _sp_output_thread = std::make_shared([&] { _el_adapter.ConsumeAllOutput(); }); } void TearDown() override { _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. std::vector reported_lines; for (const std::string &line : el_reported_lines) reported_lines.push_back(line); EXPECT_THAT(reported_lines, testing::ContainerEq(input_lines)); } #endif