mirror of
https://github.com/textmate/textmate.git
synced 2026-04-28 03:00:34 -04:00
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:
@@ -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
159
bin/gen_test
Executable 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;
|
||||
}
|
||||
Reference in New Issue
Block a user