314 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			C++
		
	
	
	
			
		
		
	
	
			314 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			C++
		
	
	
	
//===-- 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 <stdio.h>
 | 
						|
#include <unistd.h>
 | 
						|
 | 
						|
#include <memory>
 | 
						|
#include <thread>
 | 
						|
 | 
						|
#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<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:
 | 
						|
  static bool IsInputComplete(lldb_private::Editline *editline,
 | 
						|
                              lldb_private::StringList &lines, void *baton);
 | 
						|
 | 
						|
  std::unique_ptr<lldb_private::Editline> _editline_sp;
 | 
						|
 | 
						|
  lldb_utility::PseudoTerminal _pty;
 | 
						|
  int _pty_master_fd;
 | 
						|
  int _pty_slave_fd;
 | 
						|
 | 
						|
  std::unique_ptr<FilePointer> _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<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,
 | 
						|
                                      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("<stdout> 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<std::thread> _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<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.
 | 
						|
  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
 |