Module: Tokyo

Extended by:
Tokyo
Included in:
Tokyo
Defined in:
lib/tokyo/pretty_print.rb,
lib/tokyo.rb,
lib/tokyo/run.rb,
lib/tokyo/unit.rb,
lib/tokyo/util.rb,
lib/tokyo/assert.rb,
lib/tokyo/expectations.rb,
lib/tokyo/expectations/with.rb,
lib/tokyo/util/assert_raise.rb,
lib/tokyo/util/assert_throw.rb,
lib/tokyo/util/refute_raise.rb,
lib/tokyo/util/refute_throw.rb,
lib/tokyo/expectations/raise.rb,
lib/tokyo/expectations/throw.rb,
lib/tokyo/expectations/return.rb

Overview

Stolen from [Pry](github.com/pry/pry)

Copyright © 2013 John Mair (banisterfiend) Copyright © 2015 Slee Woo (sleewoo)

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Defined Under Namespace

Classes: Assert, AssertionFailure, Expectation, GenericFailure, PrettyPrint, Skip, Unit

Constant Summary collapse

DEFAULT_PATTERN =
'{spec,test}/**/{*_spec.rb,*_test.rb}'.freeze
GLOBAL_SETUPS =
[]
INDENT =
'  '.freeze
PASTEL =
Pastel.new

Instance Method Summary collapse

Instance Method Details

#assert_expected_symbol_thrown(object, expected_symbol) ⇒ Object



31
32
33
34
35
36
37
38
39
# File 'lib/tokyo/util/assert_throw.rb', line 31

def assert_expected_symbol_thrown object, expected_symbol
  return begin
    [
      'Expected :%s to be thrown at %s' % [expected_symbol, object[:caller]],
      'Instead :%s thrown' % object[:thrown]
    ]
  end unless expected_symbol == object[:thrown]
  nil
end

#assert_raised(object) ⇒ Object



21
22
23
24
25
26
# File 'lib/tokyo/util/assert_raise.rb', line 21

def assert_raised object
  return [
    'Expected a exception to be raised at %s' % object[:caller]
  ] unless object[:raised]
  nil
end

#assert_raised_as_expected(object, expected_type = nil, expected_message = nil, block = nil) ⇒ Object



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# File 'lib/tokyo/util/assert_raise.rb', line 3

def assert_raised_as_expected object, expected_type = nil, expected_message = nil, block = nil
  f = assert_raised(object)
  return f if f

  return assert_raised_as_expected_by_block(object, block) if block

  if expected_type
    f = assert_raised_expected_type(object, expected_type)
    return f if f
  end

  if expected_message
    f = assert_raised_expected_message(object, expected_message)
    return f if f
  end
  nil
end

#assert_raised_as_expected_by_block(object, block) ⇒ Object



28
29
30
31
32
33
34
# File 'lib/tokyo/util/assert_raise.rb', line 28

def assert_raised_as_expected_by_block object, block
  return [
    'Looks like wrong or no error raised at %s' % object[:caller],
    'See validation block'
  ] unless block.call(object[:raised])
  nil
end

#assert_raised_expected_message(object, expected_message) ⇒ Object



44
45
46
47
48
49
50
51
52
53
# File 'lib/tokyo/util/assert_raise.rb', line 44

def assert_raised_expected_message object, expected_message
  regexp = expected_message.is_a?(Regexp) ? expected_message : /\A#{expected_message}\z/
  return [
    'Expected the exception raised at %s' % object[:caller],
    'to match "%s"' % regexp.source,
    'Instead it looks like',
    pp(object[:raised].message)
  ] unless object[:raised].message =~ regexp
  nil
end

#assert_raised_expected_type(object, expected_type) ⇒ Object



36
37
38
39
40
41
42
# File 'lib/tokyo/util/assert_raise.rb', line 36

def assert_raised_expected_type object, expected_type
  return [
    'Expected a %s to be raised at %s' % [expected_type, object[:caller]],
    'Instead a %s raised' % object[:raised].class
  ] unless object[:raised].class == expected_type
  nil
