ftest 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/ftest +6 -0
- data/lib/ftest.rb +25 -0
- data/lib/ftest/assert.rb +37 -0
- data/lib/ftest/assert/assertion.rb +116 -0
- data/lib/ftest/assert/check.rb +53 -0
- data/lib/ftest/assert/checks.rb +76 -0
- data/lib/ftest/assert/checks/registry.rb +31 -0
- data/lib/ftest/assert/errors.rb +46 -0
- data/lib/ftest/assert/syntax.rb +94 -0
- data/lib/ftest/cli.rb +172 -0
- data/lib/ftest/colored_logger.rb +54 -0
- data/lib/ftest/config.rb +35 -0
- data/lib/ftest/runner.rb +138 -0
- data/lib/ftest/util.rb +66 -0
- metadata +58 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2d6ee01d44002fef117aa809c2817d9ff81eeac2
|
4
|
+
data.tar.gz: d13bf0a4ba5502a3a671ca0752ea20bc88ffb508
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1d5ecd73d2435c9624d95837193d05b287f36186c91a5ad074518c2d86ecb7a1aefc905c202e39a153c92773bd4d048963b6b55d9d597d01562eac8ea767d81c
|
7
|
+
data.tar.gz: d6aec2df76647153cd02ccaf21f9e5757bb99bbbd660ab0f8d8c1b19bae14e49e6995b9458eafa11d4b23e29839c93efe59f415e1785ac2deb5e392512e24d3b
|
data/bin/ftest
ADDED
data/lib/ftest.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require "logger"
|
2
|
+
|
3
|
+
module FTest
|
4
|
+
autoload :Assert, "ftest/assert"
|
5
|
+
autoload :ColoredLogger, "ftest/colored_logger"
|
6
|
+
autoload :Config, "ftest/config"
|
7
|
+
autoload :CLI, "ftest/cli"
|
8
|
+
autoload :Runner, "ftest/runner"
|
9
|
+
autoload :Util, "ftest/util"
|
10
|
+
|
11
|
+
# FTest can be included in scripts as a mixin, e.g.
|
12
|
+
#
|
13
|
+
# require "ftest"
|
14
|
+
# include FTest
|
15
|
+
#
|
16
|
+
# logger.info "hi"
|
17
|
+
# assert true
|
18
|
+
def logger
|
19
|
+
Config.logger
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.included target
|
23
|
+
Assert::Syntax.infect target
|
24
|
+
end
|
25
|
+
end
|
data/lib/ftest/assert.rb
ADDED
@@ -0,0 +1,37 @@
|
|
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 FTest
|
8
|
+
module Assert
|
9
|
+
extend self
|
10
|
+
|
11
|
+
def inspect object, truncate = 100
|
12
|
+
raw = object.inspect
|
13
|
+
return raw if raw.size <= truncate
|
14
|
+
"#{raw[0..truncate - 2]} …\""
|
15
|
+
end
|
16
|
+
|
17
|
+
def filter_trace trace
|
18
|
+
return trace unless Config.trim_backtrace
|
19
|
+
|
20
|
+
entry_point = trace.last
|
21
|
+
ftest_root = File.expand_path "../..", __FILE__
|
22
|
+
|
23
|
+
first_pass = trace.drop_while do |location|
|
24
|
+
full_path = File.expand_path location.path
|
25
|
+
full_path.start_with? ftest_root
|
26
|
+
end
|
27
|
+
|
28
|
+
second_pass = first_pass.take_while do |location|
|
29
|
+
full_path = File.expand_path location.path
|
30
|
+
not full_path.start_with? ftest_root
|
31
|
+
end
|
32
|
+
|
33
|
+
second_pass << entry_point unless second_pass.last == entry_point
|
34
|
+
second_pass
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module FTest
|
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
|
+
FTest.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
|
+
Assert.filter_trace @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 FTest
|
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 FTest
|
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 FTest
|
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 FTest
|
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 FTest
|
2
|
+
module Assert
|
3
|
+
class Syntax < Module
|
4
|
+
MISSING = :__ftest_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
|
data/lib/ftest/cli.rb
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
require "optparse"
|
2
|
+
|
3
|
+
module FTest
|
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
|
+
options.color if @stdout.tty?
|
20
|
+
setup_config
|
21
|
+
Runner.(resolve_files) or exit 1
|
22
|
+
end
|
23
|
+
|
24
|
+
def setup_config
|
25
|
+
Config.child_count = options.child_count
|
26
|
+
Config.fail_fast = options.fail_fast?
|
27
|
+
Config.logger = build_logger
|
28
|
+
Config.reverse_stack_traces = options.reverse_stack_traces?
|
29
|
+
Config.trim_backtrace = !options.full_backtrace?
|
30
|
+
end
|
31
|
+
|
32
|
+
def build_logger
|
33
|
+
cls = if options.color? then ColoredLogger else Logger end
|
34
|
+
logger = cls.new @stdout
|
35
|
+
logger.level = options.log_level
|
36
|
+
logger
|
37
|
+
end
|
38
|
+
|
39
|
+
def resolve_files
|
40
|
+
options.paths.flat_map do |path|
|
41
|
+
if path.end_with? ".rb"
|
42
|
+
[path]
|
43
|
+
else
|
44
|
+
Dir[File.join path, "**/*.rb"]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class Options
|
50
|
+
attr_reader :child_count
|
51
|
+
attr_reader :log_level
|
52
|
+
attr_reader :paths
|
53
|
+
|
54
|
+
def initialize
|
55
|
+
@paths = []
|
56
|
+
@log_level = Logger::WARN
|
57
|
+
@child_count = 2
|
58
|
+
end
|
59
|
+
|
60
|
+
def add_path path
|
61
|
+
paths << path
|
62
|
+
end
|
63
|
+
|
64
|
+
def child_count= number
|
65
|
+
@child_count = number.to_i
|
66
|
+
end
|
67
|
+
|
68
|
+
def color
|
69
|
+
@color = !@color
|
70
|
+
end
|
71
|
+
|
72
|
+
def color?
|
73
|
+
@color
|
74
|
+
end
|
75
|
+
|
76
|
+
def fail_fast
|
77
|
+
@fail_fast = !@fail_fast
|
78
|
+
end
|
79
|
+
|
80
|
+
def fail_fast?
|
81
|
+
@fail_fast
|
82
|
+
end
|
83
|
+
|
84
|
+
def full_backtrace
|
85
|
+
@full_backtrace = !@full_backtrace
|
86
|
+
end
|
87
|
+
|
88
|
+
def full_backtrace?
|
89
|
+
@full_backtrace
|
90
|
+
end
|
91
|
+
|
92
|
+
def quiet
|
93
|
+
@log_level += 1
|
94
|
+
end
|
95
|
+
|
96
|
+
def reverse_stack_traces
|
97
|
+
@reverse_stack_traces = !@reverse_stack_traces
|
98
|
+
end
|
99
|
+
|
100
|
+
def reverse_stack_traces?
|
101
|
+
@reverse_stack_traces
|
102
|
+
end
|
103
|
+
|
104
|
+
def verbose
|
105
|
+
@log_level -= 1
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class ArgvParser
|
110
|
+
def self.call argv
|
111
|
+
options = Options.new
|
112
|
+
parser = ArgvParser.new argv, options
|
113
|
+
parser.()
|
114
|
+
options
|
115
|
+
end
|
116
|
+
|
117
|
+
def initialize argv, options
|
118
|
+
@argv = argv
|
119
|
+
@options = options
|
120
|
+
end
|
121
|
+
|
122
|
+
def call
|
123
|
+
parse_options
|
124
|
+
add_paths
|
125
|
+
end
|
126
|
+
|
127
|
+
def add_paths
|
128
|
+
@argv << "tests" if @argv.empty?
|
129
|
+
@argv.each do |path|
|
130
|
+
@options.add_path path
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def parse_options
|
135
|
+
OptionParser.new do |opts|
|
136
|
+
opts.banner = <<-BANNER
|
137
|
+
Usage: #{program_name} [options] [PATH]
|
138
|
+
|
139
|
+
If no PATH is specified, ./tests is assumed
|
140
|
+
|
141
|
+
BANNER
|
142
|
+
|
143
|
+
opts.on "-b", "--full-backtrace", "Do not filter assertion stack traces" do
|
144
|
+
@options.full_backtrace
|
145
|
+
end
|
146
|
+
opts.on "-c", "--color", "Enable/disable colored output" do
|
147
|
+
@options.color
|
148
|
+
end
|
149
|
+
opts.on "-f", "--fail-fast", "When any test script fails, exit immediately" do
|
150
|
+
@options.fail_fast
|
151
|
+
end
|
152
|
+
opts.on "-n", "--num=COUNT", "Max number of sub processes to run concurrently" do |num|
|
153
|
+
@options.child_count = num
|
154
|
+
end
|
155
|
+
opts.on "-q", "--quiet", "Reduces log verbosity" do
|
156
|
+
@options.quiet
|
157
|
+
end
|
158
|
+
opts.on "-r", "--reverse-traces", "Reverse the order of stack traces" do
|
159
|
+
@options.reverse_stack_traces
|
160
|
+
end
|
161
|
+
opts.on "-v", "--verbose", "Increases log verbosity" do
|
162
|
+
@options.verbose
|
163
|
+
end
|
164
|
+
end.parse! @argv
|
165
|
+
end
|
166
|
+
|
167
|
+
def program_name
|
168
|
+
File.basename $PROGRAM_NAME
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module FTest
|
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=).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|
|
29
|
+
colored_msg = format log_level, msg
|
30
|
+
@logger.public_send log_level, colored_msg
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def format log_level, msg
|
35
|
+
formatter = palette.fetch log_level
|
36
|
+
instance_exec msg, &formatter
|
37
|
+
end
|
38
|
+
|
39
|
+
def fg *args
|
40
|
+
col :fg, *args
|
41
|
+
end
|
42
|
+
|
43
|
+
def bg *args
|
44
|
+
col :bg, *args
|
45
|
+
end
|
46
|
+
|
47
|
+
def col fgbg, color_code, intensity_code, str
|
48
|
+
color_num = ANSI_COLORS.index color_code
|
49
|
+
intensity_num = { :normal => 0, :bright => 1 }.fetch intensity_code
|
50
|
+
fgbg_num = { :fg => 3, :bg => 4 }.fetch fgbg
|
51
|
+
"\e[#{fgbg_num}#{color_num};#{intensity_num}m#{str}\e[0m"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/ftest/config.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
module FTest
|
2
|
+
module Config
|
3
|
+
extend self
|
4
|
+
|
5
|
+
attr_writer :child_count
|
6
|
+
attr_writer :fail_fast
|
7
|
+
attr_writer :logger
|
8
|
+
attr_writer :reverse_stack_traces
|
9
|
+
attr_writer :trim_backtrace
|
10
|
+
|
11
|
+
def child_count
|
12
|
+
@child_count or 1
|
13
|
+
end
|
14
|
+
|
15
|
+
def fail_fast
|
16
|
+
@fail_fast or false
|
17
|
+
end
|
18
|
+
|
19
|
+
def logger
|
20
|
+
@logger or default_logger
|
21
|
+
end
|
22
|
+
|
23
|
+
def default_logger
|
24
|
+
@default_logger ||= Logger.new $stdout
|
25
|
+
end
|
26
|
+
|
27
|
+
def reverse_stack_traces
|
28
|
+
@reverse_stack_traces or false
|
29
|
+
end
|
30
|
+
|
31
|
+
def trim_backtrace
|
32
|
+
@trim_backtrace or false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/ftest/runner.rb
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
module FTest
|
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.logger.debug "ftest found #{files.size} files: #{files * ", "}"
|
17
|
+
return if files.empty?
|
18
|
+
result = run_all
|
19
|
+
Config.logger.info "finished executing files; success=#{result.inspect}"
|
20
|
+
result
|
21
|
+
end
|
22
|
+
|
23
|
+
def run_all
|
24
|
+
set = ProcessSet.new
|
25
|
+
Signal.trap "INT" do set.shutdown end
|
26
|
+
set << files.shift until files.empty?
|
27
|
+
set.finish
|
28
|
+
end
|
29
|
+
|
30
|
+
class ProcessSet
|
31
|
+
def initialize
|
32
|
+
@set = []
|
33
|
+
@passed = true
|
34
|
+
end
|
35
|
+
|
36
|
+
def << file
|
37
|
+
wait Config.child_count - 1
|
38
|
+
@set.<< spawn_child file
|
39
|
+
end
|
40
|
+
|
41
|
+
def finish
|
42
|
+
wait 0
|
43
|
+
@passed
|
44
|
+
end
|
45
|
+
|
46
|
+
def wait max_count
|
47
|
+
tick while @set.size > max_count
|
48
|
+
end
|
49
|
+
|
50
|
+
def tick
|
51
|
+
loop do
|
52
|
+
reads, _, _ = IO.select @set.map(&:fd), [], [], 1
|
53
|
+
return reap reads if reads
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def spawn_child file
|
58
|
+
process = Process.new file
|
59
|
+
process.start
|
60
|
+
process
|
61
|
+
end
|
62
|
+
|
63
|
+
def reap reads
|
64
|
+
@set.delete_if do |process|
|
65
|
+
next unless reads.include? process.fd
|
66
|
+
Config.logger.debug "Reaping #{process.file}:#{process.pid}"
|
67
|
+
process.finish or @passed = false
|
68
|
+
true
|
69
|
+
end
|
70
|
+
|
71
|
+
shutdown if failed? and Config.fail_fast
|
72
|
+
end
|
73
|
+
|
74
|
+
def shutdown
|
75
|
+
@set.each do |process|
|
76
|
+
::Process.kill "TERM", process.pid
|
77
|
+
end
|
78
|
+
::Process.waitall
|
79
|
+
exit 1
|
80
|
+
end
|
81
|
+
|
82
|
+
def failed?
|
83
|
+
not success?
|
84
|
+
end
|
85
|
+
|
86
|
+
def success?
|
87
|
+
@passed
|
88
|
+
end
|
89
|
+
|
90
|
+
class Process
|
91
|
+
attr_reader :fd
|
92
|
+
attr_reader :file
|
93
|
+
attr_reader :pid
|
94
|
+
|
95
|
+
def initialize file
|
96
|
+
@file = file
|
97
|
+
end
|
98
|
+
|
99
|
+
def start
|
100
|
+
@fd, wr = IO.pipe
|
101
|
+
|
102
|
+
@pid = fork do
|
103
|
+
@fd.close
|
104
|
+
begin
|
105
|
+
load file
|
106
|
+
rescue => error
|
107
|
+
print_stacktrace error
|
108
|
+
exit 1
|
109
|
+
ensure
|
110
|
+
wr.write "\x00"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
freeze
|
115
|
+
end
|
116
|
+
|
117
|
+
def print_stacktrace error
|
118
|
+
locations = error.backtrace
|
119
|
+
final_location = locations.shift
|
120
|
+
|
121
|
+
lines = locations.map do |loc| "\tfrom #{loc}" end
|
122
|
+
lines.unshift "#{final_location}: #{error.message}"
|
123
|
+
|
124
|
+
lines.reverse! if Config.reverse_stack_traces
|
125
|
+
Config.logger.error "Exception:\n#{lines * "\n"}"
|
126
|
+
end
|
127
|
+
|
128
|
+
def finish
|
129
|
+
fd.read 1
|
130
|
+
_, status = ::Process.wait2 pid
|
131
|
+
status = status.exitstatus
|
132
|
+
Config.logger.debug "finished script #{@file}; status=#{status.inspect}"
|
133
|
+
status == 0
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
data/lib/ftest/util.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
module FTest
|
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,58 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ftest
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nathan Ladd
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-08-22 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Fork based runner for tests written as simple ruby scripts
|
14
|
+
email: nathanladd+github@gmail.com
|
15
|
+
executables:
|
16
|
+
- ftest
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- bin/ftest
|
21
|
+
- lib/ftest.rb
|
22
|
+
- lib/ftest/assert.rb
|
23
|
+
- lib/ftest/assert/assertion.rb
|
24
|
+
- lib/ftest/assert/check.rb
|
25
|
+
- lib/ftest/assert/checks.rb
|
26
|
+
- lib/ftest/assert/checks/registry.rb
|
27
|
+
- lib/ftest/assert/errors.rb
|
28
|
+
- lib/ftest/assert/syntax.rb
|
29
|
+
- lib/ftest/cli.rb
|
30
|
+
- lib/ftest/colored_logger.rb
|
31
|
+
- lib/ftest/config.rb
|
32
|
+
- lib/ftest/runner.rb
|
33
|
+
- lib/ftest/util.rb
|
34
|
+
homepage: https://github.com/ntl/ftest
|
35
|
+
licenses:
|
36
|
+
- MIT
|
37
|
+
metadata: {}
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
requirements: []
|
53
|
+
rubyforge_project:
|
54
|
+
rubygems_version: 2.4.5.1
|
55
|
+
signing_key:
|
56
|
+
specification_version: 4
|
57
|
+
summary: Fork based runner for tests written as simple ruby scripts
|
58
|
+
test_files: []
|