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.
@@ -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
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib_path = File.expand_path "../../lib", __FILE__
4
+ $LOAD_PATH << lib_path unless $LOAD_PATH.include? lib_path
5
+ require "ftest"
6
+ FTest::CLI.run ARGV
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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: []