293 lines · cpp
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-exception6//7//===----------------------------------------------------------------------===//8 9#include "lldb/Host/Config.h"10#include "lldb/Host/File.h"11#include "lldb/Host/HostInfo.h"12#include "lldb/lldb-forward.h"13#include "llvm/Testing/Support/Error.h"14 15#if LLDB_ENABLE_LIBEDIT16 17#define EDITLINE_TEST_DUMP_OUTPUT 018 19#include <stdio.h>20#include <unistd.h>21 22#include "gmock/gmock.h"23#include "gtest/gtest.h"24#include <memory>25#include <thread>26 27#include "TestingSupport/SubsystemRAII.h"28#include "lldb/Host/Editline.h"29#include "lldb/Host/FileSystem.h"30#include "lldb/Host/PseudoTerminal.h"31#include "lldb/Host/StreamFile.h"32#include "lldb/Utility/Status.h"33#include "lldb/Utility/StringList.h"34 35using namespace lldb_private;36 37namespace {38const size_t TIMEOUT_MILLIS = 5000;39}40 41/**42 Wraps an Editline class, providing a simple way to feed43 input (as if from the keyboard) and receive output from Editline.44 */45class EditlineAdapter {46public:47 EditlineAdapter();48 49 void CloseInput();50 51 bool IsValid() const { return _editline_sp != nullptr; }52 53 lldb_private::Editline &GetEditline() { return *_editline_sp; }54 55 bool SendLine(const std::string &line);56 57 bool SendLines(const std::vector<std::string> &lines);58 59 bool GetLine(std::string &line, bool &interrupted, size_t timeout_millis);60 61 bool GetLines(lldb_private::StringList &lines, bool &interrupted,62 size_t timeout_millis);63 64 void ConsumeAllOutput();65 66private:67 bool IsInputComplete(lldb_private::Editline *editline,68 lldb_private::StringList &lines);69 70 std::recursive_mutex output_mutex;71 std::unique_ptr<lldb_private::Editline> _editline_sp;72 73 lldb::FileSP _el_primary_file;74 lldb::FileSP _el_secondary_file;75};76 77EditlineAdapter::EditlineAdapter() : _editline_sp(), _el_secondary_file() {78 lldb_private::Status error;79 PseudoTerminal pty;80 81 // Open the first primary pty available.82 EXPECT_THAT_ERROR(pty.OpenFirstAvailablePrimary(O_RDWR), llvm::Succeeded());83 // Open the corresponding secondary pty.84 EXPECT_THAT_ERROR(pty.OpenSecondary(O_RDWR), llvm::Succeeded());85 86 // Grab the primary fd. This is a file descriptor we will:87 // (1) write to when we want to send input to editline.88 // (2) read from when we want to see what editline sends back.89 _el_primary_file.reset(90 new NativeFile(pty.ReleasePrimaryFileDescriptor(),91 lldb_private::NativeFile::eOpenOptionReadWrite, true));92 93 _el_secondary_file.reset(94 new NativeFile(pty.ReleaseSecondaryFileDescriptor(),95 lldb_private::NativeFile::eOpenOptionReadWrite, true));96 97 lldb::LockableStreamFileSP output_stream_sp =98 std::make_shared<LockableStreamFile>(_el_secondary_file, output_mutex);99 lldb::LockableStreamFileSP error_stream_sp =100 std::make_shared<LockableStreamFile>(_el_secondary_file, output_mutex);101 102 // Create an Editline instance.103 _editline_sp.reset(new lldb_private::Editline(104 "gtest editor", _el_secondary_file->GetStream(), output_stream_sp,105 error_stream_sp,106 /*color=*/false));107 _editline_sp->SetPrompt("> ");108 109 // Hookup our input complete callback.110 auto input_complete_cb = [this](Editline *editline, StringList &lines) {111 return this->IsInputComplete(editline, lines);112 };113 _editline_sp->SetIsInputCompleteCallback(input_complete_cb);114}115 116void EditlineAdapter::CloseInput() {117 if (_el_secondary_file != nullptr)118 _el_secondary_file->Close();119}120 121bool EditlineAdapter::SendLine(const std::string &line) {122 // Ensure we're valid before proceeding.123 if (!IsValid())124 return false;125 126 std::string out = line + "\n";127 128 // Write the line out to the pipe connected to editline's input.129 size_t num_bytes = out.length() * sizeof(std::string::value_type);130 EXPECT_THAT_ERROR(_el_primary_file->Write(out.c_str(), num_bytes).takeError(),131 llvm::Succeeded());132 EXPECT_EQ(num_bytes, out.length() * sizeof(std::string::value_type));133 return true;134}135 136bool EditlineAdapter::SendLines(const std::vector<std::string> &lines) {137 for (auto &line : lines) {138#if EDITLINE_TEST_DUMP_OUTPUT139 printf("<stdin> sending line \"%s\"\n", line.c_str());140#endif141 if (!SendLine(line))142 return false;143 }144 return true;145}146 147// We ignore the timeout for now.148bool EditlineAdapter::GetLine(std::string &line, bool &interrupted,149 size_t /* timeout_millis */) {150 // Ensure we're valid before proceeding.151 if (!IsValid())152 return false;153 154 _editline_sp->GetLine(line, interrupted);155 return true;156}157 158bool EditlineAdapter::GetLines(lldb_private::StringList &lines,159 bool &interrupted, size_t /* timeout_millis */) {160 // Ensure we're valid before proceeding.161 if (!IsValid())162 return false;163 164 _editline_sp->GetLines(1, lines, interrupted);165 return true;166}167 168bool EditlineAdapter::IsInputComplete(lldb_private::Editline *editline,169 lldb_private::StringList &lines) {170 // We'll call ourselves complete if we've received a balanced set of braces.171 int start_block_count = 0;172 int brace_balance = 0;173 174 for (const std::string &line : lines) {175 for (auto ch : line) {176 if (ch == '{') {177 ++start_block_count;178 ++brace_balance;179 } else if (ch == '}')180 --brace_balance;181 }182 }183 184 return (start_block_count > 0) && (brace_balance == 0);185}186 187void EditlineAdapter::ConsumeAllOutput() {188 FILE *output_file = _el_primary_file->GetStream();189 190 int ch;191 while ((ch = fgetc(output_file)) != EOF) {192#if EDITLINE_TEST_DUMP_OUTPUT193 char display_str[] = {0, 0, 0};194 switch (ch) {195 case '\t':196 display_str[0] = '\\';197 display_str[1] = 't';198 break;199 case '\n':200 display_str[0] = '\\';201 display_str[1] = 'n';202 break;203 case '\r':204 display_str[0] = '\\';205 display_str[1] = 'r';206 break;207 default:208 display_str[0] = ch;209 break;210 }211 printf("<stdout> 0x%02x (%03d) (%s)\n", ch, ch, display_str);212// putc(ch, stdout);213#endif214 }215}216 217class EditlineTestFixture : public ::testing::Test {218 SubsystemRAII<FileSystem, HostInfo> subsystems;219 EditlineAdapter _el_adapter;220 std::shared_ptr<std::thread> _sp_output_thread;221 222public:223 static void SetUpTestCase() {224 // We need a TERM set properly for editline to work as expected.225 setenv("TERM", "vt100", 1);226 }227 228 void SetUp() override {229 // Validate the editline adapter.230 EXPECT_TRUE(_el_adapter.IsValid());231 if (!_el_adapter.IsValid())232 return;233 234 // Dump output.235 _sp_output_thread =236 std::make_shared<std::thread>([&] { _el_adapter.ConsumeAllOutput(); });237 }238 239 void TearDown() override {240 _el_adapter.CloseInput();241 if (_sp_output_thread)242 _sp_output_thread->join();243 }244 245 EditlineAdapter &GetEditlineAdapter() { return _el_adapter; }246};247 248TEST_F(EditlineTestFixture, EditlineReceivesSingleLineText) {249 // Send it some text via our virtual keyboard.250 const std::string input_text("Hello, world");251 EXPECT_TRUE(GetEditlineAdapter().SendLine(input_text));252 253 // Verify editline sees what we put in.254 std::string el_reported_line;255 bool input_interrupted = false;256 const bool received_line = GetEditlineAdapter().GetLine(257 el_reported_line, input_interrupted, TIMEOUT_MILLIS);258 259 EXPECT_TRUE(received_line);260 EXPECT_FALSE(input_interrupted);261 EXPECT_EQ(input_text, el_reported_line);262}263 264TEST_F(EditlineTestFixture, EditlineReceivesMultiLineText) {265 // Send it some text via our virtual keyboard.266 std::vector<std::string> input_lines;267 input_lines.push_back("int foo()");268 input_lines.push_back("{");269 input_lines.push_back("printf(\"Hello, world\");");270 input_lines.push_back("}");271 input_lines.push_back("");272 273 EXPECT_TRUE(GetEditlineAdapter().SendLines(input_lines));274 275 // Verify editline sees what we put in.276 lldb_private::StringList el_reported_lines;277 bool input_interrupted = false;278 279 EXPECT_TRUE(GetEditlineAdapter().GetLines(el_reported_lines,280 input_interrupted, TIMEOUT_MILLIS));281 EXPECT_FALSE(input_interrupted);282 283 // Without any auto indentation support, our output should directly match our284 // input.285 std::vector<std::string> reported_lines;286 for (const std::string &line : el_reported_lines)287 reported_lines.push_back(line);288 289 EXPECT_THAT(reported_lines, testing::ContainerEq(input_lines));290}291 292#endif293