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
2 changes: 2 additions & 0 deletions include/asmgrader/api/asm_data.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class AsmData
// NOLINTNEXTLINE(google-runtime-operator) - nicer semantics?
std::uintptr_t operator&() const { return address_; }

Result<T> operator*() const { return get_value(); }

std::uintptr_t get_address() const { return address_; }

/// Get the value currently present in the asm program
Expand Down
2 changes: 1 addition & 1 deletion include/asmgrader/api/asm_symbol.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class AsmSymbolResult : public Result<T>
};

template <typename T>
class AsmSymbol : AsmData<T>
class AsmSymbol : public AsmData<T>
{
public:
AsmSymbol(Program& prog, std::string name, std::uintptr_t address);
Expand Down
19 changes: 19 additions & 0 deletions include/asmgrader/common/linux.hpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
#pragma once

#include <asmgrader/common/aliases.hpp>
#include <asmgrader/common/expected.hpp>
#include <asmgrader/common/extra_formatters.hpp>
#include <asmgrader/logging.hpp>

#include <fmt/format.h>
#include <fmt/ostream.h>
#include <gsl/zstring>
#include <libassert/assert.hpp>
#include <range/v3/algorithm/transform.hpp>

Expand Down Expand Up @@ -106,6 +108,23 @@ inline Expected<> kill(pid_t pid, int sig) {
return {};
}

/// see access(2)
/// returns success/failure; logs failure at debug level
inline Expected<> access(gsl::czstring path, int mode) {
ASSERT((static_cast<u64>(mode) & ~static_cast<u64>(R_OK | W_OK | X_OK | F_OK)) == 0, mode);

int res = ::access(path, mode);

if (res == -1) {
auto err = make_error_code(errno);

LOG_DEBUG("access failed: '{}'", err);
return err;
}

return {};
}

/// args and envp do NOT need to have an extra NULL element; this is added for you.
/// see execve(2)
/// returns success/failure; logs failure at debug level
Expand Down
28 changes: 20 additions & 8 deletions include/asmgrader/registrars/global_registrar.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ class GlobalRegistrar
void add_test(std::unique_ptr<TestBase> test) noexcept;

template <typename Func>
requires(std::is_void_v<std::invoke_result_t<Func, Assignment>>)
requires(std::is_void_v<std::invoke_result_t<Func, Assignment&>>)
void for_each_assignment(Func&& fun);

template <typename Func>
requires(!std::is_void_v<std::invoke_result_t<Func, Assignment>>)
std::vector<std::invoke_result_t<Func, Assignment>> for_each_assignment(Func&& fun);
requires(!std::is_void_v<std::invoke_result_t<Func, Assignment&>>)
std::vector<std::invoke_result_t<Func, Assignment&>> for_each_assignment(Func&& fun);

auto get_assignments() noexcept {
return registered_assignments_ |
Expand All @@ -61,6 +61,18 @@ class GlobalRegistrar
return std::nullopt;
}

std::optional<std::reference_wrapper<Assignment>> get_assignment_by_pathname(std::string_view pathname) {
auto name_matcher = [pathname](const std::unique_ptr<Assignment>& assignment) {
return assignment->get_exec_path() == pathname;
};

if (auto iter = ranges::find_if(registered_assignments_, name_matcher); iter != registered_assignments_.end()) {
return **iter;
}

return std::nullopt;
}

/// Find the assignment corresponding to the `Assignment` attribute of metadata,
/// or create a new assignment if one does not exist.
template <typename... MetadataAttrs>
Expand Down Expand Up @@ -102,16 +114,16 @@ Assignment& GlobalRegistrar::find_or_create_assignment(metadata::Metadata<Metada
}

template <typename Func>
requires(std::is_void_v<std::invoke_result_t<Func, Assignment>>)
requires(std::is_void_v<std::invoke_result_t<Func, Assignment&>>)
void GlobalRegistrar::for_each_assignment(Func&& fun) {
for (auto& assignement : get_assignments()) {
std::forward<Func>(fun)(assignement);
for (auto& assignment : get_assignments()) {
std::forward<Func>(fun)(assignment);
}
}

template <typename Func>
requires(!std::is_void_v<std::invoke_result_t<Func, Assignment>>)
std::vector<std::invoke_result_t<Func, Assignment>> GlobalRegistrar::for_each_assignment(Func&& fun) {
requires(!std::is_void_v<std::invoke_result_t<Func, Assignment&>>)
std::vector<std::invoke_result_t<Func, Assignment&>> GlobalRegistrar::for_each_assignment(Func&& fun) {
std::vector<std::invoke_result_t<Func, Assignment>> result;
result.reserve(registered_assignments_.size());

Expand Down
92 changes: 87 additions & 5 deletions src/app/student_app.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "app/student_app.hpp"

#include "api/assignment.hpp"
#include "common/linux.hpp"
#include "grading_session.hpp"
#include "logging.hpp"
#include "output/plaintext_serializer.hpp"
Expand All @@ -9,29 +11,109 @@
#include "test_runner.hpp"
#include "user/program_options.hpp"

#include <fmt/base.h>
#include <fmt/format.h>
#include <libassert/assert.hpp>
#include <range/v3/view/transform.hpp>

#include <cstdlib>
#include <filesystem>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

#include <unistd.h>

namespace asmgrader {

namespace {

bool can_execute(const std::filesystem::path& path) {
namespace fs = std::filesystem;

if (!fs::is_regular_file(path)) {
LOG_TRACE("{} is not a regular file", path);
return false;
}

if (!linux::access(path.c_str(), X_OK)) {
LOG_TRACE("{} is not executable by this program", path);
return false;
}

return true;
}

std::reference_wrapper<Assignment> infer_assignment_or_exit() {
namespace fs = std::filesystem;

auto& registrar = GlobalRegistrar::get();

std::vector<std::reference_wrapper<Assignment>> infer_res;

for (fs::path cwd = fs::current_path(); const auto& dirent : fs::directory_iterator{cwd}) {
const auto& path = dirent.path();
std::string pathname = path.filename().string();
LOG_TRACE("Checking if {:?} matches any assignments {}", pathname, dirent.path());

if (!can_execute(path)) {
continue;
}

if (auto found = registrar.get_assignment_by_pathname(pathname)) {
LOG_TRACE("{:?} matches an assignment", pathname);
infer_res.push_back(found.value());
}
}

if (infer_res.size() == 0) {
fmt::println(stderr, "Could not infer assignment based on files in the current working directory! Please "
"either change directories or specify the assignment explicitly by name. Exiting.");
std::exit(1);
}

if (infer_res.size() > 1) {
fmt::println(
stderr,
"More than 1 valid assignment found in current working directory when attempting to infer "
"assignment.\nThis is ambiguous! Please specify the assignment explicitly by name.\n(Found: {::?})",
infer_res |
ranges::views::transform([](auto& assignment_ref) { return assignment_ref.get().get_exec_path(); }));
std::exit(1);
}

// infer_res.size() == 1
return infer_res.front();
}

} // namespace

Assignment& StudentApp::get_assignment_or_exit() const {
if (OPTS.assignment_name.empty()) {
LOG_DEBUG("Assignment unspecified; attempting to infer based on files in cwd.");
return infer_assignment_or_exit();
}

auto opt_assignment = GlobalRegistrar::get().get_assignment(OPTS.assignment_name);
ASSERT(opt_assignment.has_value(), "Internal error locating assignment", OPTS.assignment_name);

return opt_assignment.value();
}

int StudentApp::run_impl() {
LOG_TRACE("Registered tests: {::}", GlobalRegistrar::get().for_each_assignment([](const Assignment& assignment) {
return fmt::format("{:?}: {}", assignment.get_name(), assignment.get_test_names());
}));

auto assignment = GlobalRegistrar::get().get_assignment(OPTS.assignment_name);

// should be verified by CLI; double-check here just in case
ASSERT(assignment, "Error locating assignment {}", OPTS.assignment_name);
Assignment& assignment = get_assignment_or_exit();

StdoutSink output_sink;
std::shared_ptr output_serializer =
std::make_shared<PlainTextSerializer>(output_sink, OPTS.colorize_option, OPTS.verbosity);
AssignmentTestRunner runner{*assignment, output_serializer, OPTS.tests_filter};
AssignmentTestRunner runner{assignment, output_serializer, OPTS.tests_filter};

output_serializer->on_run_metadata(RunMetadata{});
AssignmentResult res = runner.run_all(OPTS.file_name);
Expand Down
3 changes: 3 additions & 0 deletions src/app/student_app.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include "api/assignment.hpp"
#include "app/app.hpp" // IWYU pragma: export

namespace asmgrader {
Expand All @@ -11,6 +12,8 @@ class StudentApp final : public App

private:
int run_impl() override;

Assignment& get_assignment_or_exit() const;
};

} // namespace asmgrader
17 changes: 14 additions & 3 deletions src/user/cl_args.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,22 @@ void CommandLineArgs::setup_parser() {
// maybe want to switch to another lib, or just do it myself. Need arg choices in help.

// clang-format off
std::string assignment_help_str = fmt::format("The assignment to run tests on.\n"
#ifndef PROFESSOR_VERSION
"If left unspecified, will attempt to infer the assignment based on files in the current working directory.\n"
#endif
"One of: {}", assignment_names.empty() ? "<No assignments; this is probably an error>" : fmt::format("{:n}", assignment_names));
auto& assignment_arg = arg_parser_.add_argument("assignment")
.store_into(opts_buffer_.assignment_name)
// Add all assignment names to help msg, since argparse won't add choices by
// default for some reason
.help(fmt::format("The assignment to run tests on\nOne of: {:n}", assignment_names));
#ifdef PROFESSOR_VERSION
.help(assignment_help_str);
#else
// inferring the lab is only supported in student mode for now
.nargs(0, 1) // [optional]
.help(assignment_help_str);
#endif // !PROFESSOR_VERSION
// Add all assignment names to help msg, since argparse won't add choices by
// default for some reason
GlobalRegistrar::get().for_each_assignment([&](const Assignment& assignment) {
assignment_arg.add_choice(assignment.get_name());
});
Expand Down
17 changes: 11 additions & 6 deletions src/user/program_options.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,17 @@ struct ProgramOptions

TRY(ensure_is_directory(search_path, "Search path {:?}"));

// Only check the database path if it's not the default
// non-existance will be handled properly in ProfessorApp
if (database_path != DEFAULT_DATABASE_PATH) {
TRY(ensure_is_regular_file(database_path, "Database file {:?}"));
}

// If the assignment name is empty, we're going to be attempting to infer it elsewhere
if (assignment_name.empty()) {
return {};
}

// The CLI should verify that the specified assignment is valid
// We'll check here just in case and return an error if it's not
auto assignment = TRYE(GlobalRegistrar::get().get_assignment(assignment_name),
Expand All @@ -129,12 +140,6 @@ struct ProgramOptions
TRY(Program::check_is_compat_elf(exec_file_name));
}

// Only check the database path if it's not the default
// non-existance will be handled properly in ProfessorApp
if (database_path != DEFAULT_DATABASE_PATH) {
TRY(ensure_is_regular_file(database_path, "Database file {:?}"));
}

return {};
}
};
Expand Down
Loading
Loading