given_core 3.0.0.beta.1
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/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
|