diff --git a/.gitignore b/.gitignore index d9c190a..2d5565d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ build/ # Test results log.xml test_summary.jsonl +runner/test/test_runner test/examples/test_assertions test/examples/test_chatbot test/examples/test_description diff --git a/Makefile b/Makefile index d4cb4cb..0697be6 100644 --- a/Makefile +++ b/Makefile @@ -19,14 +19,21 @@ ifeq ($(UNAME_S),Darwin) endif +RUNNER_SRCS := runner/main.cpp runner/directory.cpp runner/process.cpp runner/output.cpp runner/runner.cpp TEST_SRCS := $(shell find test -name '*.cpp') TESTS := $(basename $(TEST_SRCS)) - test: all all: build $(TESTS) run +runner: build + g++ $(RUNNER_SRCS) -std=c++17 -g -O0 -o build/cest-runner + g++ runner/test/runner.test.cpp runner/runner.cpp runner/test/helpers/helpers.cpp -Ibuild -std=c++17 -g -O0 -o runner/test/test_runner + +runner-tests: runner + cd runner && ../build/cest-runner + build: mkdir -p build quom src/main.hpp build/cest @@ -41,5 +48,6 @@ clean: @rm -f log.xml @rm -f *.jsonl @rm -rf build + @rm -rf runner/test/test_runner -.PHONY: clean run build test +.PHONY: clean run build test runner runner-tests diff --git a/runner/directory.cpp b/runner/directory.cpp new file mode 100644 index 0000000..dad642a --- /dev/null +++ b/runner/directory.cpp @@ -0,0 +1,62 @@ +#include "directory.h" +#include +#include + +constexpr bool hasPerms(std::filesystem::perms target, std::filesystem::perms other) +{ + return std::filesystem::perms::none != (other & target); +} + +constexpr bool isExecutable(std::filesystem::perms target) +{ + return hasPerms(std::filesystem::perms::owner_exec, target) || + hasPerms(std::filesystem::perms::group_exec, target) || + hasPerms(std::filesystem::perms::others_exec, target); +} + +static std::vector filterBy(const std::string& filter, const std::vector& entries) +{ + std::vector result; + + std::copy_if( + entries.begin(), + entries.end(), + std::back_inserter(result), + [&filter](std::string path) { + return path.find(filter) != std::string::npos; + } + ); + + return result; +} + +std::vector Directory::findExecutableFiles( + const std::string& path, + const std::string& filter +) { + std::vector result; + + if (!std::filesystem::is_directory(path)) return result; + + for (const auto& entry : std::filesystem::directory_iterator(path)) + { + const auto permissions = entry.status().permissions(); + + if (entry.is_regular_file() && isExecutable(permissions)) + { + result.push_back(entry.path()); + } + else if (entry.is_directory()) + { + const auto sub_files = Directory::findExecutableFiles(entry.path(), filter); + result.insert(result.end(), sub_files.begin(), sub_files.end()); + } + } + + return filterBy(filter, result); +} + +std::string Directory::cwd() +{ + return std::filesystem::current_path(); +} diff --git a/runner/directory.h b/runner/directory.h new file mode 100644 index 0000000..d889b57 --- /dev/null +++ b/runner/directory.h @@ -0,0 +1,9 @@ +#pragma once +#include +#include + +namespace Directory +{ + std::vector findExecutableFiles(const std::string& path, const std::string& filter); + std::string cwd(); +} diff --git a/runner/main.cpp b/runner/main.cpp new file mode 100644 index 0000000..4117be0 --- /dev/null +++ b/runner/main.cpp @@ -0,0 +1,7 @@ +#include "runner.h" + +int main(int argc, char *argv[]) +{ + Runner::runTestsInCurrentPath(); + return 0; +} diff --git a/runner/output.cpp b/runner/output.cpp new file mode 100644 index 0000000..7a78434 --- /dev/null +++ b/runner/output.cpp @@ -0,0 +1,8 @@ +#include "output.h" +#include + +void Output::print(std::stringstream& text) +{ + std::cout << text.str(); + text.clear(); +} diff --git a/runner/output.h b/runner/output.h new file mode 100644 index 0000000..d5c0e00 --- /dev/null +++ b/runner/output.h @@ -0,0 +1,7 @@ +#pragma once +#include + +namespace Output +{ + void print(std::stringstream& text); +} diff --git a/runner/process.cpp b/runner/process.cpp new file mode 100644 index 0000000..3d9352a --- /dev/null +++ b/runner/process.cpp @@ -0,0 +1,69 @@ +#include "process.h" +#include +#include + +static constexpr int MAX_ARGS = 32; +static constexpr int MAX_BUFFER = 4096; + +static constexpr bool isChildProcess(pid_t pid) +{ + return pid == 0; +} + +static void handleChildProcess(int pipe_fd[2], const std::string& path, std::vector args) +{ + std::array c_args; + + dup2(pipe_fd[1], STDOUT_FILENO); + close(STDERR_FILENO); + close(pipe_fd[0]); + close(pipe_fd[1]); + + c_args.fill(NULL); + + for (int i=0; i 0); +} + +static void handleParentProcess(int pipe_fd[2], std::function on_output) +{ + std::array buffer; + + buffer.fill('\0'); + close(pipe_fd[1]); + read(pipe_fd[0], buffer.data(), buffer.size()); + + on_output(std::string(buffer.data())); + + waitForChildren(); +} + +void Process::runExecutable(const std::string& path, std::function on_output) +{ + int pipe_fd[2]; + pipe(pipe_fd); + + const auto pid = fork(); + + if (isChildProcess(pid)) + { + std::vector args = { "-o" }; + handleChildProcess(pipe_fd, path, args); + } + else + { + handleParentProcess(pipe_fd, on_output); + } +} diff --git a/runner/process.h b/runner/process.h new file mode 100644 index 0000000..5409f3c --- /dev/null +++ b/runner/process.h @@ -0,0 +1,7 @@ +#include +#include + +namespace Process +{ + void runExecutable(const std::string& path, std::function on_output); +} diff --git a/runner/runner.cpp b/runner/runner.cpp new file mode 100644 index 0000000..683be7a --- /dev/null +++ b/runner/runner.cpp @@ -0,0 +1,21 @@ +#include "runner.h" +#include "process.h" +#include "directory.h" +#include "output.h" + +void Runner::runTestsInCurrentPath() +{ + std::stringstream out; + const auto executables = Directory::findExecutableFiles(Directory::cwd(), "test_"); + + for (const auto& test_file : executables) + { + out << "Running test " << test_file << std::endl; + Output::print(out); + + Process::runExecutable(test_file, [&out](const auto& output) { + out << output; + Output::print(out); + }); + } +} diff --git a/runner/runner.h b/runner/runner.h new file mode 100644 index 0000000..cb21b9c --- /dev/null +++ b/runner/runner.h @@ -0,0 +1,6 @@ +#pragma once + +namespace Runner +{ + void runTestsInCurrentPath(); +} diff --git a/runner/test/helpers/helpers.cpp b/runner/test/helpers/helpers.cpp new file mode 100644 index 0000000..739dcca --- /dev/null +++ b/runner/test/helpers/helpers.cpp @@ -0,0 +1,56 @@ +#include +#include +#include +#include "helpers.h" +#include "../../output.h" +#include "../../process.h" +#include "../../directory.h" + +static std::vector __mock_executable_files; +static bool __find_executable_files_called = false; +static std::string __find_executable_files_path; +static std::string __find_executable_files_filter; + +static std::string __mock_run_executable_output; +static bool __run_executable_has_been_called = false; +static std::vector __run_executable_path; + +void Output::print(std::stringstream& text) +{ +} + +void Directory::findExecutableFiles_mockFiles(std::vector files) +{ + __mock_executable_files = files; +} +bool Directory::findExecutableFiles_hasBeenCalledWith(const std::string& path, const std::string& filter) +{ + return __find_executable_files_called && __find_executable_files_path == path && __find_executable_files_filter == filter; +} +std::vector Directory::findExecutableFiles(const std::string& path, const std::string& filter) +{ + __find_executable_files_path = path; + __find_executable_files_filter = filter; + __find_executable_files_called = true; + return __mock_executable_files; +} +std::string Directory::cwd() +{ + return "/cwd"; +} + +void Process::runExecutable_mockOutput(const std::string& output) +{ + __mock_run_executable_output = output; +} +void Process::runExecutable(const std::string& path, std::function on_output) +{ + __run_executable_has_been_called = true; + __run_executable_path.push_back(path); + on_output(__mock_run_executable_output); +} +bool Process::runExecutable_hasBeenCalledWith(const std::string& path) +{ + auto it = std::find(__run_executable_path.cbegin(), __run_executable_path.cend(), path); + return __run_executable_has_been_called && it != __run_executable_path.end(); +} diff --git a/runner/test/helpers/helpers.h b/runner/test/helpers/helpers.h new file mode 100644 index 0000000..714bd63 --- /dev/null +++ b/runner/test/helpers/helpers.h @@ -0,0 +1,16 @@ +#pragma once +#include +#include +#include + +namespace Directory +{ + void findExecutableFiles_mockFiles(std::vector files); + bool findExecutableFiles_hasBeenCalledWith(const std::string& path, const std::string& filter); +} + +namespace Process +{ + void runExecutable_mockOutput(const std::string& output); + bool runExecutable_hasBeenCalledWith(const std::string& path); +} diff --git a/runner/test/runner.test.cpp b/runner/test/runner.test.cpp new file mode 100644 index 0000000..7dd506c --- /dev/null +++ b/runner/test/runner.test.cpp @@ -0,0 +1,17 @@ +#include +#include "../runner.h" +#include "../directory.h" +#include "helpers/helpers.h" + +describe("Runner", []() { + it("executes all tests found in the current directory", []() { + std::vector test_files = { "first/test", "second/test" }; + Directory::findExecutableFiles_mockFiles(test_files); + + Runner::runTestsInCurrentPath(); + + expect(Directory::findExecutableFiles_hasBeenCalledWith(Directory::cwd(), "test_")).toBeTruthy(); + expect(Process::runExecutable_hasBeenCalledWith("first/test")).toBeTruthy(); + expect(Process::runExecutable_hasBeenCalledWith("second/test")).toBeTruthy(); + }); +}); diff --git a/src/arg-parser.hpp b/src/arg-parser.hpp index 1503990..2e1346f 100644 --- a/src/arg-parser.hpp +++ b/src/arg-parser.hpp @@ -50,7 +50,7 @@ namespace cest } catch (const std::invalid_argument &err) { - std::cout << "Invalid seed value: " << argv[i + 1] << std::endl; + std::cerr << "Invalid seed value: " << argv[i + 1] << std::endl; } } } @@ -59,4 +59,4 @@ namespace cest return options; } -} \ No newline at end of file +}