tokyo 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,32 @@
1
+ module Tokyo
2
+ class Expectation
3
+
4
+ # ensure received message returns expected value
5
+ #
6
+ # @note if block given it will have precedence over arguments
7
+ #
8
+ # @example
9
+ # n = mock(1)
10
+ # expect(n).to_receive(:+).with(1).and_return(2)
11
+ #
12
+ def and_return value = nil, &block
13
+ @return = block || value
14
+ end
15
+
16
+ private
17
+ def assert_message_returned_correct_value
18
+ return unless @return
19
+ if @return.is_a?(Proc)
20
+ received_messages.find {|log| @return.call(log[:returned])} || Tokyo.fail([
21
+ 'Looks like :%s message never returned expected value' % expected_message,
22
+ 'See validation block'
23
+ ], @caller)
24
+ else
25
+ received_messages.find {|log| log[:returned] == @return} || Tokyo.fail([
26
+ 'Looks like :%s message never returned expected value:' % expected_message,
27
+ Array(@return).map {|x| Tokyo.pp(x)}.join(', ')
28
+ ], @caller)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,24 @@
1
+ module Tokyo
2
+ class Expectation
3
+
4
+ # ensure received message throws as expected
5
+ #
6
+ # @note if block given it will have precedence over arguments
7
+ #
8
+ # @example
9
+ # x = mock(X.new)
10
+ # expect(x).to_receive(:y).and_throw(:z)
11
+ #
12
+ def and_throw symbol = nil, &block
13
+ @throw = [symbol, block]
14
+ end
15
+
16
+ def assert_message_thrown_as_expected
17
+ return unless @throw
18
+ received_messages.each do |log|
19
+ next unless f = Tokyo.assert_thrown_as_expected(log, *@throw)
20
+ Tokyo.fail(f, log[:caller])
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,36 @@
1
+ module Tokyo
2
+ class Expectation
3
+
4
+ # ensure expected message received with correct arguments
5
+ #
6
+ # @note if block given it will have precedence over arguments
7
+ #
8
+ # @example
9
+ # test :some_test do
10
+ # some_object = mock(SomeObject.new)
11
+ # expect(some_object).to_receive(:some_method).with(:some, :args)
12
+ # # call `some_object.some_method(:some, :args)` for test to pass
13
+ # end
14
+ #
15
+ def with *args, &block
16
+ @with = block || args
17
+ self
18
+ end
19
+
20
+ private
21
+ def assert_message_received_with_correct_arguments
22
+ return unless @with
23
+ if @with.is_a?(Proc)
24
+ received_messages.find {|log| @with.call(log[:arguments])} || Tokyo.fail([
25
+ 'Looks like :%s message never was called with expected arguments' % expected_message,
26
+ 'See validation block'
27
+ ], @caller)
28
+ else
29
+ received_messages.find {|log| log[:arguments] == @with} || Tokyo.fail([
30
+ 'Looks like :%s message never was called with expected arguments:' % expected_message,
31
+ Array(@with).map {|x| Tokyo.pp(x)}.join(', ')
32
+ ], @caller)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,103 @@
1
+ module Tokyo
2
+ class Expectation
3
+
4
+ def self.restore_object_status obj
5
+ return unless obj.instance_variable_get(:@__tokyo__original_methods__)
6
+
7
+ obj.instance_variable_get(:@__tokyo__original_methods__).each_pair do |n,m|
8
+ obj.define_singleton_method(n, &m)
9
+ end
10
+
11
+ obj.instance_variable_set(:@__tokyo__original_methods__, {})
12
+ obj.instance_variable_set(:@__tokyo__received_messages__, {})
13
+ end
14
+
15
+ attr_reader :expected_message, :received_messages
16
+
17
+ def initialize object, expected_message, assert, caller
18
+ @object = object
19
+ @expected_message = expected_message.to_sym
20
+ @assert = assert
21
+ @caller = caller
22
+ proxify(@object, @expected_message)
23
+ end
24
+
25
+ def validate
26
+ @received_messages = @object.__tokyo__received_messages__[expected_message] || []
27
+ return refute_message_received unless @assert
28
+ assert_message_received
29
+ assert_message_received_with_correct_arguments
30
+ assert_message_returned_correct_value
31
+ assert_message_raised_as_expected
32
+ assert_message_thrown_as_expected
33
+ end
34
+
35
+ private
36
+ def proxify object, method
37
+ mount_original_methods_depot(object)
38
+ mount_received_messages_depot(object)
39
+ store_original_method(object, method)
40
+
41
+ object.define_singleton_method method do |*a,&b|
42
+ log = {arguments: a, block: b, caller: Tokyo.relative_location(caller[0])}
43
+ (__tokyo__received_messages__[method] ||= []).push(log)
44
+
45
+ if __tokyo__original_methods__[method]
46
+ return begin
47
+ log[:returned] = __tokyo__original_methods__[method].call(*a, &b)
48
+ rescue UncaughtThrowError => e
49
+ log[:thrown] = Tokyo.extract_thrown_symbol(e)
50
+ rescue Exception => e
51
+ log[:raised] = e
52
+ end
53
+ end
54
+
55
+ if respond_to?(:method_missing)
56
+ return begin
57
+ log[:returned] = __send__(:method_missing, method, *a, &b)
58
+ rescue UncaughtThrowError => e
59
+ log[:thrown] = Tokyo.extract_thrown_symbol(e)
60
+ rescue Exception => e
61
+ log[:raised] = e
62
+ end
63
+ end
64
+
65
+ log[:raised] = NoMethodError.new("undefined method `%s' for %s:%s" % [method, self.inspect, self.class])
66
+ end
67
+ end
68
+
69
+ def mount_original_methods_depot object
70
+ return if object.respond_to?(:__tokyo__original_methods__)
71
+ def object.__tokyo__original_methods__; @__tokyo__original_methods__ ||= {} end
72
+ end
73
+
74
+ def mount_received_messages_depot object
75
+ return if object.respond_to?(:__tokyo__received_messages__)
76
+ def object.__tokyo__received_messages__; @__tokyo__received_messages__ ||= {} end
77
+ end
78
+
79
+ def store_original_method object, method
80
+ return unless object.respond_to?(method)
81
+ object.__tokyo__original_methods__[method] ||= object.method(method)
82
+ end
83
+
84
+ def assert_message_received
85
+ Tokyo.fail('Expected %s to receive %s message' % [
86
+ Tokyo.pp(@object),
87
+ Tokyo.pp(expected_message)
88
+ ], @caller) if received_messages.empty?
89
+ end
90
+
91
+ def refute_message_received
92
+ Tokyo.fail('Not Expected %s to receive %s message' % [
93
+ Tokyo.pp(@object),
94
+ Tokyo.pp(expected_message)
95
+ ], @caller) if received_messages.any?
96
+ end
97
+ end
98
+ end
99
+
100
+ require 'tokyo/expectations/with'
101
+ require 'tokyo/expectations/return'
102
+ require 'tokyo/expectations/raise'
103
+ require 'tokyo/expectations/throw'
@@ -0,0 +1,55 @@
1
+ require 'pp'
2
+ require 'coderay'
3
+
4
+ # Stolen from [Pry](https://github.com/pry/pry)
5
+ #
6
+ # Copyright (c) 2013 John Mair (banisterfiend)
7
+ # Copyright (c) 2015 Slee Woo (sleewoo)
8
+ #
9
+ # Permission is hereby granted, free of charge, to any person obtaining
10
+ # a copy of this software and associated documentation files (the
11
+ # 'Software'), to deal in the Software without restriction, including
12
+ # without limitation the rights to use, copy, modify, merge, publish,
13
+ # distribute, sublicense, and/or sell copies of the Software, and to
14
+ # permit persons to whom the Software is furnished to do so, subject to
15
+ # the following conditions:
16
+ #
17
+ # The above copyright notice and this permission notice shall be
18
+ # included in all copies or substantial portions of the Software.
19
+ #
20
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
21
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
23
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
24
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
25
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
26
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27
+
28
+ module Tokyo
29
+ def pp obj
30
+ out = ''
31
+ q = Tokyo::PrettyPrint.new(out)
32
+ q.guard_inspect_key { q.pp(obj) }
33
+ q.flush
34
+ out
35
+ end
36
+
37
+ class PrettyPrint < ::PP
38
+ OBJECT_LITERAL_FORMAT = "\e[32m%s\e[0m".freeze
39
+
40
+ def text str, width = str.length
41
+ if str.include?("\e[")
42
+ super "%s\e[0m" % str, width
43
+ elsif str.start_with?('#<') || str == '=' || str == '>'
44
+ super highlight_object_literal(str), width
45
+ else
46
+ super CodeRay.scan(str, :ruby).term, width
47
+ end
48
+ end
49
+
50
+ private
51
+ def highlight_object_literal object_literal
52
+ OBJECT_LITERAL_FORMAT % object_literal
53
+ end
54
+ end
55
+ end
data/lib/tokyo/run.rb ADDED
@@ -0,0 +1,126 @@
1
+ module Tokyo
2
+
3
+ def progress
4
+ @progress ||= TTY::ProgressBar.new ':current of :total [:bar]' do |cfg|
5
+ cfg.total = units.map {|u| u.tests.size}.reduce(:+) || 0
6
+ cfg.width = TTY::Screen.width
7
+ cfg.complete = '.'
8
+ end
9
+ end
10
+
11
+ def run pattern_or_files = DEFAULT_PATTERN
12
+ specs = 0
13
+ tests = 0
14
+ assertions = 0
15
+ find_files(pattern_or_files).shuffle.each do |file|
16
+ r, w = IO.pipe
17
+ pid = Kernel.fork do
18
+ r.close
19
+ load_file(file)
20
+ progress.log ''
21
+ progress.log cyan(relative_location(file))
22
+ units.shuffle.each do |unit|
23
+ # exceptions raised inside unit#__run__ will be treated as failures and pretty printed.
24
+ # any other exceptions will be treated as implementation errors and ugly printed.
25
+ unit.tests.keys.shuffle.each do |test|
26
+ status = unit.run(test)
27
+ if status.is_a?(Skip)
28
+ w.puts({skip: true, reason: status.reason, caller: status.caller}.to_json)
29
+ else
30
+ unless status == :__tokyo_passed__
31
+ render_failure(unit, unit.tests[test], status)
32
+ Kernel.exit(1)
33
+ end
34
+ end
35
+ progress.advance
36
+ end
37
+ end
38
+ w.puts(totals.to_json)
39
+ end
40
+ _, status = Process.waitpid2(pid)
41
+ Kernel.exit(1) unless status.success?
42
+ w.close
43
+ while line = r.gets
44
+ line = JSON.parse(line)
45
+ if line['skip']
46
+ skips << line
47
+ elsif line['totals']
48
+ specs += line['specs']
49
+ tests += line['tests']
50
+ assertions += line['assertions']
51
+ else
52
+ raise('Incomprehensible message received: %s' % line)
53
+ end
54
+ end
55
+ end
56
+ render_skips
57
+ render_totals(specs, tests, assertions)
58
+ end
59
+
60
+ def totals
61
+ {
62
+ totals: true,
63
+ specs: units.select {|u| u.__ancestors__.empty?}.size,
64
+ tests: units.map {|u| u.tests.size}.reduce(:+),
65
+ assertions: total_assertions.size
66
+ }
67
+ end
68
+
69
+ def render_totals specs, tests, assertions
70
+ puts
71
+ puts
72
+ puts bold.cyan(' Specs: %i' % specs)
73
+ puts bold.cyan(' Tests: %i' % tests)
74
+ puts bold.cyan(' Assertions: %i' % assertions)
75
+ puts
76
+ end
77
+
78
+ def render_failure unit, test_uuid, failure
79
+ indent = ''
80
+ [*unit.__ancestors__, unit].each do |u|
81
+ progress.log indent + u.__identity__
82
+ indent << INDENT
83
+ end
84
+ progress.log indent + test_uuid
85
+ indent << INDENT
86
+ case failure
87
+ when Exception
88
+ render_exception(indent, failure)
89
+ when GenericFailure, AssertionFailure
90
+ render_caller(indent, failure.caller)
91
+ __send__('render_%s' % failure.class.name.split('::').last, indent, failure)
92
+ else
93
+ progress.log(indent + failure.inspect)
94
+ end
95
+ progress.log ''
96
+ end
97
+
98
+ def render_exception indent, failure
99
+ progress.log indent + underline.bright_red([failure.class, failure.message]*': ')
100
+ pretty_backtrace(failure).each {|l| progress.log(indent + l)}
101
+ end
102
+
103
+ def render_GenericFailure indent, failure
104
+ Array(failure.reason).each {|l| progress.log(indent + l.to_s)}
105
+ end
106
+
107
+ def render_AssertionFailure indent, failure
108
+ progress.log indent + cyan('a: ') + pp(failure.object)
109
+ progress.log indent + cyan('b: ') + failure.arguments.map {|a| pp(a)}.join(', ')
110
+ end
111
+
112
+ def render_caller indent, caller
113
+ return unless caller
114
+ progress.log indent + underline.bright_red(readline(caller))
115
+ end
116
+
117
+ def render_skips
118
+ return if skips.empty?
119
+ puts
120
+ puts bold.magenta('Skips:')
121
+ skips.each do |skip|
122
+ puts ' %s (%s)' % [blue(skip['reason'] || 'skip'), relative_location(skip['caller'])]
123
+ end
124
+ puts
125
+ end
126
+ end
data/lib/tokyo/unit.rb ADDED
@@ -0,0 +1,185 @@
1
+ module Tokyo
2
+ class Unit
3
+
4
+ # @example
5
+ # assert(x) == y
6
+ # assert(x).include?(y)
7
+ # assert(x).nil?
8
+ # refute(a).include?(b)
9
+ # fail_if(a).include?(b)
10
+ [
11
+ :assert,
12
+ :refute
13
+ ].each do |meth|
14
+ define_method meth do |obj = nil, &block|
15
+ __objects__[obj = block ? Tokyo.call_block(block) : {returned: obj}.freeze] = true
16
+ __assertions__.push(Tokyo::Assert.new(obj, meth, caller[0])).last
17
+ end
18
+ end
19
+ alias expect assert
20
+ alias fail_if refute
21
+
22
+ # stop executing current test and mark it as skipped
23
+ #
24
+ # @example
25
+ # test :something do
26
+ # skip "recheck this after fixing X"
27
+ # assert(x) == y # this wont run
28
+ # end
29
+ #
30
+ def skip reason = nil
31
+ throw(:__tokyo_status__, Tokyo::Skip.new(reason, caller[0]))
32
+ end
33
+
34
+ def __run__ test, before, around, after
35
+ __send__(before) if before
36
+ if around
37
+ __send__(around, proc {__send__(test)})
38
+ else
39
+ __send__(test)
40
+ end
41
+ __send__(after) if after
42
+ __assertions__.each(&:__validate_expectations__)
43
+ __objects__.each_key {|o| Tokyo::Expectation.restore_object_status(o[:returned])}
44
+ :__tokyo_passed__
45
+ rescue Exception => e
46
+ throw(:__tokyo_status__, e)
47
+ end
48
+
49
+ private
50
+ def __objects__
51
+ @__objects__ ||= {}
52
+ end
53
+
54
+ def __assertions__
55
+ @__assertions__ ||= []
56
+ end
57
+ end
58
+
59
+ class << Unit
60
+
61
+ def inherited base
62
+ base.instance_variable_set(:@__tokyo_hooks__, Marshal.load(Marshal.dump(hooks)))
63
+ end
64
+
65
+ def spec
66
+ raise(NotImplementedError, 'Nested specs not supported. Please use a context instead')
67
+ end
68
+
69
+ # define a context inside current spec/context.
70
+ #
71
+ # @param label
72
+ # @param &block
73
+ #
74
+ def context label, &block
75
+ return unless block
76
+ Tokyo.define_and_register_a_context(label, block, self)
77
+ end
78
+
79
+ # define a test
80
+ #
81
+ # @param label
82
+ # @param &block
83
+ #
84
+ def test label, &block
85
+ return unless block
86
+ tests[label] = Tokyo.identity_string(:test, label, block)
87
+ define_method(tests[label], &block)
88
+ end
89
+ alias it test
90
+ alias should test
91
+
92
+ def tests
93
+ @__tokyo_tests__ ||= {}
94
+ end
95
+
96
+ # run some code before/around/after tests
97
+ #
98
+ # @example
99
+ # spec User do
100
+ # before do
101
+ # @users = ...
102
+ # end
103
+ #
104
+ # test :login do
105
+ # # @users available here
106
+ # end
107
+ # end
108
+ #
109
+ # @example
110
+ # spec User do
111
+ # around |&test|
112
+ # SomeApi.with_auth do
113
+ # test.call # run the test
114
+ # end
115
+ # end
116
+ #
117
+ # test :authorize do
118
+ # # will run inside `with_auth` block
119
+ # end
120
+ # end
121
+ #
122
+ # @note hooks are inherited from parent spec or context
123
+ #
124
+ # @example
125
+ # spec Math do
126
+ # before { @n = rand }
127
+ #
128
+ # context :PI do
129
+ # test :some_test do
130
+ # # @n available here
131
+ # end
132
+ # end
133
+ # end
134
+ #
135
+ # @note defined hooks will override inherited ones
136
+ #
137
+ # @example
138
+ # spec Math do
139
+ # before { @n = 1 }
140
+ #
141
+ # context :x do
142
+ # before { @n = 0 }
143
+ # test :some_test do
144
+ # # @n is zero here
145
+ # end
146
+ # end
147
+ # end
148
+ #
149
+ [
150
+ :before,
151
+ :around,
152
+ :after
153
+ ].each do |hook|
154
+ define_method hook do |&block|
155
+ block || raise(ArgumentError, 'block missing')
156
+ hooks[hook] = :"__tokyo__#{hook}_hook__"
157
+ define_method(hooks[hook], &block)
158
+ end
159
+ end
160
+
161
+ def hooks
162
+ @__tokyo_hooks__ ||= {}
163
+ end
164
+
165
+ # skipping a whole spec/context
166
+ #
167
+ # @example
168
+ # spec Array do
169
+ # skip
170
+ #
171
+ # # code here wont be executed
172
+ # end
173
+ #
174
+ def skip reason = nil
175
+ throw(:__tokyo_skip__, Tokyo::Skip.new(reason, caller[0]))
176
+ end
177
+
178
+ def run test
179
+ tests[test] || raise(StandardError, 'Undefined test %s at "%s"' % [test.inspect, __identity__])
180
+ catch :__tokyo_status__ do
181
+ allocate.__run__(tests[test], hooks[:before], hooks[:around], hooks[:after])
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,54 @@
1
+ module Tokyo
2
+
3
+ def assert_raised_as_expected object, expected_type = nil, expected_message = nil, block = nil
4
+ f = assert_raised(object)
5
+ return f if f
6
+
7
+ return assert_raised_as_expected_by_block(object, block) if block
8
+
9
+ if expected_type
10
+ f = assert_raised_expected_type(object, expected_type)
11
+ return f if f
12
+ end
13
+
14
+ if expected_message
15
+ f = assert_raised_expected_message(object, expected_message)
16
+ return f if f
17
+ end
18
+ nil
19
+ end
20
+
21
+ def assert_raised object
22
+ return [
23
+ 'Expected a exception to be raised at %s' % object[:caller]
24
+ ] unless object[:raised]
25
+ nil
26
+ end
27
+
28
+ def assert_raised_as_expected_by_block object, block
29
+ return [
30
+ 'Looks like wrong or no error raised at %s' % object[:caller],
31
+ 'See validation block'
32
+ ] unless block.call(object[:raised])
33
+ nil
34
+ end
35
+
36
+ def assert_raised_expected_type object, expected_type
37
+ return [
38
+ 'Expected a %s to be raised at %s' % [expected_type, object[:caller]],
39
+ 'Instead a %s raised' % object[:raised].class
40
+ ] unless object[:raised].class == expected_type
41
+ nil
42
+ end
43
+
44
+ def assert_raised_expected_message object, expected_message
45
+ regexp = expected_message.is_a?(Regexp) ? expected_message : /\A#{expected_message}\z/
46
+ return [
47
+ 'Expected the exception raised at %s' % object[:caller],
48
+ 'to match "%s"' % regexp.source,
49
+ 'Instead it looks like',
50
+ pp(object[:raised].message)
51
+ ] unless object[:raised].message =~ regexp
52
+ nil
53
+ end
54
+ end
@@ -0,0 +1,40 @@
1
+ module Tokyo
2
+
3
+ def assert_thrown_as_expected object, expected_symbol = nil, block = nil
4
+ f = assert_thrown(object)
5
+ return f if f
6
+
7
+ return assert_thrown_as_expected_by_block(object, block) if block
8
+
9
+ if expected_symbol
10
+ f = assert_expected_symbol_thrown(object, expected_symbol)
11
+ return f if f
12
+ end
13
+ nil
14
+ end
15
+
16
+ def assert_thrown_as_expected_by_block object, block
17
+ return [
18
+ 'Looks like wrong or no symbol thrown at %s' % object[:caller],
19
+ 'See validating block'
20
+ ] unless block.call(object[:thrown])
21
+ nil
22
+ end
23
+
24
+ def assert_thrown object
25
+ return [
26
+ 'Expected a symbol to be thrown at %s' % object[:caller]
27
+ ] unless object[:thrown]
28
+ nil
29
+ end
30
+
31
+ def assert_expected_symbol_thrown object, expected_symbol
32
+ return begin
33
+ [
34
+ 'Expected :%s to be thrown at %s' % [expected_symbol, object[:caller]],
35
+ 'Instead :%s thrown' % object[:thrown]
36
+ ]
37
+ end unless expected_symbol == object[:thrown]
38
+ nil
39
+ end
40
+ end
@@ -0,0 +1,48 @@
1
+ module Tokyo
2
+
3
+ def refute_raised_as_expected object, expected_type, expected_message, block = nil
4
+ f = refute_raised(object, expected_type || expected_message)
5
+ return f if f
6
+
7
+ if expected_type
8
+ f = refute_raised_expected_type(object, expected_type)
9
+ return f if f
10
+ end
11
+
12
+ if expected_message
13
+ f = refute_raised_expected_message(object, expected_message)
14
+ return f if f
15
+ end
16
+ nil
17
+ end
18
+
19
+ def refute_raised object, should_raise = false
20
+ if should_raise
21
+ return [
22
+ 'Expected a exception to be raised at %s' % object[:caller]
23
+ ] unless object[:raised]
24
+ else
25
+ return [
26
+ 'A unexpected exception raised at %s' % object[:caller],
27
+ object[:raised]
28
+ ] if object[:raised]
29
+ end
30
+ nil
31
+ end
32
+
33
+ def refute_raised_expected_type object, expected_type
34
+ return [
35
+ 'Not expected a %s to be raised at %s' % [object[:raised].class, object[:caller]],
36
+ ] if object[:raised].class == expected_type
37
+ nil
38
+ end
39
+
40
+ def refute_raised_expected_message object, expected_message
41
+ regexp = expected_message.is_a?(Regexp) ? expected_message : /\A#{expected_message}\z/
42
+ return [
43
+ 'Not expected raised exception to match %s' % regexp.source,
44
+ object[:raised]
45
+ ] if object[:raised].message =~ regexp
46
+ nil
47
+ end
48
+ end