end

#assert_thrown(object) ⇒ Object



24
25
26
27
28
29
# File 'lib/tokyo/util/assert_throw.rb', line 24

def assert_thrown object
  return [
    'Expected a symbol to be thrown at %s' % object[:caller]
  ] unless object[:thrown]
  nil
end

#assert_thrown_as_expected(object, expected_symbol = nil, block = nil) ⇒ Object



3
4
5
6
7
8
9
10
11
12
13
14
# File 'lib/tokyo/util/assert_throw.rb', line 3

def assert_thrown_as_expected object, expected_symbol = nil, block = nil
  f = assert_thrown(object)
  return f if f

  return assert_thrown_as_expected_by_block(object, block) if block

  if expected_symbol
    f = assert_expected_symbol_thrown(object, expected_symbol)
    return f if f
  end
  nil
end

#assert_thrown_as_expected_by_block(object, block) ⇒ Object



16
17
18
19
20
21
22
# File 'lib/tokyo/util/assert_throw.rb', line 16

def assert_thrown_as_expected_by_block object, block
  return [
    'Looks like wrong or no symbol thrown at %s' % object[:caller],
    'See validating block'
  ] unless block.call(object[:thrown])
  nil
end

#assertionsObject



34
35
36
# File 'lib/tokyo.rb', line 34

def assertions
  @assertions ||= {}
end

#augment_load_path(file) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/tokyo/util.rb', line 68

def augment_load_path file
  # adding ./
  $:.unshift(pwd) unless $:.include?(pwd)

  # adding ./lib/
  lib = pwd('lib')
  unless $:.include?(lib)
    $:.unshift(lib) if File.directory?(lib)
  end

  # adding file's dirname
  dir = File.dirname(file)
  $:.unshift(dir) unless $:.include?(dir)
end

#call_block(block) ⇒ Object



3
4
5
6
7
8
9
# File 'lib/tokyo/util.rb', line 3

def call_block block
  {returned: block.call, caller: relative_source_location(block)}.freeze
rescue UncaughtThrowError => e
  {raised: e, thrown: extract_thrown_symbol(e), caller: relative_source_location(block)}.freeze
rescue Exception => e
  {raised: e, caller: relative_source_location(block)}.freeze
end

#caller_to_source_location(caller) ⇒ Object



53
54
55
56
# File 'lib/tokyo/util.rb', line 53

def caller_to_source_location caller
  file, line = caller.split(/:(\d+):in.+/)
  [relative_location(file), line]
end

#define_and_register_a_context(label, block, parent) ⇒ Object



58
59
60
# File 'lib/tokyo.rb', line 58

def define_and_register_a_context label, block, parent
  units << define_context(label, block, parent)
end

#define_and_register_a_spec(label, block) ⇒ Object



50
51
52
# File 'lib/tokyo.rb', line 50

def define_and_register_a_spec label, block
  units << define_spec(label, block)
end

#define_context(label, block, parent) ⇒ Object



54
55
56
# File 'lib/tokyo.rb', line 54

def define_context label, block, parent
  define_unit_class(:context, label, block, [*parent.__ancestors__, parent].freeze)
end

#define_spec(label, block) ⇒ Object



46
47
48
# File 'lib/tokyo.rb', line 46

def define_spec label, block
  define_unit_class(:spec, label, block, [].freeze)
end

#define_unit_class(type, label, block, ancestors) ⇒ Unit

define a class that will hold contexts and tests

Parameters:

  • type (String, Symbol)
  • label (String, Symbol)
  • block (Proc)
  • ancestors (Array)

Returns:



70
71
72
73
74
75
76
77
78
79
80
# File 'lib/tokyo.rb', line 70

def define_unit_class type, label, block, ancestors
  identity = identity_string(type, label, block).freeze
  Class.new ancestors.last || Unit do
    define_singleton_method(:__ancestors__) {ancestors}
    define_singleton_method(:__identity__) {identity}
    Tokyo::GLOBAL_SETUPS.each {|b| class_exec(&b)}
    # execute given block only after global setups executed and all utility methods defined
    result = catch(:__tokyo_skip__) {class_exec(&block)}
    Tokyo.skips << result if result.is_a?(Skip)
  end
