diff --git a/Makefile b/Makefile index 143c240..590dc0e 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,8 @@ link: build: GL/version.h $(OBJS) link +test: build + $(KOS_MAKE) -C tests all samples: build $(KOS_MAKE) -C samples all diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000..7ef501d --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,34 @@ +TARGET = tests.elf +OBJS = test_runner.o + +TESTS = $(shell find . -name "*.cpp") + +CXXFLAGS := $(CXXFLAGS) -std=c++11 + +all: rm-elf $(TARGET) + +include $(KOS_BASE)/Makefile.rules + +clean: + -rm -f $(TARGET) $(OBJS) romdisk.* + +rm-elf: + -rm -f $(TARGET) romdisk.* + +test_runner.cpp: $(shell python3 test_generator.py --output=test_runner.cpp $(TESTS)) + +$(TARGET): $(OBJS) romdisk.o + kos-c++ -o $(TARGET) $(OBJS) romdisk.o + +romdisk.img: + $(KOS_GENROMFS) -f romdisk.img -d romdisk -v + +romdisk.o: romdisk.img + $(KOS_BASE)/utils/bin2o/bin2o romdisk.img romdisk romdisk.o + +run: $(TARGET) + $(KOS_LOADER) $(TARGET) + +dist: + rm -f $(OBJS) romdisk.o romdisk.img + $(KOS_STRIP) $(TARGET) diff --git a/tests/test_generator.py b/tests/test_generator.py new file mode 100755 index 0000000..2817fdd --- /dev/null +++ b/tests/test_generator.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 + +import argparse +import re +import sys + +parser = argparse.ArgumentParser(description="Generate C++ unit tests") +parser.add_argument("--output", type=str, nargs=1, help="The output source file for the generated test main()", required=True) +parser.add_argument("test_files", type=str, nargs="+", help="The list of C++ files containing your tests") +parser.add_argument("--verbose", help="Verbose logging", action="store_true", default=False) + + +CLASS_REGEX = r"\s*class\s+(\w+)\s*([\:|,]\s*(?:public|private|protected)\s+[\w|::]+\s*)*" +TEST_FUNC_REGEX = r"void\s+(?Ptest_\S[^\(]+)\(\s*(void)?\s*\)" + + +INCLUDE_TEMPLATE = "#include \"%(file_path)s\"" + +REGISTER_TEMPLATE = """ + runner->register_case<%(class_name)s>( + std::vector({%(members)s}), + {%(names)s} + );""" + +MAIN_TEMPLATE = """ + +#include +#include +#include + +#include "../utils/test.h" + +%(includes)s + + +std::map parse_args(int argc, char* argv[]) { + std::map ret; + + for(int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + + auto eq = arg.find('='); + if(eq != std::string::npos && arg[0] == '-' && arg[1] == '-') { + auto key = std::string(arg.begin(), arg.begin() + eq); + auto value = std::string(arg.begin() + eq + 1, arg.end()); + ret[key] = value; + } else if(arg[0] == '-' && arg[1] == '-') { + auto key = arg; + if(i < (argc - 1)) { + auto value = argv[++i]; + ret[key] = value; + } else { + ret[key] = ""; + } + } else { + ret[arg] = ""; // Positional, not key=value + } + } + + return ret; +} + +int main(int argc, char* argv[]) { + auto runner = std::make_shared(); + + auto args = parse_args(argc, argv); + + std::string junit_xml; + auto junit_xml_it = args.find("--junit-xml"); + if(junit_xml_it != args.end()) { + junit_xml = junit_xml_it->second; + std::cout << " Outputting junit XML to: " << junit_xml << std::endl; + args.erase(junit_xml_it); + } + + std::string test_case; + if(args.size()) { + test_case = args.begin()->first; + } + + %(registrations)s + + return runner->run(test_case, junit_xml); +} + + +""" + +VERBOSE = False + +def log_verbose(message): + if VERBOSE: + print(message) + + +def find_tests(files): + + subclasses = [] + + # First pass, find all class definitions + for path in files: + with open(path, "rt") as f: + source_file_data = f.read().replace("\r\n", "").replace("\n", "") + + while True: + match = re.search(CLASS_REGEX, source_file_data) + if not match: + break + + class_name = match.group().split(":")[0].replace("class", "").strip() + + try: + parents = match.group().split(":", 1)[1] + except IndexError: + pass + else: + parents = [ x.strip() for x in parents.split(",") ] + parents = [ + x.replace("public", "").replace("private", "").replace("protected", "").strip() + for x in parents + ] + + subclasses.append((path, class_name, parents, [])) + log_verbose("Found: %s" % str(subclasses[-1])) + + start = match.end() + + # Find the next opening brace + while source_file_data[start] in (' ', '\t'): + start += 1 + + start -= 1 + end = start + if source_file_data[start+1] == '{': + + class_data = [] + brace_counter = 1 + for i in range(start+2, len(source_file_data)): + class_data.append(source_file_data[i]) + if class_data[-1] == '{': brace_counter += 1 + if class_data[-1] == '}': brace_counter -= 1 + if not brace_counter: + end = i + break + + class_data = "".join(class_data) + + while True: + match = re.search(TEST_FUNC_REGEX, class_data) + if not match: + break + + subclasses[-1][-1].append(match.group('func_name')) + class_data = class_data[match.end():] + + source_file_data = source_file_data[end:] + + + # Now, simplify the list by finding all potential superclasses, and then keeping any classes + # that subclass them. + test_case_subclasses = [] + i = 0 + while i < len(subclasses): + subclass_names = [x.rsplit("::")[-1] for x in subclasses[i][2]] + + # If this subclasses TestCase, or it subclasses any of the already found testcase subclasses + # then add it to the list + if "TestCase" in subclass_names or "SimulantTestCase" in subclass_names or any(x[1] in subclasses[i][2] for x in test_case_subclasses): + if subclasses[i] not in test_case_subclasses: + test_case_subclasses.append(subclasses[i]) + + i = 0 # Go back to the start, as we may have just found another parent class + continue + i += 1 + + log_verbose("\n".join([str(x) for x in test_case_subclasses])) + return test_case_subclasses + + +def main(): + global VERBOSE + + args = parser.parse_args() + + VERBOSE = args.verbose + + testcases = find_tests(args.test_files) + + includes = "\n".join([ INCLUDE_TEMPLATE % { 'file_path' : x } for x in set([y[0] for y in testcases]) ]) + registrations = [] + + for path, class_name, superclasses, funcs in testcases: + BIND_TEMPLATE = "&%(class_name)s::%(func)s" + + members = ", ".join([ BIND_TEMPLATE % { 'class_name' : class_name, 'func' : x } for x in funcs ]) + names = ", ".join([ '"%s::%s"' % (class_name, x) for x in funcs ]) + + registrations.append(REGISTER_TEMPLATE % { 'class_name' : class_name, 'members' : members, 'names' : names }) + + registrations = "\n".join(registrations) + + final = MAIN_TEMPLATE % { + 'registrations' : registrations, + 'includes' : includes + } + + open(args.output[0], "w").write(final) + + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tests/test_nearz_clipping.h b/tests/test_nearz_clipping.h new file mode 100644 index 0000000..62296b1 --- /dev/null +++ b/tests/test_nearz_clipping.h @@ -0,0 +1,10 @@ +#include "../utils/test.h" + + +namespace { + +class NearZClippingTests : gldc::test::GLdcTestCase { + +}; + +} diff --git a/utils/test.h b/utils/test.h new file mode 100644 index 0000000..936c4ac --- /dev/null +++ b/utils/test.h @@ -0,0 +1,456 @@ +/* * Copyright (c) 2011-2017 Luke Benstead https://simulant-engine.appspot.com + * + * This file is part of Simulant. + * + * Simulant is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Simulant is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Simulant. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#define assert_equal(expected, actual) _assert_equal((expected), (actual), __FILE__, __LINE__) +#define assert_not_equal(expected, actual) _assert_not_equal((expected), (actual), __FILE__, __LINE__) +#define assert_false(actual) _assert_false((actual), __FILE__, __LINE__) +#define assert_true(actual) _assert_true((actual), __FILE__, __LINE__) +#define assert_close(expected, actual, difference) _assert_close((expected), (actual), (difference), __FILE__, __LINE__) +#define assert_is_null(actual) _assert_is_null((actual), __FILE__, __LINE__) +#define assert_is_not_null(actual) _assert_is_not_null((actual), __FILE__, __LINE__) +#define assert_raises(exception, func) _assert_raises((func), __FILE__, __LINE__) +#define assert_items_equal(expected, actual) _assert_items_equal((actual), (expected), __FILE__, __LINE__) +#define not_implemented() _not_implemented(__FILE__, __LINE__) + + +namespace gldc { +namespace test { + + +class StringFormatter { +public: + StringFormatter(const std::string& templ): + templ_(templ) { } + + struct Counter { + Counter(uint32_t c): c(c) {} + uint32_t c; + }; + + template + std::string format(T value) { + std::stringstream ss; + ss << value; + return _do_format(0, ss.str()); + } + + template + std::string format(Counter count, T value) { + std::stringstream ss; + ss << value; + return _do_format(count.c, ss.str()); + } + + template + std::string format(T value, const Args&... args) { + std::stringstream ss; + ss << value; + return StringFormatter(_do_format(0, ss.str())).format(Counter(1), args...); + } + + template + std::string format(Counter count, T value, const Args&... args) { + std::stringstream ss; + ss << value; + return StringFormatter(_do_format(count.c, ss.str())).format(Counter(count.c + 1), args...); + } + + std::string _do_format(uint32_t counter, const std::string& value) { + std::stringstream ss; // Can't use to_string on all platforms + ss << counter; + + const std::string to_replace = "{" + ss.str() + "}"; + std::string output = templ_; + + auto replace = [](std::string& str, const std::string& from, const std::string& to) -> bool { + size_t start_pos = str.find(from); + if(start_pos == std::string::npos) + return false; + str.replace(start_pos, from.length(), to); + return true; + }; + + replace(output, to_replace, value); + return output; + } + +private: + std::string templ_; +}; + +class StringSplitter { +public: + StringSplitter(const std::string& str): + str_(str) { + + } + + std::vector split() { + std::vector result; + std::string buffer; + + for(auto c: str_) { + if(c == '\n') { + if(!buffer.empty()) { + result.push_back(buffer); + buffer.clear(); + } + } else { + buffer.push_back(c); + } + } + + if(!buffer.empty()) { + result.push_back(buffer); + } + + return result; + } + +private: + std::string str_; +}; + +typedef StringFormatter _Format; + +class AssertionError : public std::logic_error { +public: + AssertionError(const std::string& what): + std::logic_error(what), + file(""), + line(-1) { + } + + AssertionError(const std::pair file_and_line, const std::string& what): + std::logic_error(what), + file(file_and_line.first), + line(file_and_line.second) { + + } + + ~AssertionError() noexcept (true) { + + } + + std::string file; + int line; +}; + + +class NotImplementedError: public std::logic_error { +public: + NotImplementedError(const std::string& file, int line): + std::logic_error(_Format("Not implemented at {0}:{1}").format(file, line)) {} +}; + + +class SkippedTestError: public std::logic_error { +public: + SkippedTestError(const std::string& reason): + std::logic_error(reason) { + + } +}; + +class TestCase { +public: + virtual ~TestCase() {} + + virtual void set_up() {} + virtual void tear_down() {} + + void skip_if(const bool& flag, const std::string& reason) { + if(flag) { throw test::SkippedTestError(reason); } + } + + template + void _assert_equal(T expected, U actual, std::string file, int line) { + if(expected != actual) { + auto file_and_line = std::make_pair(file, line); + throw test::AssertionError(file_and_line, test::_Format("{0} does not match {1}").format(actual, expected)); + } + } + + template + void _assert_not_equal(T lhs, U rhs, std::string file, int line) { + if(lhs == (T) rhs) { + auto file_and_line = std::make_pair(file, line); + throw test::AssertionError(file_and_line, test::_Format("{0} should not match {1}").format(lhs, rhs)); + } + } + + template + void _assert_true(T actual, std::string file, int line) { + if(!bool(actual)) { + auto file_and_line = std::make_pair(file, line); + throw test::AssertionError(file_and_line, test::_Format("{0} is not true").format(bool(actual) ? "true" : "false")); + } + } + + template + void _assert_false(T actual, std::string file, int line) { + if(bool(actual)) { + auto file_and_line = std::make_pair(file, line); + throw test::AssertionError(file_and_line, test::_Format("{0} is not false").format(bool(actual) ? "true" : "false")); + } + } + + template + void _assert_close(T expected, U actual, V difference, std::string file, int line) { + if(actual < expected - difference || + actual > expected + difference) { + auto file_and_line = std::make_pair(file, line); + throw test::AssertionError(file_and_line, test::_Format("{0} is not close enough to {1}").format(actual, expected)); + } + } + + template + void _assert_is_null(T* thing, std::string file, int line) { + if(thing != nullptr) { + auto file_and_line = std::make_pair(file, line); + throw test::AssertionError(file_and_line, "Pointer was not NULL"); + } + } + + template + void _assert_is_not_null(T* thing, std::string file, int line) { + if(thing == nullptr) { + auto file_and_line = std::make_pair(file, line); + throw test::AssertionError(file_and_line, "Pointer was unexpectedly NULL"); + } + } + + template + void _assert_raises(Func func, std::string file, int line) { + try { + func(); + auto file_and_line = std::make_pair(file, line); + throw test::AssertionError(file_and_line, test::_Format("Expected exception ({0}) was not thrown").format(typeid(T).name())); + } catch(T& e) {} + } + + template + void _assert_items_equal(const T& lhs, const U& rhs, std::string file, int line) { + auto file_and_line = std::make_pair(file, line); + + if(lhs.size() != rhs.size()) { + throw test::AssertionError(file_and_line, "Containers are not the same length"); + } + + for(auto item: lhs) { + if(std::find(rhs.begin(), rhs.end(), item) == rhs.end()) { + throw test::AssertionError(file_and_line, test::_Format("Container does not contain {0}").format(item)); + } + } + } + + void _not_implemented(std::string file, int line) { + throw test::NotImplementedError(file, line); + } +}; + +class TestRunner { +public: + template + void register_case(std::vector methods, std::vector names) { + std::shared_ptr instance = std::make_shared(); + + instances_.push_back(instance); //Hold on to it + + for(std::string name: names) { + names_.push_back(name); + } + + for(U& method: methods) { + std::function func = std::bind(method, dynamic_cast(instance.get())); + tests_.push_back([=]() { + instance->set_up(); + func(); + instance->tear_down(); + }); + } + } + + int32_t run(const std::string& test_case, const std::string& junit_output="") { + int failed = 0; + int skipped = 0; + int ran = 0; + int crashed = 0; + + auto new_tests = tests_; + auto new_names = names_; + + if(!test_case.empty()) { + new_tests.clear(); + new_names.clear(); + + for(uint32_t i = 0; i < names_.size(); ++i) { + if(names_[i].find(test_case) == 0) { + new_tests.push_back(tests_[i]); + new_names.push_back(names_[i]); + } + } + } + + std::cout << std::endl << "Running " << new_tests.size() << " tests" << std::endl << std::endl; + + std::vector junit_lines; + junit_lines.push_back("\n"); + + std::string klass = ""; + + for(std::function test: new_tests) { + std::string name = new_names[ran]; + std::string this_klass(name.begin(), name.begin() + name.find_first_of(":")); + bool close_klass = ran == (int) new_tests.size() - 1; + + if(this_klass != klass) { + if(!klass.empty()) { + junit_lines.push_back(" \n"); + } + klass = this_klass; + junit_lines.push_back(" \n"); + } + + try { + junit_lines.push_back(" \n"); + std::string output = " " + new_names[ran]; + + for(int i = output.length(); i < 76; ++i) { + output += " "; + } + + std::cout << output; + test(); + std::cout << "\033[32m" << " OK " << "\033[0m" << std::endl; + junit_lines.push_back(" \n"); + } catch(test::NotImplementedError& e) { + std::cout << "\033[34m" << " SKIPPED" << "\033[0m" << std::endl; + ++skipped; + junit_lines.push_back(" \n"); + } catch(test::SkippedTestError& e) { + std::cout << "\033[34m" << " SKIPPED" << "\033[0m" << std::endl; + ++skipped; + junit_lines.push_back(" \n"); + } catch(test::AssertionError& e) { + std::cout << "\033[33m" << " FAILED " << "\033[0m" << std::endl; + std::cout << " " << e.what() << std::endl; + if(!e.file.empty()) { + std::cout << " " << e.file << ":" << e.line << std::endl; + + std::ifstream ifs(e.file); + if(ifs.good()) { + std::string buffer; + std::vector lines; + while(std::getline(ifs, buffer)) { + lines.push_back(buffer); + } + + int line_count = lines.size(); + if(line_count && e.line <= line_count) { + std::cout << lines.at(e.line - 1) << std::endl << std::endl; + } + } + } + ++failed; + + junit_lines.push_back(" \n"); + junit_lines.push_back(" \n"); + } catch(std::exception& e) { + std::cout << "\033[31m" << " EXCEPT " << std::endl; + std::cout << " " << e.what() << "\033[0m" << std::endl; + ++crashed; + + junit_lines.push_back(" \n"); + junit_lines.push_back(" \n"); + } + std::cout << "\033[0m"; + ++ran; + + if(close_klass) { + junit_lines.push_back(" \n"); + } + } + + junit_lines.push_back("\n"); + + if(!junit_output.empty()) { + FILE* f = fopen(junit_output.c_str(), "wt"); + if(f) { + for(auto& line: junit_lines) { + fwrite(line.c_str(), sizeof(char), line.length(), f); + } + } + + fclose(f); + } + + std::cout << "-----------------------" << std::endl; + if(!failed && !crashed && !skipped) { + std::cout << "All tests passed" << std::endl << std::endl; + } else { + if(skipped) { + std::cout << skipped << " tests skipped"; + } + + if(failed) { + if(skipped) { + std::cout << ", "; + } + std::cout << failed << " tests failed"; + } + + if(crashed) { + if(failed) { + std::cout << ", "; + } + std::cout << crashed << " tests crashed"; + } + std::cout << std::endl << std::endl; + } + + return failed + crashed; + } + +private: + std::vector> instances_; + std::vector > tests_; + std::vector names_; +}; + +class GLdcTestCase : public TestCase { +public: + virtual void set_up() { + TestCase::set_up(); + } +}; + +} // test +} // gldc +