test-runner 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/test-runner +8 -0
- data/lib/test_runner.rb +38 -0
- data/lib/test_runner/assert.rb +15 -0
- data/lib/test_runner/assert/assertion.rb +116 -0
- data/lib/test_runner/assert/check.rb +53 -0
- data/lib/test_runner/assert/checks.rb +76 -0
- data/lib/test_runner/assert/checks/registry.rb +31 -0
- data/lib/test_runner/assert/errors.rb +46 -0
- data/lib/test_runner/assert/syntax.rb +94 -0
- data/lib/test_runner/backtrace_filter.rb +25 -0
- data/lib/test_runner/cli.rb +196 -0
- data/lib/test_runner/colored_logger.rb +62 -0
- data/lib/test_runner/config.rb +77 -0
- data/lib/test_runner/runner.rb +146 -0
- data/lib/test_runner/script.rb +2 -0
- data/lib/test_runner/util.rb +66 -0
- metadata +74 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1a7128fa65cf1e03aeb992c6e20a26ae50a3d1bc
|
4
|
+
data.tar.gz: cd3992292f6c60cb9916877f7512095c22a536fc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e2971ffd2f61a4315928da41273c2c55f9c9a113ac90f27b2133425dbbf7e0897203886b25a0f447c98e9f06270c1f93e77617a05e319be73c5038ae7c353842
|
7
|
+
data.tar.gz: 226c1c6b629d0ccd23d43ed502b97dd232d9c0043dea6dded2f0469976b7a5b591e4d48e4cbd97fd57357937df13cf2c53ab60a65f9ff6d88ba83230419c0ca2
|
data/bin/test-runner
ADDED
data/lib/test_runner.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require "logger"
|
2
|
+
require "logger/logging"
|
3
|
+
|
4
|
+
module TestRunner
|
5
|
+
autoload :Assert, "test_runner/assert"
|
6
|
+
autoload :BacktraceFilter, "test_runner/backtrace_filter"
|
7
|
+
autoload :ColoredLogger, "test_runner/colored_logger"
|
8
|
+
autoload :Config, "test_runner/config"
|
9
|
+
autoload :CLI, "test_runner/cli"
|
10
|
+
autoload :Runner, "test_runner/runner"
|
11
|
+
autoload :Util, "test_runner/util"
|
12
|
+
|
13
|
+
# TestRunner can be included in scripts as a mixin, e.g.
|
14
|
+
#
|
15
|
+
# require "test_runner"
|
16
|
+
# include TestRunner
|
17
|
+
#
|
18
|
+
# logger.info "hi"
|
19
|
+
# assert true
|
20
|
+
#
|
21
|
+
# You can also require "test_runner/script" to automatically include TestRunner
|
22
|
+
def logger
|
23
|
+
Config.logger
|
24
|
+
end
|
25
|
+
|
26
|
+
# Describe, context, it, and specify can help break up a larger test script
|
27
|
+
# into chunks. Blocks are run # immediately; this is cosmetic enhancement
|
28
|
+
# that also enables some degree of compatibility with other frameworks.
|
29
|
+
%i(context describe it specify).each do |method_name|
|
30
|
+
define_method method_name do |msg, &blk|
|
31
|
+
blk.() if blk
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.included target
|
36
|
+
Assert::Syntax.infect target
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative "assert/assertion"
|
2
|
+
require_relative "assert/check"
|
3
|
+
require_relative "assert/checks"
|
4
|
+
require_relative "assert/errors"
|
5
|
+
require_relative "assert/syntax"
|
6
|
+
|
7
|
+
module TestRunner
|
8
|
+
module Assert
|
9
|
+
def self.inspect object, truncate = 100
|
10
|
+
raw = object.inspect
|
11
|
+
return raw if raw.size <= truncate
|
12
|
+
"#{raw[0..truncate - 2]} …\""
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module TestRunner
|
2
|
+
module Assert
|
3
|
+
class Assertion
|
4
|
+
attr_reader :fails
|
5
|
+
attr_reader :passes
|
6
|
+
attr_accessor :source
|
7
|
+
|
8
|
+
def initialize subject_proc, checks
|
9
|
+
@subject_proc = subject_proc
|
10
|
+
@checks = checks
|
11
|
+
@passes = []
|
12
|
+
@fails = []
|
13
|
+
@trace = caller_locations
|
14
|
+
end
|
15
|
+
|
16
|
+
def call
|
17
|
+
perform_checks
|
18
|
+
freeze
|
19
|
+
raise to_error if failed?
|
20
|
+
subject
|
21
|
+
end
|
22
|
+
|
23
|
+
def build_checks
|
24
|
+
@checks.map do |check_name, argument|
|
25
|
+
TestRunner::Config.internal_logger.debug "Resolving check #{check_name.inspect}, arg=#{argument.inspect}"
|
26
|
+
resolve_check check_name, argument
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def failed?
|
31
|
+
fails.any?
|
32
|
+
end
|
33
|
+
|
34
|
+
def file
|
35
|
+
@trace[0].path
|
36
|
+
end
|
37
|
+
|
38
|
+
def freeze
|
39
|
+
passes.freeze
|
40
|
+
fails.freeze
|
41
|
+
end
|
42
|
+
|
43
|
+
def line
|
44
|
+
@trace[0].lineno
|
45
|
+
end
|
46
|
+
|
47
|
+
def passed?
|
48
|
+
not failed?
|
49
|
+
end
|
50
|
+
|
51
|
+
def perform_checks
|
52
|
+
build_checks.each do |check|
|
53
|
+
check.evaluate
|
54
|
+
ary = if check.passed? then passes else fails end
|
55
|
+
ary.push check
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def resolve_check check_name, argument
|
60
|
+
check = Checks.resolve check_name do
|
61
|
+
check_name = :include if check_name == :includes
|
62
|
+
argument = [check_name, *argument]
|
63
|
+
Checks[:predicate]
|
64
|
+
end
|
65
|
+
check.new subject_thunk, argument
|
66
|
+
end
|
67
|
+
|
68
|
+
def subject
|
69
|
+
subject_thunk.call
|
70
|
+
end
|
71
|
+
|
72
|
+
def subject_thunk
|
73
|
+
@subject_thunk ||= SubjectThunk.new @subject_proc
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_error
|
77
|
+
AssertionFailed.new self
|
78
|
+
end
|
79
|
+
|
80
|
+
def trace
|
81
|
+
BacktraceFilter.(@trace)
|
82
|
+
end
|
83
|
+
|
84
|
+
class Refutation < Assertion
|
85
|
+
def build_checks
|
86
|
+
super.each &:negate
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class SubjectThunk
|
91
|
+
def initialize block
|
92
|
+
@block = block
|
93
|
+
end
|
94
|
+
|
95
|
+
def call
|
96
|
+
return @subject if subject_resolved?
|
97
|
+
@subject = @block.call
|
98
|
+
end
|
99
|
+
|
100
|
+
def expect_error
|
101
|
+
raise "called after initially fetched" if subject_resolved?
|
102
|
+
@block.call
|
103
|
+
nothing_raised = true
|
104
|
+
rescue => error
|
105
|
+
@subject = error
|
106
|
+
ensure
|
107
|
+
raise NothingRaised.new if nothing_raised
|
108
|
+
end
|
109
|
+
|
110
|
+
def subject_resolved?
|
111
|
+
instance_variable_defined? :@subject
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module TestRunner
|
2
|
+
module Assert
|
3
|
+
class Check
|
4
|
+
singleton_class.send :attr_reader, :block, :name
|
5
|
+
|
6
|
+
attr :argument
|
7
|
+
|
8
|
+
def initialize subject_thunk, argument
|
9
|
+
@argument = argument
|
10
|
+
@subject_thunk = subject_thunk
|
11
|
+
end
|
12
|
+
|
13
|
+
def evaluate
|
14
|
+
self.class.block.call self, @argument
|
15
|
+
freeze
|
16
|
+
end
|
17
|
+
|
18
|
+
def expect_error
|
19
|
+
@subject_thunk.expect_error
|
20
|
+
end
|
21
|
+
|
22
|
+
def fail message
|
23
|
+
passed = negated? ^ yield
|
24
|
+
@fail_message = message unless passed
|
25
|
+
end
|
26
|
+
|
27
|
+
def fail_message
|
28
|
+
"expected #{Assert.inspect subject} to#{" not" if negated?} #{@fail_message}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def failed?
|
32
|
+
@fail_message ? true : false
|
33
|
+
end
|
34
|
+
|
35
|
+
def negated?
|
36
|
+
@negated ? true : false
|
37
|
+
end
|
38
|
+
|
39
|
+
def negate
|
40
|
+
@negated = !@negated
|
41
|
+
end
|
42
|
+
|
43
|
+
def passed?
|
44
|
+
not failed?
|
45
|
+
end
|
46
|
+
|
47
|
+
def subject
|
48
|
+
@expected_error or @subject_thunk.call
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module TestRunner
|
2
|
+
module Assert
|
3
|
+
module Checks
|
4
|
+
require_relative "checks/registry"
|
5
|
+
extend Registry
|
6
|
+
|
7
|
+
register :equals do |check, object|
|
8
|
+
obj_inspect = Assert.inspect object
|
9
|
+
|
10
|
+
if Assert.inspect(check.subject) == obj_inspect and not check.negated?
|
11
|
+
fail_message = "equal other object (no difference in #inspect output)"
|
12
|
+
else
|
13
|
+
fail_message = "equal #{obj_inspect}"
|
14
|
+
end
|
15
|
+
|
16
|
+
check.fail fail_message do check.subject == object end
|
17
|
+
end
|
18
|
+
|
19
|
+
register :included_in do |check, list|
|
20
|
+
check.fail "be included in #{Assert.inspect list}" do
|
21
|
+
list.include? check.subject
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
register :kind_of do |check, type|
|
26
|
+
check.fail "be a kind of #{type}" do
|
27
|
+
check.subject.kind_of? type
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
register :matches do |check, matcher|
|
32
|
+
check.fail "match #{Assert.inspect matcher}" do
|
33
|
+
check.subject.match matcher
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
register :predicate do |check, argument|
|
38
|
+
name, *args = argument
|
39
|
+
method_name = "#{name}?"
|
40
|
+
method = check.subject.method method_name
|
41
|
+
|
42
|
+
if method.arity == 0
|
43
|
+
expect_truth = args.all?
|
44
|
+
message = "be #{name}"
|
45
|
+
result = check.subject.public_send method_name
|
46
|
+
result = result ^ !expect_truth
|
47
|
+
else
|
48
|
+
result = check.subject.public_send method_name, *args
|
49
|
+
args = args.map &Assert.method(:inspect)
|
50
|
+
message = "#{name} #{args * ', '}"
|
51
|
+
end
|
52
|
+
|
53
|
+
check.fail message do result end
|
54
|
+
end
|
55
|
+
|
56
|
+
register :raises do |check, error_type|
|
57
|
+
check.expect_error
|
58
|
+
check.fail "be a #{Assert.inspect error_type}" do
|
59
|
+
check.subject.is_a? error_type
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
register :responds_to do |check, method_name|
|
64
|
+
check.fail "respond to ##{method_name}" do
|
65
|
+
check.subject.respond_to? method_name
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
register :truthy do |check, arg|
|
70
|
+
check.fail "be #{arg ? "truthy" : "falsey"}" do
|
71
|
+
!arg ^ check.subject
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module TestRunner
|
2
|
+
module Assert
|
3
|
+
module Checks
|
4
|
+
module Registry
|
5
|
+
def self.extended base
|
6
|
+
base.instance_variable_set :@registry, {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.define check_name, block
|
10
|
+
klass = Class.new Check
|
11
|
+
klass.instance_variable_set :@block, block
|
12
|
+
constant_name = Util.to_camel_case check_name
|
13
|
+
const_set constant_name, klass
|
14
|
+
klass
|
15
|
+
end
|
16
|
+
|
17
|
+
attr :registry
|
18
|
+
|
19
|
+
def register check_name, &block
|
20
|
+
registry[check_name] = Registry.define check_name, block
|
21
|
+
end
|
22
|
+
|
23
|
+
def resolve check_name, &block
|
24
|
+
block ||= -> * do raise MissingCheck.new check_name end
|
25
|
+
registry.fetch check_name, &block
|
26
|
+
end
|
27
|
+
alias_method :[], :resolve
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module TestRunner
|
2
|
+
module Assert
|
3
|
+
Error = Class.new StandardError
|
4
|
+
|
5
|
+
class AssertionFailed < Error
|
6
|
+
attr :assertion
|
7
|
+
|
8
|
+
def initialize assertion
|
9
|
+
@assertion = assertion
|
10
|
+
end
|
11
|
+
|
12
|
+
def backtrace
|
13
|
+
assertion.trace.map &:to_s
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
failures = assertion.fails.map do |failed_check|
|
18
|
+
failed_check.fail_message
|
19
|
+
end
|
20
|
+
if failures.size > 1
|
21
|
+
"Assertion failure:\n\n * #{failures * "\n * "}\n"
|
22
|
+
else
|
23
|
+
"Assertion failure: #{failures * ", "}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class MissingCheck < Error
|
29
|
+
attr :check_name
|
30
|
+
|
31
|
+
def initialize check_name
|
32
|
+
@check_name = check_name
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
"could not resolve check #{check_name.inspect}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class NothingRaised < Error
|
41
|
+
def to_s
|
42
|
+
"expected subject block to raise an error"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module TestRunner
|
2
|
+
module Assert
|
3
|
+
class Syntax < Module
|
4
|
+
MISSING = :__test_runner_assert_arg_missing__
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def infect target
|
8
|
+
instance = new
|
9
|
+
if target.is_a? Class
|
10
|
+
target.send :include, instance
|
11
|
+
else
|
12
|
+
target.extend instance
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Public: assert is designed to be called a number of different ways,
|
17
|
+
# from 0 to 2 positional arguments, and maybe a block.
|
18
|
+
#
|
19
|
+
# subject_or_checks - the first optional argument passed in
|
20
|
+
# checks - the second optional argument passed in
|
21
|
+
# block - the &block parameter
|
22
|
+
#
|
23
|
+
# Valid examples:
|
24
|
+
#
|
25
|
+
# assert :raises => Error do … end
|
26
|
+
# assert "foo"
|
27
|
+
# assert 3, :included_in => [6, 3]
|
28
|
+
# assert [6, 3], :includes => 3
|
29
|
+
# assert :equal => 4 do 2 + 2 end
|
30
|
+
#
|
31
|
+
# Invalid examples:
|
32
|
+
#
|
33
|
+
# # Can't determine if the block or 2 is the subject
|
34
|
+
# assert 2, :equals => 4 do ̒… end
|
35
|
+
# # There's no subject at all
|
36
|
+
# assert :incuded_in => 4
|
37
|
+
#
|
38
|
+
# Returns two arguments, the subject, and a checks hash. If the checks
|
39
|
+
# would be empty, returns { :truthy => true }. The subject will always be
|
40
|
+
# a Proc that gets lazy evaluated when the assertion is checked.
|
41
|
+
def decode_assert_arguments subject_or_checks, checks, block
|
42
|
+
if checks == MISSING
|
43
|
+
if subject_or_checks == MISSING
|
44
|
+
missing_subject! unless block
|
45
|
+
subject_thunk = block
|
46
|
+
checks = { :truthy => true }
|
47
|
+
elsif block
|
48
|
+
ambiguous_subject! unless subject_or_checks.is_a? Hash
|
49
|
+
subject_thunk = block
|
50
|
+
checks = subject_or_checks
|
51
|
+
else
|
52
|
+
subject_thunk = -> do subject_or_checks end
|
53
|
+
checks = { :truthy => true }
|
54
|
+
end
|
55
|
+
else
|
56
|
+
ambiguous_subject! if block
|
57
|
+
subject_thunk = -> do subject_or_checks end
|
58
|
+
end
|
59
|
+
[subject_thunk, checks]
|
60
|
+
end
|
61
|
+
|
62
|
+
def ambiguous_subject!
|
63
|
+
raise ArgumentError, "cannot supply a block subject *and* a positional subject"
|
64
|
+
end
|
65
|
+
|
66
|
+
def missing_subject!
|
67
|
+
raise ArgumentError, "must supply either a positional subject *or* a block subject (but not both)"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def extended base
|
72
|
+
graft base.singleton_class
|
73
|
+
end
|
74
|
+
|
75
|
+
def included base
|
76
|
+
graft base
|
77
|
+
end
|
78
|
+
|
79
|
+
def graft base
|
80
|
+
method_body = -> method_name, assertion_class, syntax do
|
81
|
+
define_method method_name do |arg1 = MISSING, arg2 = MISSING, &block|
|
82
|
+
subject_thunk, checks = syntax.decode_assert_arguments arg1, arg2, block
|
83
|
+
assertion = assertion_class.new subject_thunk, checks
|
84
|
+
assertion.source = self
|
85
|
+
assertion.()
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
base.class_exec :assert, Assertion, self.class, &method_body
|
90
|
+
base.class_exec :refute, Assertion::Refutation, self.class, &method_body
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module TestRunner
|
2
|
+
module BacktraceFilter
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def call trace
|
6
|
+
return trace unless Config.trim_backtraces
|
7
|
+
|
8
|
+
entry_point = trace.last
|
9
|
+
test_runner_root = File.expand_path "../..", __FILE__
|
10
|
+
|
11
|
+
first_pass = trace.drop_while do |location|
|
12
|
+
full_path = File.expand_path location.to_s
|
13
|
+
full_path.start_with? test_runner_root
|
14
|
+
end
|
15
|
+
|
16
|
+
second_pass = first_pass.take_while do |location|
|
17
|
+
full_path = File.expand_path location.to_s
|
18
|
+
not full_path.start_with? test_runner_root
|
19
|
+
end
|
20
|
+
|
21
|
+
second_pass << entry_point unless second_pass.last == entry_point
|
22
|
+
second_pass
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
require "optparse"
|
2
|
+
|
3
|
+
module TestRunner
|
4
|
+
class CLI
|
5
|
+
def self.run argv
|
6
|
+
instance = new argv, $stdout
|
7
|
+
instance.()
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :options
|
11
|
+
|
12
|
+
def initialize argv, stdout
|
13
|
+
@argv = argv
|
14
|
+
@stdout = stdout
|
15
|
+
end
|
16
|
+
|
17
|
+
def call
|
18
|
+
@options = ArgvParser.(@argv)
|
19
|
+
setup_config
|
20
|
+
Runner.(filter_files) or exit 1
|
21
|
+
end
|
22
|
+
|
23
|
+
def setup_config
|
24
|
+
Config.child_count = options.child_count
|
25
|
+
Config.fail_fast = options.fail_fast?
|
26
|
+
Config.logger = build_logger
|
27
|
+
Config.reverse_backtraces = options.reverse_backtraces?
|
28
|
+
Config.trim_backtraces = options.trim_backtraces?
|
29
|
+
end
|
30
|
+
|
31
|
+
def build_logger
|
32
|
+
logger = Logger.new @stdout
|
33
|
+
logger.level = options.log_level
|
34
|
+
logger.progname = "test_runner"
|
35
|
+
logger
|
36
|
+
end
|
37
|
+
|
38
|
+
def filter_files
|
39
|
+
resolve_files.reject do |path|
|
40
|
+
path.end_with? "init.rb"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def resolve_files
|
45
|
+
options.paths.flat_map do |path|
|
46
|
+
if path.end_with? ".rb"
|
47
|
+
[path]
|
48
|
+
else
|
49
|
+
Dir[File.join path, "**/*.rb"]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Options
|
55
|
+
def self.build
|
56
|
+
if ENV["TEST_RUNNER_VERBOSE"]
|
57
|
+
log_level = Logger::DEBUG
|
58
|
+
else
|
59
|
+
log_level = Logger::INFO
|
60
|
+
end
|
61
|
+
new log_level
|
62
|
+
end
|
63
|
+
|
64
|
+
attr_reader :child_count
|
65
|
+
attr_reader :log_level
|
66
|
+
attr_reader :paths
|
67
|
+
|
68
|
+
def initialize log_level
|
69
|
+
@paths = []
|
70
|
+
@log_level = log_level
|
71
|
+
end
|
72
|
+
|
73
|
+
def add_path path
|
74
|
+
paths << path
|
75
|
+
end
|
76
|
+
|
77
|
+
def child_count= number
|
78
|
+
@child_count = number.to_i
|
79
|
+
end
|
80
|
+
|
81
|
+
def fail_fast
|
82
|
+
@fail_fast = !@fail_fast
|
83
|
+
end
|
84
|
+
|
85
|
+
def fail_fast?
|
86
|
+
@fail_fast
|
87
|
+
end
|
88
|
+
|
89
|
+
def full_backtrace
|
90
|
+
@full_backtrace = !@full_backtrace
|
91
|
+
end
|
92
|
+
|
93
|
+
def trim_backtraces?
|
94
|
+
return nil if @full_backtrace.nil?
|
95
|
+
not @full_backtrace
|
96
|
+
end
|
97
|
+
|
98
|
+
def quiet
|
99
|
+
@log_level += 1
|
100
|
+
end
|
101
|
+
|
102
|
+
def reverse_backtraces
|
103
|
+
@reverse_backtraces = !@reverse_backtraces
|
104
|
+
end
|
105
|
+
|
106
|
+
def reverse_backtraces?
|
107
|
+
@reverse_backtraces
|
108
|
+
end
|
109
|
+
|
110
|
+
def verbose
|
111
|
+
@log_level -= 1
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class ArgvParser
|
116
|
+
def self.call argv
|
117
|
+
options = Options.build
|
118
|
+
parser = ArgvParser.new argv, options
|
119
|
+
parser.()
|
120
|
+
options
|
121
|
+
end
|
122
|
+
|
123
|
+
def initialize argv, options
|
124
|
+
@argv = argv
|
125
|
+
@options = options
|
126
|
+
end
|
127
|
+
|
128
|
+
def call
|
129
|
+
parse_options
|
130
|
+
add_paths
|
131
|
+
end
|
132
|
+
|
133
|
+
def add_paths
|
134
|
+
@argv << "tests" if @argv.empty?
|
135
|
+
@argv.each do |path|
|
136
|
+
@options.add_path path
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def parse_options
|
141
|
+
OptionParser.new do |opts|
|
142
|
+
opts.banner = <<-BANNER
|
143
|
+
Usage: #{program_name} [options] [PATH1] [PATH2] … [PATHn]
|
144
|
+
|
145
|
+
If no PATH(s) are specified, ./tests is assumed
|
146
|
+
|
147
|
+
BANNER
|
148
|
+
|
149
|
+
opts.on "-b", "--full-backtrace", "Do not filter assertion stack traces" do
|
150
|
+
@options.full_backtrace
|
151
|
+
end
|
152
|
+
|
153
|
+
opts.on "-f", "--fail-fast", "When any test script fails, exit immediately" do
|
154
|
+
@options.fail_fast
|
155
|
+
end
|
156
|
+
|
157
|
+
opts.on "-h", "--help", "Print this message and exit successfully" do
|
158
|
+
puts opts
|
159
|
+
exit 0
|
160
|
+
end
|
161
|
+
|
162
|
+
opts.on "-m", "--monkeypatch", "Turn on monkeypatching (equivalent to `require \"test_runner/script\"'" do
|
163
|
+
require "test_runner/script"
|
164
|
+
end
|
165
|
+
|
166
|
+
opts.on "-n", "--num=COUNT", "Max number of sub processes to run concurrently" do |num|
|
167
|
+
@options.child_count = num
|
168
|
+
end
|
169
|
+
|
170
|
+
opts.on "-q", "--quiet", "Reduces log verbosity" do
|
171
|
+
@options.quiet
|
172
|
+
end
|
173
|
+
|
174
|
+
opts.on "-r", "--reverse-traces", "Reverse the order of stack traces" do
|
175
|
+
@options.reverse_backtraces
|
176
|
+
end
|
177
|
+
|
178
|
+
opts.on "-V", "--version", "Print version and exit successfully" do
|
179
|
+
spec = Gem.loaded_specs["test_runner"]
|
180
|
+
version = if spec then spec.version else "(local)" end
|
181
|
+
puts "Version: #{version}"
|
182
|
+
exit 0
|
183
|
+
end
|
184
|
+
|
185
|
+
opts.on "-v", "--verbose", "Increases log verbosity" do
|
186
|
+
@options.verbose
|
187
|
+
end
|
188
|
+
end.parse! @argv
|
189
|
+
end
|
190
|
+
|
191
|
+
def program_name
|
192
|
+
File.basename $PROGRAM_NAME
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module TestRunner
|
2
|
+
class ColoredLogger
|
3
|
+
ANSI_COLORS = %i(black red green yellow blue magenta cyan white)
|
4
|
+
|
5
|
+
DEFAULT_PALETTE = {
|
6
|
+
:unknown => -> msg { bg(:blue, :bright, fg(:white, :bright, msg)) },
|
7
|
+
:fatal => -> msg { bg(:red, :bright, fg(:white, :bright, msg)) },
|
8
|
+
:error => -> msg { fg(:red, :bright, msg) },
|
9
|
+
:warn => -> msg { fg(:yellow, :bright, msg) },
|
10
|
+
:info => -> msg { fg(:default, :normal, msg) },
|
11
|
+
:debug => -> msg { fg(:cyan, :bright, msg) },
|
12
|
+
}
|
13
|
+
|
14
|
+
attr_reader :palette
|
15
|
+
|
16
|
+
def initialize io, palette = DEFAULT_PALETTE
|
17
|
+
@logger = Logger.new io
|
18
|
+
@palette = palette
|
19
|
+
end
|
20
|
+
|
21
|
+
%i(level= progname=).each do |method_name|
|
22
|
+
define_method method_name do |*args, &block|
|
23
|
+
@logger.public_send method_name, *args, &block
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
%i(unknown fatal error warn info debug).each do |log_level|
|
28
|
+
define_method log_level do |msg_or_progname = nil, &orig_block|
|
29
|
+
colored_msg = format log_level, msg_or_progname
|
30
|
+
|
31
|
+
if orig_block
|
32
|
+
progname = colored_msg
|
33
|
+
block = -> do format log_level, orig_block.() end
|
34
|
+
else
|
35
|
+
block = -> do colored_msg end
|
36
|
+
end
|
37
|
+
|
38
|
+
@logger.public_send log_level, progname, &block
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def format log_level, msg
|
43
|
+
formatter = palette.fetch log_level
|
44
|
+
instance_exec msg, &formatter
|
45
|
+
end
|
46
|
+
|
47
|
+
def fg *args
|
48
|
+
col :fg, *args
|
49
|
+
end
|
50
|
+
|
51
|
+
def bg *args
|
52
|
+
col :bg, *args
|
53
|
+
end
|
54
|
+
|
55
|
+
def col fgbg, color_code, intensity_code, str
|
56
|
+
color_num = ANSI_COLORS.index color_code
|
57
|
+
intensity_num = { :normal => 0, :bright => 1 }.fetch intensity_code
|
58
|
+
fgbg_num = { :fg => 3, :bg => 4 }.fetch fgbg
|
59
|
+
"\e[#{fgbg_num}#{color_num};#{intensity_num}m#{str}\e[0m"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module TestRunner
|
2
|
+
module Config
|
3
|
+
extend self
|
4
|
+
|
5
|
+
extend Logger::Logging
|
6
|
+
|
7
|
+
attr_writer :child_count
|
8
|
+
attr_writer :fail_fast
|
9
|
+
attr_writer :internal_logger
|
10
|
+
attr_writer :reverse_backtraces
|
11
|
+
attr_writer :trim_backtraces
|
12
|
+
|
13
|
+
def child_count
|
14
|
+
Option::Number.evaluate @child_count do
|
15
|
+
ENV.fetch "TEST_RUNNER_CHILD_COUNT", 1
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def fail_fast
|
20
|
+
Option::Boolean.evaluate @fail_fast do
|
21
|
+
ENV["TEST_RUNNER_FAIL_FAST"]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def internal_logger
|
26
|
+
if ENV["TEST_RUNNER_INTERNAL_LOGGING"]
|
27
|
+
logger
|
28
|
+
else
|
29
|
+
Logger::Logging::NullLogger
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def reverse_backtraces
|
34
|
+
Option::Boolean.evaluate @reverse_backtraces do
|
35
|
+
ENV["TEST_RUNNER_REVERSE_BACKTRACES"]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def trim_backtraces
|
40
|
+
Option::Boolean.evaluate @trim_backtraces do
|
41
|
+
ENV.fetch "TEST_RUNNER_TRIM_BACKTRACES", true
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Option
|
46
|
+
def self.evaluate value, &block
|
47
|
+
instance = new value
|
48
|
+
instance.fetch &block
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize value
|
52
|
+
@value = value
|
53
|
+
end
|
54
|
+
|
55
|
+
def fetch
|
56
|
+
@value = yield unless set?
|
57
|
+
value
|
58
|
+
end
|
59
|
+
|
60
|
+
def set?
|
61
|
+
not @value.nil?
|
62
|
+
end
|
63
|
+
|
64
|
+
class Number < Option
|
65
|
+
def value
|
66
|
+
@value.to_i
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class Boolean < Option
|
71
|
+
def value
|
72
|
+
not [nil, false, "", "0", "n", "no", "false"].include? @value
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
module TestRunner
|
2
|
+
class Runner
|
3
|
+
def self.call paths
|
4
|
+
instance = new paths
|
5
|
+
instance.call
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :files
|
9
|
+
attr_reader :pids
|
10
|
+
|
11
|
+
def initialize files
|
12
|
+
@files = files
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
Config.internal_logger.debug "test_runner found #{files.size} files: #{files * ", "}"
|
17
|
+
return if files.empty?
|
18
|
+
result = run_all
|
19
|
+
|
20
|
+
log_msg = "finished executing files; success=#{result.inspect}"
|
21
|
+
if result
|
22
|
+
Config.logger.info log_msg
|
23
|
+
else
|
24
|
+
Config.logger.warn log_msg
|
25
|
+
end
|
26
|
+
|
27
|
+
result
|
28
|
+
end
|
29
|
+
|
30
|
+
def run_all
|
31
|
+
set = ProcessSet.new
|
32
|
+
Signal.trap "INT" do set.shutdown end
|
33
|
+
set << files.shift until files.empty?
|
34
|
+
set.finish
|
35
|
+
end
|
36
|
+
|
37
|
+
class ProcessSet
|
38
|
+
def initialize
|
39
|
+
@set = []
|
40
|
+
@passed = true
|
41
|
+
end
|
42
|
+
|
43
|
+
def << file
|
44
|
+
wait Config.child_count - 1
|
45
|
+
@set.<< spawn_child file
|
46
|
+
end
|
47
|
+
|
48
|
+
def finish
|
49
|
+
wait 0
|
50
|
+
@passed
|
51
|
+
end
|
52
|
+
|
53
|
+
def wait max_count
|
54
|
+
tick while @set.size > max_count
|
55
|
+
end
|
56
|
+
|
57
|
+
def tick
|
58
|
+
loop do
|
59
|
+
reads, _, _ = IO.select @set.map(&:fd), [], [], 1
|
60
|
+
return reap reads if reads
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def spawn_child file
|
65
|
+
process = Process.new file
|
66
|
+
process.start
|
67
|
+
process
|
68
|
+
end
|
69
|
+
|
70
|
+
def reap reads
|
71
|
+
@set.delete_if do |process|
|
72
|
+
next unless reads.include? process.fd
|
73
|
+
Config.internal_logger.debug "Reaping #{process.file}:#{process.pid}"
|
74
|
+
process.finish or @passed = false
|
75
|
+
true
|
76
|
+
end
|
77
|
+
|
78
|
+
shutdown if failed? and Config.fail_fast
|
79
|
+
end
|
80
|
+
|
81
|
+
def shutdown
|
82
|
+
@set.each do |process|
|
83
|
+
::Process.kill "TERM", process.pid
|
84
|
+
end
|
85
|
+
::Process.waitall
|
86
|
+
exit 1
|
87
|
+
end
|
88
|
+
|
89
|
+
def failed?
|
90
|
+
not success?
|
91
|
+
end
|
92
|
+
|
93
|
+
def success?
|
94
|
+
@passed
|
95
|
+
end
|
96
|
+
|
97
|
+
class Process
|
98
|
+
attr_reader :fd
|
99
|
+
attr_reader :file
|
100
|
+
attr_reader :pid
|
101
|
+
|
102
|
+
def initialize file
|
103
|
+
@file = file
|
104
|
+
end
|
105
|
+
|
106
|
+
def start
|
107
|
+
@fd, wr = IO.pipe
|
108
|
+
|
109
|
+
@pid = fork do
|
110
|
+
@fd.close
|
111
|
+
begin
|
112
|
+
load file
|
113
|
+
rescue => error
|
114
|
+
print_stacktrace error
|
115
|
+
exit 1
|
116
|
+
ensure
|
117
|
+
wr.write "\x00"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
freeze
|
122
|
+
end
|
123
|
+
|
124
|
+
def print_stacktrace error
|
125
|
+
locations = error.backtrace
|
126
|
+
final_location = locations.shift
|
127
|
+
locations = TestRunner::BacktraceFilter.(locations)
|
128
|
+
|
129
|
+
lines = locations.map do |loc| "\tfrom #{loc}" end
|
130
|
+
lines.unshift "#{final_location}: #{error.message} (#{error.class.name})"
|
131
|
+
|
132
|
+
lines.reverse! if Config.reverse_backtraces
|
133
|
+
Config.logger.error "Exception:\n#{lines * "\n"}"
|
134
|
+
end
|
135
|
+
|
136
|
+
def finish
|
137
|
+
fd.read 1
|
138
|
+
_, status = ::Process.wait2 pid
|
139
|
+
status = status.exitstatus
|
140
|
+
Config.internal_logger.debug "finished script #{@file}; status=#{status.inspect}"
|
141
|
+
status == 0
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module TestRunner
|
2
|
+
module Util
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def extract_key_args hsh, *args
|
6
|
+
defaults, args = extract_hash args
|
7
|
+
unknown_args = hsh.keys - (args + defaults.keys)
|
8
|
+
missing_args = args - hsh.keys
|
9
|
+
unless unknown_args.empty? and missing_args.empty?
|
10
|
+
raise ArgumentError, key_arg_error(unknown_args, missing_args)
|
11
|
+
end
|
12
|
+
(args + defaults.keys).map do |arg|
|
13
|
+
hsh.fetch arg do defaults.fetch arg end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def extract_hash ary
|
18
|
+
if ary.last.is_a? Hash
|
19
|
+
hsh = ary.pop
|
20
|
+
else
|
21
|
+
hsh = {}
|
22
|
+
end
|
23
|
+
[hsh, ary]
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_camel_case str
|
27
|
+
str = "_#{str}"
|
28
|
+
str.gsub!(%r{_[a-z]}) { |snake| snake.slice(1).upcase }
|
29
|
+
str.gsub!('/', '::')
|
30
|
+
str
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_snake_case str
|
34
|
+
str = str.gsub '::', '/'
|
35
|
+
# Convert FOOBar => FooBar
|
36
|
+
str.gsub! %r{[[:upper:]]{2,}} do |uppercase|
|
37
|
+
bit = uppercase[0]
|
38
|
+
bit << uppercase[1...-1].downcase
|
39
|
+
bit << uppercase[-1]
|
40
|
+
bit
|
41
|
+
end
|
42
|
+
# Convert FooBar => foo_bar
|
43
|
+
str.gsub! %r{[[:lower:]][[:upper:]]+[[:lower:]]} do |camel|
|
44
|
+
bit = camel[0]
|
45
|
+
bit << '_'
|
46
|
+
bit << camel[1..-1].downcase
|
47
|
+
end
|
48
|
+
str.downcase!
|
49
|
+
str
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def key_arg_error unknown, missing
|
55
|
+
str = "bad arguments. "
|
56
|
+
if unknown.any?
|
57
|
+
str.concat " unknown: #{unknown.join ', '}"
|
58
|
+
str.concat "; " if missing.any?
|
59
|
+
end
|
60
|
+
if missing.any?
|
61
|
+
str.concat " missing: #{missing.join ', '}"
|
62
|
+
end
|
63
|
+
str
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: test-runner
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nathan Ladd
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-09-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: logger-logging
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: Fork based runner for tests written as simple ruby scripts
|
28
|
+
email: nathanladd+github@gmail.com
|
29
|
+
executables:
|
30
|
+
- test-runner
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- bin/test-runner
|
35
|
+
- lib/test_runner.rb
|
36
|
+
- lib/test_runner/assert.rb
|
37
|
+
- lib/test_runner/assert/assertion.rb
|
38
|
+
- lib/test_runner/assert/check.rb
|
39
|
+
- lib/test_runner/assert/checks.rb
|
40
|
+
- lib/test_runner/assert/checks/registry.rb
|
41
|
+
- lib/test_runner/assert/errors.rb
|
42
|
+
- lib/test_runner/assert/syntax.rb
|
43
|
+
- lib/test_runner/backtrace_filter.rb
|
44
|
+
- lib/test_runner/cli.rb
|
45
|
+
- lib/test_runner/colored_logger.rb
|
46
|
+
- lib/test_runner/config.rb
|
47
|
+
- lib/test_runner/runner.rb
|
48
|
+
- lib/test_runner/script.rb
|
49
|
+
- lib/test_runner/util.rb
|
50
|
+
homepage: https://github.com/ntl/ftest
|
51
|
+
licenses:
|
52
|
+
- MIT
|
53
|
+
metadata: {}
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
requirements: []
|
69
|
+
rubyforge_project:
|
70
|
+
rubygems_version: 2.4.5.1
|
71
|
+
signing_key:
|
72
|
+
specification_version: 4
|
73
|
+
summary: Fork based runner for tests written as simple ruby scripts
|
74
|
+
test_files: []
|