Add test system supporting grand central dispatch

The motivation for introducing a new test generator is that CxxTest cannot be used with tests that (indirectly) schedule code to run in the main queue.

There are a few other advantages of breaking with CxxTest:

 1. Less boilerplate: A test file need only contain a
    function named with a ‘test_’ prefix. No classes,
    inheritance, or similar. If you need fixtures, use the
    multitude of ways that C/C++ allows that (constructor
    functions or non-POD types with static storage).

 2. Concurrent tests: Test functions are scheduled with
    ‘dispatch_apply’ and will thus run concurrently. If
    you need serial execution you can wrap your tests in a
    block and schedule that to run in the main queue.
    Though you should catch exceptions and re-throw these
    in the test’s original queue, as the test assertions
    are using exceptions.

 3. Easier output of custom types: The assertion macros
    will call ‘to_s’ on the arguments given, so the only
    thing required to make these output nicely is to
    provide a ‘to_s’ overload for your custom type /
    enumeration. I know that the standard way to do this
    is overloading operator<< for a stream, but the
    TextMate code-base already uses the ‘to_s’
    convention.

Long-term I can see a few other advantages, like calling preprocessor on the input files to support #if/#else/#endif to disable tests, better support for Cocoa code (NSRunLoop), and introducing test timeouts.
This commit is contained in:
Allan Odgaard
2013-02-21 15:30:31 +01:00
parent acd3bdf234
commit 01417054cb
2 changed files with 201 additions and 19 deletions

View File

