Add synchronization to the IOHandler to prevent multiple threads from writing concurrently to the output or error stream. A scenario where this could happen is when a thread (the default event thread for example) is using the debugger's asynchronous stream. We would delegate this operation to the IOHandler which might be running on another thread. Until this patch there was nothing to synchronize the two at the IOHandler level. Differential revision: https://reviews.llvm.org/D121500
313 lines
8.6 KiB
C++
313 lines
8.6 KiB
C++
//===-- 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"
|
|
|
|
#if LLDB_ENABLE_LIBEDIT
|
|
|
|
#define EDITLINE_TEST_DUMP_OUTPUT 0
|
|
|
|
#include <stdio.h>
|
|
#include <unistd.h>
|
|
|
|
#include "gmock/gmock.h"
|
|
#include "gtest/gtest.h"
|
|
#include <memory>
|
|
#include <thread>
|
|
|
|
#include "TestingSupport/SubsystemRAII.h"
|
|
#include "lldb/Host/Editline.h"
|
|
#include "lldb/Host/FileSystem.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 != nullptr; }
|
|
|
|
lldb_private::Editline &GetEditline() { return *_editline_sp; }
|
|
|
|
bool SendLine(const std::string &line);
|
|
|
|
bool SendLines(const std::vector<std::string> &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<lldb_private::Editline> _editline_sp;
|
|
|
|
PseudoTerminal _pty;
|
|
int _pty_primary_fd = -1;
|
|
int _pty_secondary_fd = -1;
|
|
|
|
std::unique_ptr<FilePointer> _el_secondary_file;
|
|
};
|
|
|
|
EditlineAdapter::EditlineAdapter()
|
|
: _editline_sp(), _pty(), _el_secondary_file() {
|
|
lldb_private::Status error;
|
|
|
|
// Open the first primary pty available.
|
|
EXPECT_THAT_ERROR(_pty.OpenFirstAvailablePrimary(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.
|
|
_pty_primary_fd = _pty.GetPrimaryFileDescriptor();
|
|
|
|
// Open the corresponding secondary pty.
|
|
EXPECT_THAT_ERROR(_pty.OpenSecondary(O_RDWR), llvm::Succeeded());
|
|
_pty_secondary_fd = _pty.GetSecondaryFileDescriptor();
|
|
|
|
_el_secondary_file.reset(new FilePointer(fdopen(_pty_secondary_fd, "rw")));
|
|
EXPECT_FALSE(nullptr == *_el_secondary_file);
|
|
if (*_el_secondary_file == nullptr)
|
|
return;
|
|
|
|
// Create an Editline instance.
|
|
_editline_sp.reset(new lldb_private::Editline(
|
|
"gtest editor", *_el_secondary_file, *_el_secondary_file,
|
|
*_el_secondary_file, output_mutex, 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.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_primary_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_primary_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<std::string> &lines) {
|
|
for (auto &line : lines) {
|
|
#if EDITLINE_TEST_DUMP_OUTPUT
|
|
printf("<stdin> 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() {
|
|
FilePointer output_file(fdopen(_pty_primary_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("<stdout> 0x%02x (%03d) (%s)\n", ch, ch, display_str);
|
|
// putc(ch, stdout);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
class EditlineTestFixture : public ::testing::Test {
|
|
SubsystemRAII<FileSystem> subsystems;
|
|
EditlineAdapter _el_adapter;
|
|
std::shared_ptr<std::thread> _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<std::thread>([&] { _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<std::string> 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<std::string> reported_lines;
|
|
for (const std::string &line : el_reported_lines)
|
|
reported_lines.push_back(line);
|
|
|
|
EXPECT_THAT(reported_lines, testing::ContainerEq(input_lines));
|
|
}
|
|
|
|
#endif
|