From 4bc58bfacadce92da8c2b5f35a35fb35d5bd8b87 Mon Sep 17 00:00:00 2001 From: Brandon Cheng Date: Sat, 23 Feb 2019 12:46:13 -0500 Subject: [PATCH] Add Assignment 2 tests Fixes #5 --- .gitmodules | 6 +++ CMakeLists.txt | 11 ++++ README.md | 1 + external/cppast | 1 + external/optional | 1 + includes/internal/exists.hpp | 14 ++++++ includes/internal/simple_parse.hpp | 14 ++++++ src/asgn2/buildlist.hpp | 81 ++++++++++++++++++++++++++++++ src/asgn2/main.cpp | 36 +++++++++++++ src/asgn2/question.hpp | 70 ++++++++++++++++++++++++++ src/asgn2/tests.cpp | 78 ++++++++++++++++++++++++++++ 11 files changed, 313 insertions(+) create mode 160000 external/cppast create mode 160000 external/optional create mode 100644 includes/internal/exists.hpp create mode 100644 includes/internal/simple_parse.hpp create mode 100644 src/asgn2/buildlist.hpp create mode 100644 src/asgn2/main.cpp create mode 100644 src/asgn2/question.hpp create mode 100644 src/asgn2/tests.cpp diff --git a/.gitmodules b/.gitmodules index d8dc293..ae94d12 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,9 @@ [submodule "external/Catch2"] path = external/Catch2 url = https://github.com/catchorg/Catch2 +[submodule "external/optional"] + path = external/optional + url = https://github.com/TartanLlama/optional +[submodule "external/cppast"] + path = external/cppast + url = https://github.com/foonathan/cppast diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a32781..fcfb30b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,20 @@ cmake_minimum_required(VERSION 3.10.2) +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +# Disable test compilation for the optional tl library. +set(OPTIONAL_ENABLE_TESTS OFF CACHE INTERNAL "Enable tl::optional tests") include_directories(includes) add_subdirectory(external/Catch2) +add_subdirectory(external/cppast) +add_subdirectory(external/optional) add_executable(asgn1 src/asgn1/main.cpp) target_link_libraries(asgn1 Catch2) + +add_executable(asgn2 src/asgn2/main.cpp) +target_link_libraries(asgn2 Catch2 cppast optional) diff --git a/README.md b/README.md index 5e217d9..f95289a 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ This project uses `cmake` to manage dependencies. Compiling the assignment testing binaries involves running a few commands. This produces an "out of source" build. ```sh +$ git submodule update --init --recursive $ mkdir build $ cd build $ cmake .. diff --git a/external/cppast b/external/cppast new file mode 160000 index 0000000..e2a98b3 --- /dev/null +++ b/external/cppast @@ -0,0 +1 @@ +Subproject commit e2a98b35335adbf64f285e2e534190b620e4bff0 diff --git a/external/optional b/external/optional new file mode 160000 index 0000000..dff20e9 --- /dev/null +++ b/external/optional @@ -0,0 +1 @@ +Subproject commit dff20e9c9f3d64a5a9e91eb3897e174b81e512e0 diff --git a/includes/internal/exists.hpp b/includes/internal/exists.hpp new file mode 100644 index 0000000..633188c --- /dev/null +++ b/includes/internal/exists.hpp @@ -0,0 +1,14 @@ +#ifndef __CSE3150_TESTING_EXISTS_H +#define __CSE3150_TESTING_EXISTS_H + +#include +#include + +// https://stackoverflow.com/a/12774387 +inline bool exists (const std::string& name) { + struct stat buffer; + const auto status = stat(name.c_str(), &buffer); + return status == 0; +} + +#endif diff --git a/includes/internal/simple_parse.hpp b/includes/internal/simple_parse.hpp new file mode 100644 index 0000000..561caca --- /dev/null +++ b/includes/internal/simple_parse.hpp @@ -0,0 +1,14 @@ +#ifndef __CSE3150_TESTING_PARSER14_H +#define __CSE3150_TESTING_PARSER14_H + +#include + +std::unique_ptr simple_parse(const std::string& path) { + cppast::libclang_compile_config config; + cppast::cpp_entity_index idx; + cppast::stderr_diagnostic_logger logger; + cppast::libclang_parser parser(type_safe::ref(logger)); + return parser.parse(idx, path, config); +} + +#endif diff --git a/src/asgn2/buildlist.hpp b/src/asgn2/buildlist.hpp new file mode 100644 index 0000000..e03b40d --- /dev/null +++ b/src/asgn2/buildlist.hpp @@ -0,0 +1,81 @@ +#ifndef __CSE3150_TESTING_BUILDLIST_H +#define __CSE3150_TESTING_BUILDLIST_H + +#include +#include +#include +#include +#include +#include +#include + +tl::optional find_build_list_fn(const cppast::cpp_file& file) { + tl::optional result = tl::nullopt; + + cppast::visit(file, [&](const cppast::cpp_entity& e, cppast::visitor_info info) { + const auto is_function = e.kind() == cppast::cpp_entity_kind::function_t; + const auto correct_name = e.name() == "buildList"; + + if (is_function && correct_name) { + result = e; + } + + // Stop visiting further nodes if we found the one we're looking for. + const auto should_keep_looking = result == tl::nullopt; + return should_keep_looking; + }); + + return result; +} + +bool type_is_list_template(const cppast::cpp_type& type) { + const auto is_template_instantiation = type.kind() == cppast::cpp_type_kind::template_instantiation_t; + + if (!is_template_instantiation) { + return false; + } + + const auto& template_instantiation = static_cast(type); + const auto primary_template = template_instantiation.primary_template(); + + return + primary_template.name() == "list" || + primary_template.name() == "std::list"; +} + +bool function_takes_list_reference(const cppast::cpp_function& fn) { + const auto list_param = std::find_if( + fn.parameters().begin(), + fn.parameters().end(), + [](const cppast::cpp_function_parameter& parameter) + { + const auto& type = parameter.type(); + const auto is_reference = type.kind() == cppast::cpp_type_kind::reference_t; + + if (!is_reference) { + return false; + } + + const auto& reference = static_cast(type); + return type_is_list_template(reference.referee()); + }); + + return list_param != fn.parameters().end(); +} + +bool function_returns_list_pointer(const cppast::cpp_function& fn) { + const cppast::cpp_type& return_type = fn.return_type(); + const cppast::cpp_type_kind& kind = return_type.kind(); + const auto is_pointer = kind == cppast::cpp_type_kind::pointer_t; + + if (!is_pointer) { + return false; + } + + const auto& pointer = static_cast(return_type); + const auto& pointee = pointer.pointee(); + + return type_is_list_template(pointer.pointee()); +} + +#endif diff --git a/src/asgn2/main.cpp b/src/asgn2/main.cpp new file mode 100644 index 0000000..f682935 --- /dev/null +++ b/src/asgn2/main.cpp @@ -0,0 +1,36 @@ +#define CATCH_CONFIG_RUNNER +#include +#include +#include +#include "tests.cpp" + +// There doesn't seem to be a way to get the total number of check/require +// statements from a Catch2 session. We will fortunately have to update +// this value manually as tests are added/removed. +const unsigned int TOTAL_ASSERTIONS = 13; + +int main(int argc, char* argv[]) { + const int num_failed = Catch::Session().run(argc, argv); + const bool perfect = num_failed == 0; + + if (perfect) { + std::cout + << "Congratulations, you finished this assignment with a 100%!" << std::endl + << "Looks like we need to make the next one harder. ;)" << std::endl + << std::endl; + } + + std::cout + << "Passed " + << (TOTAL_ASSERTIONS - num_failed) << '/' << TOTAL_ASSERTIONS + << std::endl; + + std::cout + << "Your score: " + << std::fixed + << std::setprecision(2) + << (1 - (static_cast(num_failed) / TOTAL_ASSERTIONS)) * 100 << "%" + << std::endl; + + return perfect ? 0 : 1; +} diff --git a/src/asgn2/question.hpp b/src/asgn2/question.hpp new file mode 100644 index 0000000..9ceed37 --- /dev/null +++ b/src/asgn2/question.hpp @@ -0,0 +1,70 @@ +#ifndef __CSE3150_TESTING_ASGN2_QUESTION_H +#define __CSE3150_TESTING_ASGN2_QUESTION_H + +#include +#include +#include +#include +#include +#include + +struct Question { + std::string id; + std::string desc; + + std::string directory() const { + return "./asgn2/" + this->id + "/"; + } + + std::string wlist_path() const { + return this->directory() + "WList.cpp"; + } + + std::string binary_path() const { + return this->directory() + "wlist"; + } + + tl::optional> exec(const std::string& in) const { + if (!exists(this->binary_path())) { + return tl::nullopt; + } + + try { + subprocess::popen cmd(this->binary_path(), {}); + cmd.stdin() << in << std::endl; + cmd.close(); + + std::stringstream s; + s << cmd.stdout().rdbuf(); + std::string out = s.str(); + + return explode(out, '\n'); + } catch (std::system_error) { + return tl::nullopt; + } + } + + bool passes_valgrind(const std::string& in) const { + if (!exists(this->binary_path())) { + return false; + } + + try { + subprocess::popen cmd("valgrind", { + "--error-exitcode=1", + "--leak-check=full", + this->binary_path() + }); + cmd.stdin() << in << std::endl; + cmd.close(); + int exit_status = cmd.wait(); + return exit_status != 1; + } catch (std::system_error) { + return false; + } + + return false; + } +}; + +#endif diff --git a/src/asgn2/tests.cpp b/src/asgn2/tests.cpp new file mode 100644 index 0000000..f0724d3 --- /dev/null +++ b/src/asgn2/tests.cpp @@ -0,0 +1,78 @@ +#include +#include +#include +#include +#include "buildlist.hpp" +#include "question.hpp" + +TEST_CASE("Assignment 2") { + const auto question = GENERATE( + Question { .id = "q1", .desc = "Q1: References" }, + Question { .id = "q2", .desc = "Q2: Pointers" }, + Question { .id = "q3", .desc = "Q3: Exception" } + ); + + DYNAMIC_SECTION(question.desc) { + DYNAMIC_SECTION("Compile " + question.id) { + // I'm not sure if running make before running our tests is a good idea or + // not yet. We'll see what responses are like. -Brandon + subprocess::popen make("make", {"-C", question.directory()}); + const auto make_exit_status = make.wait(); + INFO(make.stdout().rdbuf()); + CHECK(make_exit_status == 0); + } + + DYNAMIC_SECTION("Test a simple phrase for " + question.id) { + INFO("Make sure that the output is dumped one word per line"); + CHECK_THAT( + *question.exec("Vim is better than emacs").disjunction({}), + Catch::Equals(std::vector({ "Vim", "is", "better", "than", "emacs" })) + ); + } + + DYNAMIC_SECTION("Check a simple phrase with valgrind") { + INFO("Check that valgrind passes on your input and returns with no memory errors."); + CHECK(question.passes_valgrind("Emacs is better than vim")); + } + + DYNAMIC_SECTION("Check that buildList (in " + question.wlist_path() + ") meets question criteria.") { + if (!exists(question.wlist_path())) { + FAIL("File not found: " + question.wlist_path()); + } + + const auto w_list_file = simple_parse(question.wlist_path()); + if (w_list_file == nullptr) { + FAIL("Failed to parse wList.cpp file. Check the file's syntax and make sure it compiles."); + } + + const auto build_list = find_build_list_fn(*w_list_file); + if (!build_list.has_value()) { + FAIL("Make sure the \"buildList\" function exists in \"asgn2/q1/WList.cpp\""); + } + + // Ew... casting. (The first-party example uses it too, so this may be + // required by the cppast API design.) + auto& build_list_fn = static_cast(*build_list); + + if (question.id == "q1" || question.id == "q3") { + INFO("\"buildList\" needs to take an \"std::list\" as a reference for q1.\""); + CHECK(function_takes_list_reference(build_list_fn)); + } + + if (question.id == "q2") { + INFO("\"buildList\" needs to return an \"std::list\" pointer.\""); + CHECK(function_returns_list_pointer(build_list_fn)); + } + } + + if (question.id == "q3") { + SECTION("Output should be different if non-alphabetic character is entered") { + INFO("Have buildList throw an exception, then output an error (instead of the user input). The non-alphabetic characters should not be printed back out."); + CHECK_THAT( + *question.exec("Vim = emacs").disjunction({}), + !Catch::Equals(std::vector({ "Vim", "=", "emacs" })) + ); + } + } + } +}