@@ -23,7 +23,7 @@ require 'shellwords'
require 'set'
require 'pp'
GLOB_KEYS = %w{ CP_Resources CP_SharedSupport CP_PlugIns EXPORT HTML_FOOTER HTML_HEADER MARKDOWN_HEADER MARKDOWN_FOOTER PRELUDE SOURCES TARGETS TEST_SOURCES }
GLOB_KEYS = %w{ CP_Resources CP_SharedSupport CP_PlugIns EXPORT HTML_FOOTER HTML_HEADER MARKDOWN_HEADER MARKDOWN_FOOTER PRELUDE SOURCES TARGETS TESTS TEST_SOURCES }
STRING_KEYS = %w{ TARGET_NAME BUNDLE_EXTENSION FLAGS C_FLAGS CXX_FLAGS OBJC_FLAGS OBJCXX_FLAGS LN_FLAGS PLIST_FLAGS BUILD }
COMPILER_INFO = {
@@ -263,11 +263,25 @@ def clean(target)
end
def tests(target)
return '' if target['TEST_SOURCES'].to_s.empty? && target['TESTS'].to_s.empty?
headers = target['LINK'].to_a.map { |goal| "#{goal}/headers" }
dst = build_path("#{target[:path]}/test_#{target[:name]}")
res = ''
unless target['TEST_SOURCES'].to_s.empty?
headers = target['LINK'].to_a.map { |goal| "#{goal}/headers" }
if !target['TESTS'].to_s.empty?
src = target['TESTS'].map { |path| esc_path(File.join(target[:path], path)) }.join(' ')
ext = target['TESTS'].any? { |path| path =~ /\.mm$/ } ? 'mm' : 'cc'
res << "build #{dst}.#{ext}: gen_oak_test #{src} | bin/gen_test\n"
res << "build #{dst}.o: #{compiler_for("#{dst}.#{ext}")} #{dst}.#{ext} | #{pch_for(src, target)}.gch || #{target[:name]}/headers\n"
res << " depfile = #{dst}.o.d\n"
res << " RAVE_FLAGS = -include #{pch_for(src, target)}\n"
res << flags_for_target(target)
res << "build #{dst}: link_oak_test #{all_values_for_key(target, :objects).join(' ')} #{dst}.o\n"
res << flags_for_target(target)
else
src = target['TEST_SOURCES'].map { |path| esc_path(File.join(target[:path], path)) }.join(' ')
dst = build_path("#{target[:path]}/test_#{target[:name]}")
ext = target['TEST_SOURCES'].any? { |path| path =~ /\.mm$/ } ? 'mm' : 'cc'
res << "build #{dst}.#{ext}: gen_test #{src}\n"
@@ -277,22 +291,23 @@ def tests(target)
res << flags_for_target(target)
res << "build #{dst}: link_test #{all_values_for_key(target, :objects).join(' ')} #{dst}.o\n"
res << flags_for_target(target)
fws = prepend('-framework ', all_values_for_key(target, 'FRAMEWORKS'))
libs = prepend('-l', all_values_for_key(target, 'LIBS'))
res << " RAVE_FLAGS = #{fws} #{libs}\n"
res << "build #{dst}.run: run_test #{dst}\n"
res << "build #{dst}.always-run: always_run_test #{dst}\n"
res << "build #{dst}.coerce: skip_test #{dst}\n"
res << " seal = #{dst}.run\n"
res << "build #{target[:name]}: phony #{dst}.run\n"
res << "build #{target[:name]}/test: phony #{dst}.always-run\n"
res << "build #{target[:name]}/coerce: phony #{dst}.coerce\n"
target[:test] = "#{dst}.run"
end
fws = prepend('-framework ', all_values_for_key(target, 'FRAMEWORKS'))
libs = prepend('-l', all_values_for_key(target, 'LIBS'))
res << " RAVE_FLAGS = #{fws} #{libs}\n"
res << "build #{dst}.run: run_test #{dst}\n"
res << "build #{dst}.always-run: always_run_test #{dst}\n"
res << "build #{dst}.coerce: skip_test #{dst}\n"
res << " seal = #{dst}.run\n"
res << "build #{target[:name]}: phony #{dst}.run\n"
res << "build #{target[:name]}/test: phony #{dst}.always-run\n"
res << "build #{target[:name]}/coerce: phony #{dst}.coerce\n"
target[:test] = "#{dst}.run"
res
end
@@ -781,6 +796,14 @@ rule link_test
command = '$CXX' -o $out $in $LN_FLAGS $RAVE_FLAGS
description = Link test $out
rule gen_oak_test
command = bin/gen_test $in > $out~ && mv $out~ $out
description = Generate test $out
rule link_oak_test
command = '$CXX' -o $out $in $LN_FLAGS $RAVE_FLAGS
description = Link test $out
rule run_test
command = $in && touch $out
description = Run test $in

159
bin/gen_test Executable file
View File

@@ -0,0 +1,159 @@
#!/usr/bin/env ruby -wKU
require 'pathname'
require 'erb'
user_code = ""
functions = [ ]
ARGV.each do |file|
if p = Pathname.new(file)
ns = p.basename.sub(/\.[^.]+$/, '')
lineno, includes, body = 1, [ ], [ ]
p.each_line do |line|
if line =~ /^\s*#(include|import)\b/
includes << "#line #{lineno} \"#{p.realpath}\"\n"
includes << line
else
body << "#line #{lineno} \"#{p.realpath}\"\n"
body << line
functions << "#{ns}::#$1" if line =~ /^\s*void ((?:async_)?test_\w+) \(\)/
end
lineno += 1
end
user_code << "#{includes}namespace #{ns} {\n#{body}} /* #{ns} */\n"
end
end
STDOUT << ERB.new(DATA.read, 0, '-<>').result(binding)
__END__
#line 33 "<%= $PROGRAM_NAME %>"
static std::string oak_format (char const* format, ...) __attribute__ ((format (printf, 1, 2)));
static std::string oak_format (char const* format, ...)
{
char* tmp = NULL;
va_list ap;
va_start(ap, format);
vasprintf(&tmp, format, ap);
va_end(ap);
std::string res(tmp);
free(tmp);
return res;
}
std::string to_s (bool value) { return value ? "YES" : "NO"; }
std::string to_s (char value) { return oak_format("%c", value); }
std::string to_s (size_t value) { return oak_format("%zu", value); }
std::string to_s (ssize_t value) { return oak_format("%zd", value); }
std::string to_s (int value) { return oak_format("%d", value); }
std::string to_s (double value) { return oak_format("%g", value); }
std::string to_s (char const* value) { return value == NULL ? "«NULL»" : oak_format("\"%s\"", value); }
std::string to_s (std::string const& value) { return value == NULL_STR ? "«NULL_STR»" : "\"" + value + "\""; }
template <typename _X, typename _Y>
std::string to_s (std::pair<_X, _Y> const& pair)
{
return "pair<" + to_s(pair.first) + ", " + to_s(pair.second) + ">";
}
template <typename _T>
std::string to_s (_T const& container)
{
std::string res = "( ";
for(auto element : container)
res += to_s(element) + ", ";
res += ")";
return res;
}
void oak_warning (std::string const& message, char const* file, int line)
{
fprintf(stderr, "%s:%d: %s\n", file, line, message.c_str());
}
struct oak_exception : std::exception
{
oak_exception (std::string const& message) : _message(message) { }
virtual char const* what () const throw() { return _message.c_str(); }
private:
std::string _message;
};
void oak_assertion_error (std::string const& message, char const* file, int line)
{
throw oak_exception(oak_format("%s:%d: ", file, line) + message);
}
std::string oak_format_bad_relation (char const* lhs, char const* op, char const* rhs, std::string const& realLHS, char const* realOp, std::string const& realRHS)
{
return oak_format("Expected (%s %s %s), found (%s %s %s)", lhs, op, rhs, realLHS.c_str(), realOp, realRHS.c_str());
}
#define OAK_WARN(msg) oak_warning(msg, __FILE__, __LINE__)
#define OAK_FAIL(msg) oak_assertion_error(msg, __FILE__, __LINE__)
#define OAK_ASSERT(expr) if(!(expr)) oak_assertion_error(oak_format("Assertion failed: %s", #expr), __FILE__, __LINE__)
#define OAK_ASSERT_LT(lhs, rhs) if(!((lhs) < (rhs))) oak_assertion_error(oak_format_bad_relation(#lhs, "<", #rhs, to_s(lhs), ">=", to_s(rhs)), __FILE__, __LINE__)
#define OAK_ASSERT_LE(lhs, rhs) if(!((lhs) <= (rhs))) oak_assertion_error(oak_format_bad_relation(#lhs, "<=", #rhs, to_s(lhs), ">", to_s(rhs)), __FILE__, __LINE__)
#define OAK_ASSERT_GT(lhs, rhs) if(!((lhs) > (rhs))) oak_assertion_error(oak_format_bad_relation(#lhs, ">", #rhs, to_s(lhs), "<=", to_s(rhs)), __FILE__, __LINE__)
#define OAK_ASSERT_GE(lhs, rhs) if(!((lhs) >= (rhs))) oak_assertion_error(oak_format_bad_relation(#lhs, ">=", #rhs, to_s(lhs), "<", to_s(rhs)), __FILE__, __LINE__)
#define OAK_ASSERT_EQ(lhs, rhs) if(!((lhs) == (rhs))) oak_assertion_error(oak_format_bad_relation(#lhs, "==", #rhs, to_s(lhs), "!=", to_s(rhs)), __FILE__, __LINE__)
#define OAK_ASSERT_NE(lhs, rhs) if(!((lhs) != (rhs))) oak_assertion_error(oak_format_bad_relation(#lhs, "!=", #rhs, to_s(lhs), "==", to_s(rhs)), __FILE__, __LINE__)
#define OAK_MASSERT(msg, expr) if(!(expr)) oak_assertion_error(msg, __FILE__, __LINE__)
#define OAK_MASSERT_LT(msg, lhs, rhs) if(!((lhs) < (rhs))) oak_assertion_error(msg, __FILE__, __LINE__)
#define OAK_MASSERT_LE(msg, lhs, rhs) if(!((lhs) <= (rhs))) oak_assertion_error(msg, __FILE__, __LINE__)
#define OAK_MASSERT_GT(msg, lhs, rhs) if(!((lhs) > (rhs))) oak_assertion_error(msg, __FILE__, __LINE__)
#define OAK_MASSERT_GE(msg, lhs, rhs) if(!((lhs) >= (rhs))) oak_assertion_error(msg, __FILE__, __LINE__)
#define OAK_MASSERT_EQ(msg, lhs, rhs) if(!((lhs) == (rhs))) oak_assertion_error(msg, __FILE__, __LINE__)
#define OAK_MASSERT_NE(msg, lhs, rhs) if(!((lhs) != (rhs))) oak_assertion_error(msg, __FILE__, __LINE__)
<%= user_code %>
#line 117 "<%= $PROGRAM_NAME %>"
int main (int argc, char const* argv[])
{
struct test_t
{
test_t (void (*f)()) : run(f) { }
void (*run)();
std::string message;
};
__block std::vector<test_t> tests;
<%= functions.map { |fun| "\ttests.emplace_back(&#{fun});" }.join("\n") %>
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_apply(tests.size(), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t n){
try {
tests[n].run();
}
catch(std::exception const& e) {
tests[n].message = e.what();
}
});
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
std::vector<test_t> failed;
std::copy_if(tests.begin(), tests.end(), back_inserter(failed), [](test_t const& t){ return !t.message.empty(); });
if(failed.empty())
exit(0);
fprintf(stderr, "%s: %zu of %zu %s failed:\n", getprogname(), failed.size(), tests.size(), tests.size() == 1 ? "test" : "tests");
for(auto test : failed)
fprintf(stderr, "%s\n", test.message.c_str());
exit(1);
});
dispatch_main();
return 0;
}