end

#define_unit_module(block) ⇒ Module

define a module that when included will execute the given block on base

Parameters:

  • block (Proc)

Returns:

  • (Module)


87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/tokyo.rb', line 87

def define_unit_module block
  block || raise(ArgumentError, 'missing block')
  Module.new do
    # any spec/context that will include this module will "inherit" it's logic
    #
    # @example
    #   EnumeratorSpec = spec 'Enumerator tests' do
    #     # some tests here
    #   end
    #
    #   spec Array do
    #     include EnumeratorSpec
    #   end
    #
    #   spec Hash do
    #     include EnumeratorSpec
    #   end
    #
    define_singleton_method(:included) {|b| b.class_exec(&block)}
  end
end

#extract_thrown_symbol(exception) ⇒ Object

extract thrown symbol from given exception

Parameters:

  • exception


15
16
17
18
19
# File 'lib/tokyo/util.rb', line 15

def extract_thrown_symbol exception
  return unless exception.is_a?(Exception)
  return unless s = exception.message.scan(/uncaught throw\W+(\w+)/).flatten[0]
  s.to_sym
end

#fail(reason, caller) ⇒ Object

stop any code and report a failure



110
111
112
# File 'lib/tokyo.rb', line 110

def fail reason, caller
  throw(:__tokyo_status__, GenericFailure.new(Array(reason), caller))
end

#find_files(pattern_or_files) ⇒ Object



58
59
60
61
# File 'lib/tokyo/util.rb', line 58

def find_files pattern_or_files
  return pattern_or_files if pattern_or_files.is_a?(Array)
  Dir[pwd(pattern_or_files)]
end

#identity_string(type, label, block) ⇒ Object



21
22
23
24
25
26
27
# File 'lib/tokyo/util.rb', line 21

def identity_string type, label, block
  '%s %s (%s:%s)' % [
    blue(type),
    label.inspect,
    *relative_source_location(block)
  ]
end

#load_file(file) ⇒ Object



63
64
65
66
# File 'lib/tokyo/util.rb', line 63

def load_file file
  augment_load_path(file)
  require(file)
end

#pp(obj) ⇒ Object



29
30
31
32
33
34
35
# File 'lib/tokyo/pretty_print.rb', line 29

def pp obj
  out = ''
  q = Tokyo::PrettyPrint.new(out)
  q.guard_inspect_key { q.pp(obj) }
  q.flush
  out
end

#pretty_backtrace(e) ⇒ Object



41
42
43
# File 'lib/tokyo/util.rb', line 41

def pretty_backtrace e
  Array(e.backtrace).map {|l| relative_location(l)}
end

#progressObject



3
4
5
6
7
8
9
# File 'lib/tokyo/run.rb', line 3

def progress
  @progress ||= TTY::ProgressBar.new ':current of :total [:bar]' do |cfg|
    cfg.total = units.map {|u| u.tests.size}.reduce(:+) || 0
    cfg.width = TTY::Screen.width
    cfg.complete = '.'
  end
end

#pwd(*args) ⇒ Object



83
84
85
# File 'lib/tokyo/util.rb', line 83

def pwd *args
  File.join(Dir.pwd, *args.map!(&:to_s))
end

#readline(caller) ⇒ Object



45
46
47
48
49
50
51
# File 'lib/tokyo/util.rb', line 45

