speq 0.3.0 → 0.4.0

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,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Some characters you can recruit for your tests
4
+ module Recruit
5
+ module_function
6
+
7
+ def duck(**mapping)
8
+ Duck.new(**mapping)
9
+ end
10
+
11
+ def spy(proxy_target = nil, permit_all_methods = false)
12
+ Spy.new(proxy_target, permit_all_methods)
13
+ end
14
+
15
+ # Mostly just quacks like one
16
+ class Duck
17
+ def initialize(**mapping)
18
+ mapping.each do |method_name, value|
19
+ if mapping[method_name].respond_to?(:call)
20
+ define_singleton_method(method_name, value)
21
+ else
22
+ define_singleton_method(method_name, &-> { mapping[method_name] })
23
+ define_singleton_method(
24
+ "#{method_name}=".to_sym,
25
+ &->(val) { mapping[method_name] = val }
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ # Lets you can keep an eye on your ducks
33
+ class Spy
34
+ def initialize(proxy_target = nil, permit_all_methods = false)
35
+ @report = Hash.new { |hash, key| hash[key] = [] }
36
+ @proxy_target = proxy_target
37
+ @permit_all_methods = permit_all_methods
38
+ end
39
+
40
+ def respond_to_missing?(method_name, *)
41
+ permit_all_methods ? true : @proxy_target&.respond_to?(method_name) || super
42
+ end
43
+
44
+ def method_missing(method_name, *args, &block)
45
+ @report[method_name] << [args, block]
46
+ if @proxy_target&.respond_to?(method_name)
47
+ @proxy_target.send(method_name, *args, &block)
48
+ else
49
+ super
50
+ end
51
+ rescue NoMethodError => e
52
+ @permit_all_methods ? nil : e
53
+ end
54
+ end
55
+
56
+ # It's the same thing, except cheaper and not very useful
57
+ # def dupe(target, **_mapping)
58
+ # klass = target.is_a?(Class) ? target : target.class
59
+
60
+ # Class.new do
61
+ # target.public_methods.each do |method_name|
62
+ # define_singleton_method(
63
+ # method_name,
64
+ # *klass.method(method_name).parameters
65
+ # )
66
+ # end
67
+ # end
68
+ # end
69
+
70
+ # Will let you claim `fake('Lies').equal?(Lies)`
71
+ def fake(
72
+ class_name = 'Fake',
73
+ super_class: Object,
74
+ namespace: Object,
75
+ avoid_collisions: false,
76
+ &block
77
+ )
78
+ if avoid_collisions && namespace.const_defined?(class_name)
79
+ throw NameError(
80
+ "constant '#{class_name}' already defined for #{namespace}"
81
+ )
82
+ end
83
+
84
+ namespace.const_set(class_name, Class.new(super_class, &block))
85
+ end
86
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'colorize'
4
+
5
+ module Speq
6
+ class TestBlock
7
+ def inspect
8
+ "block in '#{@parent.description}'"
9
+ end
10
+ end
11
+
12
+ class Group
13
+ def report
14
+ outcome.report
15
+ end
16
+
17
+ def indent
18
+ parent ? parent.indent + ' ' : ''
19
+ end
20
+
21
+ def newline
22
+ "\n#{indent}"
23
+ end
24
+ end
25
+
26
+ class Test < Group
27
+ def title
28
+ "#{outcome} #{description}"
29
+ end
30
+
31
+ def to_s
32
+ [newline, title, units.join(''), parent ? '' : newline + report].join('')
33
+ end
34
+ end
35
+
36
+ class Expression < Group
37
+ def indent
38
+ parent.indent
39
+ end
40
+
41
+ def to_s
42
+ "#{newline} #{outcome} #{context} #{units.join('; ')}."
43
+ end
44
+ end
45
+
46
+ class Question
47
+ def to_s
48
+ "#{phrase}#{outcome.fail? ? " (result = #{result.inspect})" : ''}"
49
+ .send(outcome.pass? ? :green : :red)
50
+ end
51
+ end
52
+
53
+ class Outcome
54
+ def report
55
+ "pass: #{pass_count}, fail: #{fail_count}, errors: #{error_count}"
56
+ end
57
+
58
+ def to_s
59
+ pass? ? '✔'.green : 'x'.red
60
+ end
61
+ end
62
+
63
+ class Arguments
64
+ def to_s
65
+ arg_str = args.map(&:inspect).join(', ')
66
+ sep = args.empty? && block ? '' : ', '
67
+ block_str = block ? "#{sep}&{ ... }" : ''
68
+ "#{arg_str}#{block_str}"
69
+ end
70
+ end
71
+
72
+ class SomeValue
73
+ def to_s
74
+ description || value.inspect
75
+ end
76
+ end
77
+
78
+ class Message < SomeValue
79
+ def to_s
80
+ extra = description ? " (#{description})" : nil
81
+ "#{value}#{extra}"
82
+ end
83
+ end
84
+
85
+ class Context
86
+ def to_s
87
+ [arguments && !message ? "with #{arguments}" : nil,
88
+ arguments && message ? "#{message}(#{arguments})" : message,
89
+ message && subject ? 'on' : nil,
90
+ subject].reject(&:nil?).join(' ')
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Speq
4
+ # Encodes the outcome of a group or individual test
5
+ class Outcome
6
+ def self.error
7
+ Outcome.new([0, 0, 1])
8
+ end
9
+
10
+ def self.pass
11
+ Outcome.new([1, 0, 0])
12
+ end
13
+
14
+ def self.fail
15
+ Outcome.new([0, 1, 0])
16
+ end
17
+
18
+ def self.aggregate(outcomes)
19
+ Outcome.new(
20
+ [outcomes.map(&:pass?).count(true),
21
+ outcomes.map(&:fail?).count(true),
22
+ outcomes.map(&:error?).count(true)]
23
+ )
24
+ end
25
+
26
+ attr_accessor :values
27
+ def initialize(values)
28
+ @values = values
29
+ end
30
+
31
+ def pass_count
32
+ values[0]
33
+ end
34
+
35
+ def fail_count
36
+ values[1]
37
+ end
38
+
39
+ def error_count
40
+ values[2]
41
+ end
42
+
43
+ def ==(other)
44
+ other.values == values
45
+ end
46
+
47
+ def pass?
48
+ fail_count.zero? && error_count.zero?
49
+ end
50
+
51
+ def fail?
52
+ !pass? && !error?
53
+ end
54
+
55
+ def error?
56
+ error_count.positive?
57
+ end
58
+ end
59
+
60
+ # Holds arguments to be passed to something later
61
+ class Arguments
62
+ attr_reader :args, :block
63
+ def initialize(*args, &block)
64
+ @args = args
65
+ @block = block if block_given?
66
+ end
67
+ end
68
+
69
+ # Holds any value allowing for a distiction between Some(nil) and None
70
+ class SomeValue
71
+ attr_reader :value
72
+ attr_accessor :description
73
+
74
+ def initialize(value, description = nil)
75
+ @value = value
76
+ @description = description
77
+ end
78
+ end
79
+
80
+ class Subject < SomeValue; end
81
+ class Message < SomeValue; end
82
+
83
+ # Carries expression context for evaluation
84
+ class Context
85
+ attr_accessor :subject, :message, :arguments
86
+
87
+ def initialize(subject: nil, message: nil, arguments: nil)
88
+ @subject = subject
89
+ @message = message
90
+ @arguments = arguments
91
+ end
92
+
93
+ def merge(context)
94
+ Context.new(to_h.merge(context.to_h))
95
+ end
96
+
97
+ def to_h
98
+ output = {}
99
+ %i[subject message arguments].each do |val|
100
+ output[val] = send(val) if send(val)
101
+ end
102
+ output
103
+ end
104
+
105
+ def evaluate
106
+ obj = subject&.value
107
+ msg = message&.value
108
+ args = arguments&.args
109
+ block = arguments&.block
110
+
111
+ val = get_value(obj, msg, args, block)
112
+
113
+ SomeValue.new(val)
114
+ rescue StandardError => e
115
+ SomeValue.new(e)
116
+ end
117
+
118
+ def get_value(obj, msg, args, block)
119
+ if !msg
120
+ obj
121
+ elsif !args
122
+ obj&.public_send(msg) || send(msg)
123
+ elsif !block
124
+ obj&.public_send(msg, *args) || send(msg, *args)
125
+ else
126
+ obj&.public_send(msg, *args, &block) || send(msg, *args, &block)
127
+ end
128
+ end
129
+ end
130
+ end
data/lib/speq/version.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Speq
2
- VERSION = '0.3.0'.freeze
4
+ VERSION = '0.4.0'
3
5
  end
data/lib/speq.rb CHANGED
@@ -1,44 +1,165 @@
1
- require 'speq/version'
2
- require 'speq/test'
3
- require 'speq/matcher'
4
- require 'speq/report'
5
- require 'speq/unit'
6
- require 'speq/fake'
7
- require 'speq/action'
8
- require 'speq/cli'
1
+ # frozen_string_literal: true
9
2
 
3
+ require 'speq/recruit'
4
+ require 'speq/values'
5
+ require 'speq/string_fmt'
6
+ require 'speq/question'
7
+
8
+ def speq(description, &block)
9
+ Speq::Test.new(description, &block)
10
+ end
11
+
12
+ # Build specs with fewer words
10
13
  module Speq
11
- @tests = [Test.new]
12
- @descriptions = { Object => nil }
14
+ # Test code executes within the context of this class
15
+ class TestBlock
16
+ METHODS = %i[speq on does with of is].freeze
13
17
 
14
- def self.test(&block)
15
- self << Test.new
16
- module_exec(&block)
17
- end
18
+ def initialize(parent, &block)
19
+ @outer_scope = eval('self', block.binding)
20
+ @parent = parent
21
+ instance_eval(&block)
22
+ end
18
23
 
19
- def self.<<(test)
20
- @tests << test
21
- end
24
+ def speq(description, &block)
25
+ @parent.speq(description, &block)
26
+ end
22
27
 
23
- def self.descriptions
24
- @descriptions
28
+ def method_missing(method_name, *args, &block)
29
+ if METHODS.include?(method_name) ||
30
+ @parent.context && Question.question?(method_name)
31
+ Expression.new(@parent).send(method_name, *args, &block)
32
+ elsif @outer_scope.respond_to?(method_name)
33
+ @outer_scope.send(method_name, *args, &block)
34
+ else
35
+ super
36
+ end
37
+ end
38
+
39
+ def respond_to_missing?(method_name, *)
40
+ METHODS.include?(method_name) ||
41
+ @outer_scope.respond_to?(method_name) ||
42
+ super
43
+ end
25
44
  end
26
45
 
27
- module_function
46
+ # Common interface for all for Test and Expression
47
+ class Group
48
+ attr_reader :units, :context, :parent
28
49
 
29
- def method_missing(method_name, *args, &block)
30
- if Action.instance_methods.include?(method_name)
31
- Action.new(@tests.last).send(method_name, *args, &block)
32
- else
33
- super
50
+ def initialize(parent = nil)
51
+ @parent = parent
52
+ @context = Context.new
53
+ @units = []
54
+ end
55
+
56
+ def outcome
57
+ Outcome.aggregate(units.map(&:outcome))
58
+ end
59
+
60
+ def pass?
61
+ outcome.pass?
62
+ end
63
+
64
+ def fail?
65
+ outcome.fail?
66
+ end
67
+
68
+ def error?
69
+ outcome.error?
70
+ end
71
+
72
+ def <<(unit)
73
+ units << unit
74
+ end
75
+
76
+ def full_context
77
+ parent ? context.merge(parent.full_context) : context
34
78
  end
35
79
  end
36
80
 
37
- def report
38
- Report.new(@tests).print_report
81
+ # A test group is a collection of tests with a description
82
+ class Test < Group
83
+ attr_reader :description
84
+
85
+ def initialize(description, parent = nil)
86
+ super(parent)
87
+ @description = description
88
+ run(&proc) if block_given?
89
+ end
90
+
91
+ def run(&block)
92
+ TestBlock.new(self, &block)
93
+ end
94
+
95
+ def speq(description, &block)
96
+ self << Test.new(description, self, &block)
97
+ end
39
98
  end
40
99
 
41
- def fake(**mapping)
42
- Fake.new(mapping)
100
+ # An expression group carries context for evaluation and associated questions
101
+ class Expression < Group
102
+ attr_reader :result
103
+
104
+ def initialize(parent)
105
+ super(parent)
106
+ @result = nil
107
+ end
108
+
109
+ def result=(val)
110
+ @result = val unless result
111
+ end
112
+
113
+ def evaluate_result
114
+ self.result = full_context.evaluate
115
+ end
116
+
117
+ def speq(description = "#{context}...", &block)
118
+ parent << Test.new(description, self, &block)
119
+ end
120
+
121
+ def on(val, description = nil)
122
+ context.subject = Subject.new(val, description)
123
+ speq(&proc) if block_given?
124
+ self
125
+ end
126
+
127
+ def does(val, description = nil)
128
+ context.message = Message.new(val, description)
129
+ speq(&proc) if block_given?
130
+ self
131
+ end
132
+
133
+ def with(*args, &block)
134
+ context.arguments = Arguments.new(*args, &block)
135
+ self
136
+ end
137
+
138
+ alias of with
139
+
140
+ def is(thing, description = nil, &block)
141
+ if thing.is_a?(Symbol)
142
+ does(thing, description, &block)
143
+ else
144
+ on(thing, description, &block)
145
+ end
146
+ end
147
+
148
+ def method_missing(method_name, *args, &block)
149
+ if Question.question?(method_name)
150
+ if result.nil?
151
+ parent << self
152
+ evaluate_result
153
+ end
154
+ self << Question.for(result.value, method_name, *args, &block)
155
+ self
156
+ else
157
+ super
158
+ end
159
+ end
160
+
161
+ def respond_to_missing?(method_name, *)
162
+ Question.question?(method_name) || super
163
+ end
43
164
  end
44
165
  end
data/speq.gemspec CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  lib = File.expand_path('lib', __dir__)
2
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
5
  require 'speq/version'
@@ -13,7 +15,9 @@ Gem::Specification.new do |spec|
13
15
  spec.license = 'MIT'
14
16
 
15
17
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
16
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ `git ls-files -z`.split("\x0").reject do |f|
19
+ f.match(%r{^(test|spec|features)/})
20
+ end
17
21
  end
18
22
 
19
23
  spec.bindir = 'exe'
@@ -21,8 +25,8 @@ Gem::Specification.new do |spec|
21
25
  spec.require_paths = ['lib']
22
26
 
23
27
  spec.add_development_dependency 'bundler', '~> 1.16'
24
- spec.add_development_dependency 'rake', '~> 10.0'
25
28
  spec.add_development_dependency 'pry', '~> 0.11.3'
29
+ spec.add_development_dependency 'rake', '~> 10.0'
26
30
 
27
31
  spec.add_dependency 'colorize', '~> 0.8.1'
28
32
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: speq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - zaniar moradian
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-11-28 00:00:00.000000000 Z
11
+ date: 2019-12-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -25,33 +25,33 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.16'
27
27
  - !ruby/object:Gem::Dependency
28
- name: rake
28
+ name: pry
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
33
+ version: 0.11.3
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '10.0'
40
+ version: 0.11.3
41
41
  - !ruby/object:Gem::Dependency
42
- name: pry
42
+ name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 0.11.3
47
+ version: '10.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 0.11.3
54
+ version: '10.0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: colorize
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -75,22 +75,20 @@ extensions: []
75
75
  extra_rdoc_files: []
76
76
  files:
77
77
  - ".gitignore"
78
+ - ".ruby-version"
78
79
  - ".travis.yml"
79
80
  - Gemfile
80
- - Gemfile.lock
81
81
  - README.md
82
82
  - Rakefile
83
83
  - bin/console
84
84
  - bin/setup
85
85
  - exe/speq
86
86
  - lib/speq.rb
87
- - lib/speq/action.rb
88
87
  - lib/speq/cli.rb
89
- - lib/speq/fake.rb
90
- - lib/speq/matcher.rb
91
- - lib/speq/report.rb
92
- - lib/speq/test.rb
93
- - lib/speq/unit.rb
88
+ - lib/speq/question.rb
89
+ - lib/speq/recruit.rb
90
+ - lib/speq/string_fmt.rb
91
+ - lib/speq/values.rb
94
92
  - lib/speq/version.rb
95
93
  - speq.gemspec
96
94
  homepage: https://github.com/znrm/speq
@@ -112,8 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
112
110
  - !ruby/object:Gem::Version
113
111
  version: '0'
114
112
  requirements: []
115
- rubyforge_project:
116
- rubygems_version: 2.7.6
113
+ rubygems_version: 3.0.3
117
114
  signing_key:
118
115
  specification_version: 4
119
116
  summary: A tiny library to build specs with fewer words.
data/Gemfile.lock DELETED
@@ -1,28 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- speq (0.3.0)
5
- colorize (~> 0.8.1)
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- coderay (1.1.2)
11
- colorize (0.8.1)
12
- method_source (0.9.0)
13
- pry (0.11.3)
14
- coderay (~> 1.1.0)
15
- method_source (~> 0.9.0)
16
- rake (10.5.0)
17
-
18
- PLATFORMS
19
- ruby
20
-
21
- DEPENDENCIES
22
- bundler (~> 1.16)
23
- pry (~> 0.11.3)
24
- rake (~> 10.0)
25
- speq!
26
-
27
- BUNDLED WITH
28
- 1.16.6