test-runner 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib_path = File.join Dir.pwd, "lib"
4
+ if File.directory? lib_path and not $LOAD_PATH.include? lib_path
5
+ $LOAD_PATH << lib_path
6
+ end
7
+ require "test_runner"
8
+ TestRunner::CLI.run ARGV
@@ -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,2 @@
1
+ require "test_runner"
2
+ include TestRunner
@@ -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: []