def readline caller
  file, line = caller_to_source_location(caller)
  return unless file && line
  lines = ((@__readlinecache__ ||= {})[file] ||= File.readlines(file))
  return unless line = lines[line.to_i - 1]
  line.sub(/(do|\{)\Z/, '').strip
end

#refute_expected_symbol_thrown(object, expected_symbol) ⇒ Object



27
28
29
30
31
32
33
# File 'lib/tokyo/util/refute_throw.rb', line 27

def refute_expected_symbol_thrown object, expected_symbol
  return [
    'Not expected :%s to be thrown' % expected_symbol,
    'at %s' % object[:caller]
  ] if expected_symbol == object[:thrown]
  nil
end

#refute_raised(object, should_raise = false) ⇒ Object



19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/tokyo/util/refute_raise.rb', line 19

def refute_raised object, should_raise = false
  if should_raise
    return [
      'Expected a exception to be raised at %s' % object[:caller]
    ] unless object[:raised]
  else
    return [
      'A unexpected exception raised at %s' % object[:caller],
      object[:raised]
    ] if object[:raised]
  end
  nil
end

#refute_raised_as_expected(object, expected_type, expected_message, block = nil) ⇒ Object



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# File 'lib/tokyo/util/refute_raise.rb', line 3

def refute_raised_as_expected object, expected_type, expected_message, block = nil
  f = refute_raised(object, expected_type || expected_message)
  return f if f

  if expected_type
    f = refute_raised_expected_type(object, expected_type)
    return f if f
  end

  if expected_message
    f = refute_raised_expected_message(object, expected_message)
    return f if f
  end
  nil
end

#refute_raised_expected_message(object, expected_message) ⇒ Object



40
41
42
43
44
45
46
47
# File 'lib/tokyo/util/refute_raise.rb', line 40

def refute_raised_expected_message object, expected_message
  regexp = expected_message.is_a?(Regexp) ? expected_message : /\A#{expected_message}\z/
  return [
    'Not expected raised exception to match %s' % regexp.source,
    object[:raised]
  ] if object[:raised].message =~ regexp
  nil
end

#refute_raised_expected_type(object, expected_type) ⇒ Object



33
34
35
36
37
38
# File 'lib/tokyo/util/refute_raise.rb', line 33

def refute_raised_expected_type object, expected_type
  return [
    'Not expected a %s to be raised at %s' % [object[:raised].class, object[:caller]],
  ] if object[:raised].class == expected_type
  nil
end

#refute_thrown(object, should_throw = false) ⇒ Object



14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/tokyo/util/refute_throw.rb', line 14

def refute_thrown object, should_throw = false
  if should_throw
    return [
      'Expected a symbol to be thrown at %s' % object[:caller]
    ] unless object[:thrown]
  else
    return [
      'Not expected a symbol to be thrown at %s' % object[:caller]
    ] if object[:thrown]
  end
  nil
end

#refute_thrown_as_expected(object, expected_symbol, block = nil) ⇒ Object



3
4
5
6
7
8
9
10
11
12
# File 'lib/tokyo/util/refute_throw.rb', line 3

def refute_thrown_as_expected object, expected_symbol, block = nil
  f = refute_thrown(object, expected_symbol)
  return f if f

  if expected_symbol
    f = refute_expected_symbol_thrown(object, expected_symbol)
    return f if f
  end
  nil
end

#relative_location(line) ⇒ Object



37
38
39
# File 'lib/tokyo/util.rb', line 37

def relative_location line
  line.sub(/\A#{pwd}\/+/, '')
end

#relative_source_location(block) ⇒ Object



29
30
31
32
33
34
35
# File 'lib/tokyo/util.rb', line 29

def relative_source_location block
  return unless block
  [
    relative_location(block.source_location[0]),
    block.source_location[1]
  ]
end

#render_AssertionFailure(indent, failure) ⇒ Object



107
108
109
110
# File 'lib/tokyo/run.rb', line 107

def render_AssertionFailure indent, failure
  progress.log indent + cyan('a: ') + pp(failure.object)
  progress.log indent + cyan('b: ') + failure.arguments.map {|a| pp(a)}.join(', ')
end

#render_caller(indent, caller) ⇒ Object



112
113
114
115
# File 'lib/tokyo/run.rb', line 112

def render_caller indent, caller
  return unless caller
  progress.log indent + underline.bright_red(readline(caller))
end

#render_exception(indent, failure) ⇒ Object



98
99
100
101
# File 'lib/tokyo/run.rb', line 98

def render_exception indent, failure
  progress.log indent + underline.bright_red([failure.class, failure.message]*': ')
  pretty_backtrace(failure).each {|l| progress.log(indent + l)}
end

#render_failure(unit, test_uuid, failure) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/tokyo/run.rb', line 78

def render_failure unit, test_uuid, failure
  indent = ''
  [*unit.__ancestors__, unit].each do |u|
    progress.log indent + u.__identity__
    indent << INDENT
  end
  progress.log indent + test_uuid
  indent << INDENT
  case failure
  when Exception
    render_exception(indent, failure)
  when GenericFailure, AssertionFailure
    render_caller(indent, failure.caller)
    __send__('render_%s' % failure.class.name.split('::').last, indent, failure)
  else
    progress.log(indent + failure.inspect)
  end
  progress.log ''
end

#render_GenericFailure(indent, failure) ⇒ Object



103
104
105
# File 'lib/tokyo/run.rb', line 103

def render_GenericFailure indent, failure
  Array(failure.reason).each {|l| progress.log(indent + l.to_s)}
end

#render_skipsObject



117
118
119
120
121
122
123
124
125
# File 'lib/tokyo/run.rb', line 117

def render_skips
  return if skips.empty?
  puts
  puts bold.magenta('Skips:')
  skips.each do |skip|
    puts '  %s (%s)' % [blue(skip['reason'] || 'skip'), relative_location(skip['caller'])]
  end
  puts
end

#render_totals(specs, tests, assertions) ⇒ Object



69
70
71
72
73
74
75
76
# File 'lib/tokyo/run.rb', line 69

def render_totals specs, tests, assertions
  puts
  puts
  puts bold.cyan('           Specs: %i' % specs)
  puts bold.cyan('           Tests: %i' % tests)
  puts bold.cyan('      Assertions: %i' % assertions)
  puts
end

#run(pattern_or_files = DEFAULT_PATTERN) ⇒ Object



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/tokyo/run.rb', line 11

def run pattern_or_files = DEFAULT_PATTERN
  specs = 0
  tests = 0
  assertions = 0
  find_files(pattern_or_files).shuffle.each do |file|
    r, w = IO.pipe
    pid = Kernel.fork do
      r.close
      load_file(file)
      progress.log ''
      progress.log cyan(relative_location(file))
      units.shuffle.each do |unit|
        # exceptions raised inside unit#__run__ will be treated as failures and pretty printed.
        # any other exceptions will be treated as implementation errors and ugly printed.
        unit.tests.keys.shuffle.each do |test|
          status = unit.run(test)
          if status.is_a?(Skip)
            w.puts({skip: true, reason: status.reason, caller: status.caller}.to_json)
          else
            unless status == :__tokyo_passed__
              render_failure(unit, unit.tests[test], status)
              Kernel.exit(1)
            end
          end
          progress.advance
        end
      end
      w.puts(totals.to_json)
    end
    _, status = Process.waitpid2(pid)
    Kernel.exit(1) unless status.success?
    w.close
    while line = r.gets
      line = JSON.parse(line)
      if line['skip']
        skips << line
      elsif line['totals']
        specs += line['specs']
        tests += line['tests']
        assertions += line['assertions']
      else
        raise('Incomprehensible message received: %s' % line)
      end
    end
  end
  render_skips
  render_totals(specs, tests, assertions)
end

#skipsObject



42
43
44
# File 'lib/tokyo.rb', line 42

def skips
  @skips ||= []
end

#total_assertionsObject



38
39
40
# File 'lib/tokyo.rb', line 38

def total_assertions
  @total_assertions ||= []
end

#totalsObject



60
61
62
63
64
65
66
67
# File 'lib/tokyo/run.rb', line 60

def totals
  {
    totals: true,
    specs: units.select {|u| u.__ancestors__.empty?}.size,
    tests: units.map {|u| u.tests.size}.reduce(:+),
    assertions: total_assertions.size
  }
end

#unitsObject



30
31
32
# File 'lib/tokyo.rb', line 30

def units
  @units ||= []
end