Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions include/asmgrader/api/test_context.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <asmgrader/program/program.hpp>
#include <asmgrader/subprocess/memory/concepts.hpp>
#include <asmgrader/subprocess/run_result.hpp>
#include <asmgrader/subprocess/subprocess.hpp>
#include <asmgrader/subprocess/syscall_record.hpp>

#include <fmt/base.h>
Expand Down Expand Up @@ -81,6 +82,9 @@ class TestContext
/// Get all stdout from since the beginning of the test invokation
std::string get_full_stdout();

Subprocess::OutputResult get_output(Subprocess::WhichOutput which = Subprocess::WhichOutput::StdoutAndStderr);
Subprocess::OutputResult get_full_output(Subprocess::WhichOutput which = Subprocess::WhichOutput::StdoutAndStderr);

/// Flushes any reamaining unread data in the stdin buffer
/// Returns: number of bytes flushed, or error kind if failure occured
std::size_t flush_stdin();
Expand Down
11 changes: 11 additions & 0 deletions include/asmgrader/common/expected.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ class [[nodiscard]] Expected
}

template <typename Func>
requires(!std::is_void_v<T>)
constexpr Expected<std::invoke_result_t<Func, T>, E> transform(const Func& func) {
if (!has_value()) {
return error();
Expand All @@ -168,6 +169,16 @@ class [[nodiscard]] Expected
return func(value());
}

template <typename Func>
requires(std::is_void_v<T>)
constexpr Expected<std::invoke_result_t<Func, T>, E> transform(const Func& func) {
if (!has_value()) {
return error();
}

return func();
}

private:
template <typename Td, typename Ed>
struct ExpectedData
Expand Down
90 changes: 79 additions & 11 deletions include/asmgrader/subprocess/subprocess.hpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
#pragma once

#include <asmgrader/common/aliases.hpp>
#include <asmgrader/common/class_traits.hpp>
#include <asmgrader/common/error_types.hpp>
#include <asmgrader/common/expected.hpp>
#include <asmgrader/common/linux.hpp>

#include <fmt/format.h>

#include <chrono>
#include <cstddef>
#include <optional>
Expand All @@ -28,17 +31,35 @@ class Subprocess : NonCopyable
Subprocess(Subprocess&&) noexcept;
Subprocess& operator=(Subprocess&&) noexcept;

/// Which output file descriptor to read from
enum class WhichOutput : u8 { None = 0, Stdout = 1, Stderr = 2, StdoutAndStderr = Stdout | Stderr };

/// stdout_str is only valid if WhichOutput::Stdout was included in the request
/// stderr_str is only valid if WhichOutput::Stderr was included in the request
struct OutputResult
{
std::string stdout_str;
std::string stderr_str;
};

/// Read buffered output since the last call to this function
OutputResult read_output(WhichOutput which = WhichOutput::StdoutAndStderr);

/// Get all output since the program has launched
OutputResult read_full_output(WhichOutput which = WhichOutput::StdoutAndStderr);

template <typename Rep, typename Period>
Result<std::string> read_stdout(const std::chrono::duration<Rep, Period>& timeout) {
return read_stdout_poll_impl(std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count());
[[deprecated]] Result<std::string> read_stdout(const std::chrono::duration<Rep, Period>& timeout) {
read_pipe_poll(stdout_, std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count());
return new_output(WhichOutput::Stdout).stdout_str;
}

Result<std::string> read_stdout();
/// Updates output result based on cursor positions
/// If the cursor is located before the end of the string, then a substring is used
/// (starting at the cursor position)
void get_new_output(OutputResult& res);

/// Get all stdout since the program has launched
const std::string& get_full_stdout();

Result<void> send_stdin(std::string_view str);
Result<void> send_stdin(std::string_view str) const;

// Forks the current process to start a new subprocess as specified
virtual Result<void> start();
Expand Down Expand Up @@ -78,12 +99,20 @@ class Subprocess : NonCopyable
/// pipes to communicate with subprocess' stdout and stdin respectively
/// The parent process will only make use of the write end of stdin_pipe_, and the read end of stdout_pipe_
linux::Pipe stdin_pipe_{};
linux::Pipe stdout_pipe_{};

std::string stdout_buffer_;
std::size_t stdout_cursor_{};
struct OutputPipe
{
linux::Pipe pipe;
std::string buffer;
std::size_t cursor;
};

Result<std::string> read_stdout_poll_impl(int timeout_ms);
OutputPipe stdout_{};
OutputPipe stderr_{};

/// Marks all open fds (other than 0,1,2) as FD_CLOEXEC so that they get closed in the child proc
/// Run in the PARENT process.
Expected<> mark_cloexec_all() const;

/// Marks all open fds (other than 0,1,2) as FD_CLOEXEC so that they get closed in the child proc
/// Run in the PARENT process.
Expand All @@ -92,10 +121,49 @@ class Subprocess : NonCopyable
/// Reads any data on the stdout pipe to stdout_buffer_
Result<void> read_stdout_impl();

/// Reads any immediately available data available on the specified pipe's read end
/// Writes any new data to that OutputPipe's buffer
/// Throws a std::logic_error if any syscalls fail
static void read_pipe_nonblock(OutputPipe& pipe);

/// Polls the pipe's read end for timeout_ms millis, or until available input arrives
/// Writes any new data to that OutputPipe's buffer
/// \returns true if a successful read occurred, false if timed out
static bool read_pipe_poll(OutputPipe& pipe, int timeout_ms);

/// Obtain new output based on cursor positions and buffers,
/// and set cursors to the end of their resp. buffers
OutputResult new_output(WhichOutput which);

std::optional<int> exit_code_;

std::string exec_;
std::vector<std::string> args_;
};

// TODO: macro for bitfield enums

constexpr std::string_view format_as(const Subprocess::WhichOutput& from) {
switch (from) {
case Subprocess::WhichOutput::None:
return "none";
case Subprocess::WhichOutput::Stdout:
return "stdout";
case Subprocess::WhichOutput::Stderr:
return "stderr";
case Subprocess::WhichOutput::StdoutAndStderr:
return "stdout&stderr";
default:
return "<unknown>";
}
}

constexpr Subprocess::WhichOutput operator&(const Subprocess::WhichOutput& lhs, const Subprocess::WhichOutput& rhs) {
return static_cast<Subprocess::WhichOutput>(fmt::underlying(lhs) & fmt::underlying(rhs));
}

constexpr Subprocess::WhichOutput operator|(const Subprocess::WhichOutput& lhs, const Subprocess::WhichOutput& rhs) {
return static_cast<Subprocess::WhichOutput>(fmt::underlying(lhs) | fmt::underlying(rhs));
}

} // namespace asmgrader
13 changes: 11 additions & 2 deletions src/api/test_context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "logging.hpp"
#include "program/program.hpp"
#include "subprocess/run_result.hpp"
#include "subprocess/subprocess.hpp"
#include "subprocess/syscall_record.hpp"

#include <fmt/color.h>
Expand Down Expand Up @@ -83,11 +84,19 @@ std::string_view TestContext::get_name() const {
}

std::string TestContext::get_stdout() {
return TRY_OR_THROW(prog_.get_subproc().read_stdout(), "failed to read stdout");
return get_output(Subprocess::WhichOutput::Stdout).stdout_str;
}

std::string TestContext::get_full_stdout() {
return prog_.get_subproc().get_full_stdout();
return get_full_output(Subprocess::WhichOutput::Stdout).stdout_str;
}

Subprocess::OutputResult TestContext::get_output(Subprocess::WhichOutput which) {
return prog_.get_subproc().read_output(which);
}

Subprocess::OutputResult TestContext::get_full_output(Subprocess::WhichOutput which) {
return prog_.get_subproc().read_full_output(which);
}

void TestContext::send_stdin(std::string_view input) {
Expand Down
Loading
Loading