ftest 0.1.0
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 +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: []
|