blob: 18625ff1fb66338053152b0bb386edd9c56b897b [file] [log] [blame]
// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This is a TCP service and a fidl service. The TCP portion of this process
// accepts test commands, runs them, waits for completion or error, and reports
// back to the TCP client.
// The TCP protocol is as follows:
// - Client connects, sends a single line representing the test command to run:
// run <test_id> <shell command to run>\n
// - To send a log message, we send to the TCP client:
// <test_id> log <msg>
// - Once the test is done, we reply to the TCP client:
// <test_id> teardown pass|fail\n
//
// The <test_id> is an unique ID string that the TCP client gives us per test;
// we tag our replies and device logs with it so the TCP client can identify
// device logs (and possibly if multiple tests are run at the same time).
//
// The shell command representing the running test is launched in a new
// Environment for easy teardown. This Environment
// contains a TestRunner service (see test_runner.fidl). The applications
// launched by the shell command (which may launch more than 1 process) may use
// the |TestRunner| service to signal completion of the test, and also provides
// a way to signal process crashes.
// TODO(vardhan): Make it possible to run multiple tests within the same test
// runner environment, without teardown; useful for testing modules, which may
// not need to tear down device_runner.
#include <functional>
#include <string>
#include <vector>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async/cpp/task.h>
#include <lib/zx/time.h>
#include <test_runner/cpp/fidl.h>
#include "lib/app/cpp/startup_context.h"
#include "lib/fxl/logging.h"
#include "lib/fxl/strings/split_string.h"
#include "lib/fxl/strings/string_view.h"
#include "lib/test_runner/cpp/scope.h"
#include "lib/test_runner/cpp/test_runner.h"
#include "lib/test_runner/cpp/test_runner_store_impl.h"
// TODO(vardhan): Make listen port command-line configurable.
constexpr uint16_t kListenPort = 8342; // TCP port
namespace test_runner {
// Represents a client connection, and is self-owned (it will exit the
// MessageLoop upon completion). TestRunnerConnection receives commands to run
// tests and runs them one at a time using TestRunContext.
class TestRunnerConnection : public TestRunObserver {
public:
TestRunnerConnection(async::Loop* loop, int socket_fd,
std::shared_ptr<fuchsia::sys::StartupContext> app_context)
: loop_(loop), app_context_(app_context), socket_(socket_fd) {}
void Start() {
FXL_CHECK(!test_context_);
ReadAndRunCommand();
}
void SendMessage(const std::string& test_id, const std::string& operation,
const std::string& msg) override {
std::stringstream stream;
stream << test_id << " " << operation << " " << msg << "\n";
std::string bytes = stream.str();
FXL_CHECK(write(socket_, bytes.data(), bytes.size()) > 0);
}
// Called by TestRunContext when it has finished running its test. This will
// trigger reading more commands from TCP socket.
void Teardown(const std::string& test_id, bool success) override {
FXL_CHECK(test_context_);
FXL_LOG(INFO) << "test_runner: teardown " << test_id
<< " success=" << success;
SendMessage(test_id, "teardown", success ? "pass" : "fail");
test_context_.reset();
Start();
}
private:
~TestRunnerConnection() override {
close(socket_);
loop_->Quit();
}
// Read and entire command (which consists of one line) and return it.
// Can be called again to read the next command. Blocks until an entire line
// has been read.
std::string ReadCommand() {
char buf[1024];
// Read until we see a new line.
auto newline_pos = command_buffer_.find('\n');
while (newline_pos == std::string::npos) {
ssize_t n = read(socket_, buf, sizeof(buf));
if (n <= 0) {
return std::string();
}
command_buffer_.append(buf, n);
newline_pos = command_buffer_.find('\n');
}
// Consume only until the new line (and leave the rest of the bytes for
// subsequent ReadCommand()s.
auto retval = command_buffer_.substr(0, newline_pos + 1);
command_buffer_.erase(0, newline_pos + 1);
return retval;
}
// Read an entire line representing the command to run and run it. When the
// test has finished running, TestRunnerConnection::Teardown is invoked. We do
// not read any further commands until that has happened.
void ReadAndRunCommand() {
std::string command = ReadCommand();
if (command.empty()) {
delete this;
return;
}
// command_parse[0] = "run"
// command_parse[1] = test_id
// command_parse[2] = url
// command_parse[3..] = args (optional)
std::vector<std::string> command_parse = fxl::SplitStringCopy(
command, " ", fxl::kTrimWhitespace, fxl::kSplitWantNonEmpty);
FXL_CHECK(command_parse.size() >= 3)
<< "Not enough args. Must be: `run <test id> <command to run>`";
FXL_CHECK(command_parse[0] == "run")
<< command_parse[0] << " is not a supported command.";
FXL_LOG(INFO) << "test_runner: run " << command_parse[1];
std::vector<std::string> args;
for (size_t i = 3; i < command_parse.size(); i++) {
args.push_back(std::move(command_parse[i]));
}
// When TestRunContext is done with the test, it calls
// TestRunnerConnection::Teardown().
test_context_.reset(new TestRunContext(app_context_, this, command_parse[1],
command_parse[2], args));
}
async::Loop* const loop_;
std::shared_ptr<fuchsia::sys::StartupContext> app_context_;
std::unique_ptr<TestRunContext> test_context_;
// Posix fd for the TCP connection.
const int socket_;
std::string command_buffer_;
FXL_DISALLOW_COPY_AND_ASSIGN(TestRunnerConnection);
};
// TestRunnerTCPServer is a TCP server that accepts connections and launches
// them as TestRunnerConnection.
class TestRunnerTCPServer {
public:
TestRunnerTCPServer(async::Loop* loop, uint16_t port)
: loop_(loop),
app_context_(fuchsia::sys::StartupContext::CreateFromStartupInfo()) {
struct sockaddr_in6 addr;
addr.sin6_family = AF_INET6;
addr.sin6_port = htons(port);
// in6addr_any (by default) allows connections to be established from any
// IPv4 or IPv6 client that specifies the given port.
addr.sin6_addr = in6addr_any;
// 1. Make a TCP socket.
// We need to retry because there's a race condition at boot
// between netstack initializing and us calling socket().
const auto duration = zx::msec(200);
for (int i = 0; i < 5 * 10; ++i) {
listener_ = socket(addr.sin6_family, SOCK_STREAM, IPPROTO_TCP);
if (listener_ != -1) {
break;
}
zx::nanosleep(zx::deadline_after(duration));
}
FXL_CHECK(listener_ != -1);
// 2. Bind it to an address.
FXL_CHECK(bind(listener_, reinterpret_cast<struct sockaddr*>(&addr),
sizeof(addr)) != -1);
// 3. Make it a listening socket.
FXL_CHECK(listen(listener_, 100) != -1);
}
~TestRunnerTCPServer() { close(listener_); }
// Blocks until there is a new connection.
TestRunnerConnection* AcceptConnection() {
int sockfd = accept(listener_, nullptr, nullptr);
if (sockfd == -1) {
FXL_LOG(INFO) << "accept() oops";
}
return new TestRunnerConnection(loop_, sockfd, app_context_);
}
private:
async::Loop* const loop_;
int listener_;
std::shared_ptr<fuchsia::sys::StartupContext> app_context_;
FXL_DISALLOW_COPY_AND_ASSIGN(TestRunnerTCPServer);
};
} // namespace test_runner
int main() {
async::Loop loop(&kAsyncLoopConfigMakeDefault);
test_runner::TestRunnerTCPServer server(&loop, kListenPort);
while (1) {
// TODO(vardhan): Because our sockets are POSIX fds, they don't work with
// our message loop, so we do some synchronous operations and have to do
// manipulate the message loop to pass control back and forth. Consider
// using separate threads for handle message loop vs. fd polling.
auto* runner = server.AcceptConnection();
async::PostTask(
loop.async(),
std::bind(&test_runner::TestRunnerConnection::Start, runner));
loop.Run();
}
return 0;
}