tokyo 0.0.5

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,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