given_core 3.0.0.beta.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +27 -0
- data/MIT-LICENSE +20 -0
- data/README.md +720 -0
- data/Rakefile +232 -0
- data/TODO +13 -0
- data/doc/main.rdoc +7 -0
- data/lib/given.rb +2 -0
- data/lib/given/core.rb +11 -0
- data/lib/given/evaluator.rb +38 -0
- data/lib/given/ext/numeric.rb +31 -0
- data/lib/given/extensions.rb +242 -0
- data/lib/given/failure.rb +56 -0
- data/lib/given/failure_matcher.rb +63 -0
- data/lib/given/file_cache.rb +18 -0
- data/lib/given/fuzzy_number.rb +68 -0
- data/lib/given/fuzzy_shortcuts.rb +1 -0
- data/lib/given/line_extractor.rb +41 -0
- data/lib/given/module_methods.rb +69 -0
- data/lib/given/natural_assertion.rb +177 -0
- data/lib/given/version.rb +11 -0
- data/lib/rspec-given.rb +9 -0
- data/rakelib/bundler_fix.rb +17 -0
- data/rakelib/gemspec.rake +157 -0
- data/rakelib/metrics.rake +30 -0
- data/rakelib/preview.rake +14 -0
- data/test/before_test.rb +22 -0
- data/test/meme_test.rb +36 -0
- metadata +93 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
|
2
|
+
module Given
|
3
|
+
|
4
|
+
# Failure objects will raise the given exception whenever you try
|
5
|
+
# to send it *any* message.
|
6
|
+
class Failure < BasicObject
|
7
|
+
undef_method :==, :!=, :!
|
8
|
+
|
9
|
+
def initialize(exception)
|
10
|
+
@exception = exception
|
11
|
+
end
|
12
|
+
|
13
|
+
def is_a?(klass)
|
14
|
+
klass == Failure
|
15
|
+
end
|
16
|
+
|
17
|
+
def ==(other)
|
18
|
+
if failure_matcher?(other)
|
19
|
+
other.matches?(self)
|
20
|
+
else
|
21
|
+
die
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def !=(other)
|
26
|
+
if failure_matcher?(other)
|
27
|
+
other.does_not_match?(self)
|
28
|
+
else
|
29
|
+
die
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def method_missing(sym, *args, &block)
|
34
|
+
die
|
35
|
+
end
|
36
|
+
|
37
|
+
def respond_to?(method_symbol)
|
38
|
+
method_symbol == :call ||
|
39
|
+
method_symbol == :== ||
|
40
|
+
method_symbol == :!= ||
|
41
|
+
method_symbol == :is_a?
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def die
|
47
|
+
::Kernel.raise @exception
|
48
|
+
end
|
49
|
+
|
50
|
+
def failure_matcher?(other)
|
51
|
+
other.is_a?(::Given::FailureMatcher) ||
|
52
|
+
other.is_a?(::RSpec::Given::HaveFailed::HaveFailedMatcher)
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Given
|
2
|
+
|
3
|
+
class FailureMatcher
|
4
|
+
def initialize(exception_class, message_pattern)
|
5
|
+
@no_pattern = false
|
6
|
+
@expected_exception_class = exception_class
|
7
|
+
@expected_message_pattern = message_pattern
|
8
|
+
if @expected_message_pattern.nil?
|
9
|
+
@expected_message_pattern = //
|
10
|
+
@no_pattern = true
|
11
|
+
elsif @expected_message_pattern.is_a?(String)
|
12
|
+
@expected_message_pattern =
|
13
|
+
Regexp.new("\\A" + Regexp.quote(@expected_message_pattern) + "\\Z")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def matches?(possible_failure)
|
18
|
+
if possible_failure.respond_to?(:call)
|
19
|
+
make_sure_it_throws_an_exception(possible_failure)
|
20
|
+
else
|
21
|
+
Given.fail_with("#{description}, but nothing failed")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def does_not_match?(possible_failure)
|
26
|
+
if possible_failure.respond_to?(:call)
|
27
|
+
false
|
28
|
+
else
|
29
|
+
true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def make_sure_it_throws_an_exception(possible_failure)
|
36
|
+
possible_failure.call
|
37
|
+
Given.fail_with("Expected an exception")
|
38
|
+
rescue Exception => ex
|
39
|
+
if ! ex.is_a?(@expected_exception_class)
|
40
|
+
Given.fail_with("#{description}, but got #{ex.inspect}")
|
41
|
+
elsif @expected_message_pattern !~ ex.message
|
42
|
+
Given.fail_with("#{description}, but got #{ex.inspect}")
|
43
|
+
else
|
44
|
+
true
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def description
|
51
|
+
result = "Expected failure with #{@expected_exception_class}"
|
52
|
+
result << " matching #{@expected_message_pattern.inspect}" unless @no_pattern
|
53
|
+
result
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
module FailureMethod
|
58
|
+
def Failure(exception_class=Exception, message_pattern=nil)
|
59
|
+
FailureMatcher.new(exception_class, message_pattern)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
|
2
|
+
module Given
|
3
|
+
class FileCache
|
4
|
+
def initialize
|
5
|
+
@lines = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def get(file_name)
|
9
|
+
@lines[file_name] ||= read_lines(file_name)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def read_lines(file_name)
|
15
|
+
open(file_name) { |f| f.readlines }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
|
2
|
+
module Given
|
3
|
+
module Fuzzy
|
4
|
+
class FuzzyNumber
|
5
|
+
|
6
|
+
DEFAULT_EPSILON = 10 * Float::EPSILON
|
7
|
+
|
8
|
+
attr_reader :exact_value, :delta_amount
|
9
|
+
|
10
|
+
def initialize(exact_value)
|
11
|
+
@exact_value = exact_value
|
12
|
+
@delta_amount = exact_value * DEFAULT_EPSILON
|
13
|
+
end
|
14
|
+
|
15
|
+
def exactly_equals?(other)
|
16
|
+
other.is_a?(self.class) &&
|
17
|
+
exact_value == other.exact_value &&
|
18
|
+
delta_amount == other.delta_amount
|
19
|
+
end
|
20
|
+
|
21
|
+
# Low limit of the fuzzy number.
|
22
|
+
def low_limit
|
23
|
+
exact_value - delta_amount
|
24
|
+
end
|
25
|
+
|
26
|
+
# High limit of the fuzzy number.
|
27
|
+
def high_limit
|
28
|
+
exact_value + delta_amount
|
29
|
+
end
|
30
|
+
|
31
|
+
# True if the other number is in range of the fuzzy number.
|
32
|
+
def ==(other)
|
33
|
+
(other - exact_value).abs <= delta_amount
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
"<Approximately #{exact_value} +/- #{delta_amount}>"
|
38
|
+
end
|
39
|
+
|
40
|
+
# Set the delta for a fuzzy number.
|
41
|
+
def delta(delta)
|
42
|
+
@delta_amount = delta.abs
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
# Specifying a percentage of the exact number to be used in
|
47
|
+
# setting the delta.
|
48
|
+
def percent(percentage)
|
49
|
+
delta(exact_value * (percentage / 100.0))
|
50
|
+
end
|
51
|
+
|
52
|
+
# Specifying the number of epsilons to be used in setting the
|
53
|
+
# delta.
|
54
|
+
def epsilon(neps)
|
55
|
+
delta(exact_value * (neps * Float::EPSILON))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Create an approximate number that is approximately equal to
|
60
|
+
# the given number, plus or minus the delta value. If no
|
61
|
+
# explicit delta is given, then the default delta that is about
|
62
|
+
# 10X the size of the smallest possible change in the given
|
63
|
+
# number will be used.
|
64
|
+
def about(*args)
|
65
|
+
FuzzyNumber.new(*args)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'given/ext/numeric'
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'given/file_cache'
|
2
|
+
|
3
|
+
module Given
|
4
|
+
class LineExtractor
|
5
|
+
def initialize(file_cache=nil)
|
6
|
+
@files = file_cache || FileCache.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def line(file_name, line)
|
10
|
+
lines = @files.get(file_name)
|
11
|
+
extract_lines_from(lines, line-1)
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
"<LineExtractor>"
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def extract_lines_from(lines, line_index)
|
21
|
+
result = lines[line_index]
|
22
|
+
if continued?(result)
|
23
|
+
level = indentation_level(result)
|
24
|
+
begin
|
25
|
+
line_index += 1
|
26
|
+
result << lines[line_index]
|
27
|
+
end while indentation_level(lines[line_index]) > level
|
28
|
+
end
|
29
|
+
result
|
30
|
+
end
|
31
|
+
|
32
|
+
def continued?(string)
|
33
|
+
string =~ /(\{|do) *$/
|
34
|
+
end
|
35
|
+
|
36
|
+
def indentation_level(string)
|
37
|
+
string =~ /^(\s*)\S/
|
38
|
+
$1.nil? ? 1000000 : $1.size
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
|
2
|
+
module Given
|
3
|
+
# Does this platform support natural assertions?
|
4
|
+
RBX_IN_USE = (defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx')
|
5
|
+
JRUBY_IN_USE = defined?(JRUBY_VERSION)
|
6
|
+
|
7
|
+
NATURAL_ASSERTIONS_SUPPORTED = ! (JRUBY_IN_USE || RBX_IN_USE)
|
8
|
+
|
9
|
+
def self.framework
|
10
|
+
@_gvn_framework
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.framework=(framework)
|
14
|
+
@_gvn_framework = framework
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.source_caching_disabled
|
18
|
+
@_gvn_source_caching_disabled
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.source_caching_disabled=(value)
|
22
|
+
@_gvn_source_caching_disabled = value
|
23
|
+
end
|
24
|
+
|
25
|
+
# Detect the formatting requested in the given configuration object.
|
26
|
+
#
|
27
|
+
# If the format requires it, source caching will be enabled.
|
28
|
+
def self.detect_formatters(c)
|
29
|
+
format_active = c.formatters.any? { |f| f.class.name !~ /ProgressFormatter/ }
|
30
|
+
Given.source_caching_disabled = ! format_active
|
31
|
+
end
|
32
|
+
|
33
|
+
# Globally enable/disable natural assertions.
|
34
|
+
#
|
35
|
+
# There is a similar function in Extensions that works at a
|
36
|
+
# describe or context scope.
|
37
|
+
def self.use_natural_assertions(enabled=true)
|
38
|
+
ok_to_use_natural_assertions(enabled)
|
39
|
+
@natural_assertions_enabled = enabled
|
40
|
+
end
|
41
|
+
|
42
|
+
# TRUE if natural assertions are globally enabled?
|
43
|
+
def self.natural_assertions_enabled?
|
44
|
+
@natural_assertions_enabled
|
45
|
+
end
|
46
|
+
|
47
|
+
# Is is OK to use natural assertions on this platform.
|
48
|
+
#
|
49
|
+
# An error is raised if the the platform does not support natural
|
50
|
+
# assertions and the flag is attempting to enable them.
|
51
|
+
def self.ok_to_use_natural_assertions(enabled)
|
52
|
+
if enabled && ! NATURAL_ASSERTIONS_SUPPORTED
|
53
|
+
fail ArgumentError, "Natural Assertions are disabled for JRuby"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Fail an example with the given messages.
|
58
|
+
#
|
59
|
+
# This should be the only place we reference the RSpec function.
|
60
|
+
# Everywhere else in rspec-given should be calling this function.
|
61
|
+
def self.fail_with(*args)
|
62
|
+
Given.framework.fail_with(*args)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Error object used by RSpec to indicate a pending example.
|
66
|
+
def self.pending_error
|
67
|
+
Given.framework.pending_error
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
require 'given/module_methods'
|
2
|
+
require 'given/evaluator'
|
3
|
+
|
4
|
+
if Given::NATURAL_ASSERTIONS_SUPPORTED
|
5
|
+
require 'ripper'
|
6
|
+
require 'sorcerer'
|
7
|
+
end
|
8
|
+
|
9
|
+
module Given
|
10
|
+
|
11
|
+
InvalidThenError = Class.new(StandardError)
|
12
|
+
|
13
|
+
class NaturalAssertion
|
14
|
+
|
15
|
+
def initialize(clause_type, block, example, line_extractor)
|
16
|
+
@clause_type = clause_type
|
17
|
+
@evaluator = Evaluator.new(example, block)
|
18
|
+
@line_extractor = line_extractor
|
19
|
+
set_file_and_line(block)
|
20
|
+
end
|
21
|
+
|
22
|
+
VOID_SEXP = [:void_stmt]
|
23
|
+
|
24
|
+
def has_content?
|
25
|
+
assertion_sexp != VOID_SEXP
|
26
|
+
end
|
27
|
+
|
28
|
+
def message
|
29
|
+
@output = "#{@clause_type} expression failed at #{source_line}\n"
|
30
|
+
@output << "Failing expression: #{source.strip}\n" if @clause_type != "Then"
|
31
|
+
explain_failure
|
32
|
+
display_pairs(expression_value_pairs)
|
33
|
+
@output << "\n"
|
34
|
+
@output
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
BINARY_EXPLAINATIONS = {
|
40
|
+
:== => "to equal",
|
41
|
+
:!= => "to not equal",
|
42
|
+
:< => "to be less than",
|
43
|
+
:<= => "to be less or equal to",
|
44
|
+
:> => "to be greater than",
|
45
|
+
:>= => "to be greater or equal to",
|
46
|
+
:=~ => "to match",
|
47
|
+
:!~ => "to not match",
|
48
|
+
}
|
49
|
+
|
50
|
+
def explain_failure
|
51
|
+
if assertion_sexp.first == :binary && msg = BINARY_EXPLAINATIONS[assertion_sexp[2]]
|
52
|
+
@output << explain_expected("expected", assertion_sexp[1], msg, assertion_sexp[3])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def explain_expected(expect_msg, expect_sexp, got_msg, got_sexp)
|
57
|
+
width = [expect_msg.size, got_msg.size].max
|
58
|
+
sprintf("%#{width}s: %s\n%#{width}s: %s\n",
|
59
|
+
expect_msg, eval_sexp(expect_sexp),
|
60
|
+
got_msg, eval_sexp(got_sexp))
|
61
|
+
end
|
62
|
+
|
63
|
+
def expression_value_pairs
|
64
|
+
assertion_subexpressions.map { |exp|
|
65
|
+
[exp, @evaluator.eval_string(exp)]
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def assertion_subexpressions
|
70
|
+
Sorcerer.subexpressions(assertion_sexp).reverse.uniq.reverse
|
71
|
+
end
|
72
|
+
|
73
|
+
def assertion_sexp
|
74
|
+
@assertion_sexp ||= extract_test_expression(Ripper::SexpBuilder.new(source).parse)
|
75
|
+
end
|
76
|
+
|
77
|
+
def source
|
78
|
+
@source ||= @line_extractor.line(@code_file, @code_line)
|
79
|
+
end
|
80
|
+
|
81
|
+
def set_file_and_line(block)
|
82
|
+
@code_file, @code_line = eval "[__FILE__, __LINE__]", block.binding
|
83
|
+
@code_line = @code_line.to_i
|
84
|
+
end
|
85
|
+
|
86
|
+
def extract_test_expression(sexp)
|
87
|
+
brace_block = extract_brace_block(sexp)
|
88
|
+
extract_first_statement(brace_block)
|
89
|
+
end
|
90
|
+
|
91
|
+
def extract_brace_block(sexp)
|
92
|
+
unless then_block?(sexp)
|
93
|
+
source = Sorcerer.source(sexp)
|
94
|
+
fail InvalidThenError, "Unexpected code at #{source_line}\n#{source}"
|
95
|
+
end
|
96
|
+
sexp[1][2][2]
|
97
|
+
end
|
98
|
+
|
99
|
+
def then_block?(sexp)
|
100
|
+
delve(sexp,0) == :program &&
|
101
|
+
delve(sexp,1,0) == :stmts_add &&
|
102
|
+
delve(sexp,1,2,0) == :method_add_block &&
|
103
|
+
(delve(sexp,1,2,2,0) == :brace_block || delve(sexp,1,2,2,0) == :do_block)
|
104
|
+
end
|
105
|
+
|
106
|
+
def extract_first_statement(block_sexp)
|
107
|
+
if contains_multiple_statements?(block_sexp)
|
108
|
+
source = Sorcerer.source(block_sexp)
|
109
|
+
fail InvalidThenError, "Multiple statements in Then block at #{source_line}\n#{source}"
|
110
|
+
end
|
111
|
+
extract_statement_from_block(block_sexp)
|
112
|
+
end
|
113
|
+
|
114
|
+
def contains_multiple_statements?(block_sexp)
|
115
|
+
!(delve(block_sexp,2,0) == :stmts_add &&
|
116
|
+
delve(block_sexp,2,1,0) == :stmts_new)
|
117
|
+
end
|
118
|
+
|
119
|
+
def extract_statement_from_block(block_sexp)
|
120
|
+
delve(block_sexp,2,2)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Safely dive into an array with a list of indicies. Return nil
|
124
|
+
# if the element doesn't exist, or if the intermediate result is
|
125
|
+
# not indexable.
|
126
|
+
def delve(ary, *indicies)
|
127
|
+
result = ary
|
128
|
+
while !indicies.empty? && result
|
129
|
+
return nil unless result.respond_to?(:[])
|
130
|
+
i = indicies.shift
|
131
|
+
result = result[i]
|
132
|
+
end
|
133
|
+
result
|
134
|
+
end
|
135
|
+
|
136
|
+
def eval_sexp(sexp)
|
137
|
+
expr_string = Sorcerer.source(sexp)
|
138
|
+
@evaluator.eval_string(expr_string)
|
139
|
+
end
|
140
|
+
|
141
|
+
WRAP_WIDTH = 20
|
142
|
+
|
143
|
+
def display_pairs(pairs)
|
144
|
+
width = suggest_width(pairs) + 4
|
145
|
+
pairs.each do |x, v|
|
146
|
+
v = adjust_indentation(v)
|
147
|
+
fmt = multi_line?(v) ?
|
148
|
+
"%-#{width}s\n#{' '*width} <- %s\n" :
|
149
|
+
"%-#{width}s <- %s\n"
|
150
|
+
@output << sprintf(fmt, v, x)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def adjust_indentation(string)
|
155
|
+
string.to_s.gsub(/^/, ' ')
|
156
|
+
end
|
157
|
+
|
158
|
+
def multi_line?(string)
|
159
|
+
(string.size > WRAP_WIDTH) || (string =~ /\n/)
|
160
|
+
end
|
161
|
+
|
162
|
+
def suggest_width(pairs)
|
163
|
+
pairs.map { |x,v|
|
164
|
+
max_line_length(v)
|
165
|
+
}.select { |n| n < WRAP_WIDTH }.max || 10
|
166
|
+
end
|
167
|
+
|
168
|
+
def max_line_length(string)
|
169
|
+
string.to_s.split(/\n/).map { |s| s.size }.max
|
170
|
+
end
|
171
|
+
|
172
|
+
def source_line
|
173
|
+
"#{@code_file}:#{@code_line}"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|