fix 1.0.0.beta4 → 1.0.0.beta8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6245525d719965043d4187cbcce446e21c2b946303048159a13cc37161851ada
4
- data.tar.gz: c5b10da14f591df283e832f15f4839b420ecfbcbca4af6ed2974b28c20815d15
3
+ metadata.gz: 9d095777befeb78f0e16a218f3f14c0f8b39955265db3848cd26a0afb0b224f3
4
+ data.tar.gz: d918e8bfd693765450dfb14477f4fe9285642e22a35995446b89acac78662837
5
5
  SHA512:
6
- metadata.gz: 9ba29c713607b3cfe280c7eb82e9eee07adc69b8d0c58c0ab9cf54117befc5a0f35c76c32d93cd0e5c4bf911a5c09786109ecc336d4866ada3e7d79f0296551b
7
- data.tar.gz: 6267c5c89cd4a6cf4a7573bb4866e4855ed10a403cd2cb606fbe9a18f6ff3f3206559e01f8c349f0ca82d6b8a60fed13546dce80744b0967ca9ac3aeda2d381b
6
+ metadata.gz: c063f295f4d133635d70da15659c613018b51931db21d40c3bedb7db9b442fa756e8ed52833fc49333a816749df99c73956d43c237159f7bd6e728ea36e7791d
7
+ data.tar.gz: 1e4317121e610e67d0d396571b86e0a82b07c16122cc0be5c2c7ee5514136d9ec349282b643f06fe6d7f7b33cd04584048cf72ec3d8d88345e93f1135e7d5dee
data/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2014-2020 Cyril Kato
3
+ Copyright (c) 2014-2021 Cyril Kato
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,91 +1,120 @@
1
1
  # Fix
2
2
 
3
- [![Build Status](https://api.travis-ci.org/fixrb/fix.svg?branch=master)][travis]
4
- [![Code Climate](https://codeclimate.com/github/fixrb/fix/badges/gpa.svg)][codeclimate]
5
- [![Gem Version](https://badge.fury.io/rb/fix.svg)][gem]
6
- [![Inline docs](https://inch-ci.org/github/fixrb/fix.svg?branch=master)][inchpages]
7
- [![Documentation](https://img.shields.io/:yard-docs-38c800.svg)][rubydoc]
8
- [![Gitter](https://badges.gitter.im/Join%20Chat.svg)][gitter]
3
+ [![Home](https://img.shields.io/badge/Home-fixrb.dev-00af8b)](https://fixrb.dev/)
4
+ [![Version](https://img.shields.io/github/v/tag/fixrb/fix?label=Version&logo=github)](https://github.com/fixrb/fix/releases)
5
+ [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/fixrb/fix/main)
6
+ [![CI](https://github.com/fixrb/fix/workflows/CI/badge.svg?branch=main)](https://github.com/fixrb/fix/actions?query=workflow%3Aci+branch%3Amain)
7
+ [![RuboCop](https://github.com/fixrb/fix/workflows/RuboCop/badge.svg?branch=main)](https://github.com/fixrb/fix/actions?query=workflow%3Arubocop+branch%3Amain)
8
+ [![License](https://img.shields.io/github/license/fixrb/fix?label=License&logo=github)](https://github.com/fixrb/fix/raw/main/LICENSE.md)
9
9
 
10
- > Specing framework for Ruby.
10
+ ⚠️ This project is still in the experimental phase. May be used at your own risk.
11
+
12
+ ![Fix specing framework for Ruby](https://fixrb.dev/fix.webp)
13
+
14
+ ## Project goals
15
+
16
+ * Extract specs from the tests.
17
+ * Look like English documents.
18
+ * Be minimalist and easy to use.
19
+ * Run tests quickly.
11
20
 
12
21
  ## Installation
13
22
 
14
23
  Add this line to your application's Gemfile:
15
24
 
16
25
  ```ruby
17
- gem 'fix', '>= 1.0.0.beta4'
26
+ gem "fix", ">= 1.0.0.beta8"
18
27
  ```
19
28
 
20
29
  And then execute:
21
30
 
22
- $ bundle
31
+ ```sh
32
+ bundle
33
+ ```
23
34
 
24
35
  Or install it yourself as:
25
36
 
26
- $ gem install fix --pre
37
+ ```sh
38
+ gem install fix --pre
39
+ ```
27
40
 
28
41
  ## Example
29
42
 
30
- Given this app:
43
+ Given these specifications:
31
44
 
32
45
  ```ruby
33
- # examples/duck/app.rb
34
- class Duck
35
- def walks
36
- 'Klop klop!'
46
+ # examples/duck/fix.rb
47
+
48
+ require "fix"
49
+
50
+ Fix :Duck do
51
+ it SHOULD be_an_instance_of :Duck
52
+
53
+ on :swims do
54
+ it MUST be_an_instance_of :String
55
+ it MUST eql "Swoosh..."
37
56
  end
38
57
 
39
- def swims
40
- 'Swoosh...'
58
+ on :speaks do
59
+ it MUST raise_exception NoMethodError
41
60
  end
42
61
 
43
- def quacks
44
- puts 'Quaaaaaack!'
62
+ on :sings do
63
+ it MAY eql "♪... ♫..."
45
64
  end
46
65
  end
47
66
  ```
48
67
 
49
- When you run this:
68
+ When we load this `Duck` application:
50
69
 
51
70
  ```ruby
52
- # examples/duck/fix.rb
53
- require_relative 'app'
54
- require_relative '../../lib/fix'
55
-
56
- @bird = Duck.new
57
-
58
- Fix(@bird) do
59
- on :swims do
60
- it { MUST eql 'Swoosh...' }
71
+ # examples/duck/app.rb
72
+ class Duck
73
+ def walks
74
+ "Klop klop!"
61
75
  end
62
76
 
63
- on :speaks do
64
- it { MUST raise_exception NoMethodError }
77
+ def swims
78
+ "Swoosh..."
65
79
  end
66
80
 
67
- on :sings do
68
- it { MAY eql '♪... ♫...' }
81
+ def quacks
82
+ puts "Quaaaaaack!"
69
83
  end
70
84
  end
71
85
  ```
72
86
 
73
- Then the output should look like this:
87
+ And we run this test:
88
+
89
+ ```ruby
90
+ # examples/duck/test.rb
91
+
92
+ require_relative "app"
93
+ require_relative "fix"
94
+
95
+ Fix[:Duck].against { Duck.new }
96
+ ```
74
97
 
75
98
  ```sh
76
- ruby examples/duck/fix.rb
99
+ ruby examples/duck/test.rb
77
100
  ```
78
101
 
102
+ We should see this output:
103
+
79
104
  ```txt
80
- examples/duck/fix.rb:8: Success: expected to eql "Swoosh...".
81
- examples/duck/fix.rb:12: Success: undefined method `speaks' for #<Duck:0x00007fe3be868ea0>.
82
- examples/duck/fix.rb:16: NoMethodError: undefined method `sings' for #<Duck:0x00007fe3be868ea0>.
105
+ (irb):3 Success: expected #<Duck:0x00007fb2fa208708> to be an instance of Duck.
106
+ (irb):7 Success: expected to eq "Swoosh...".
107
+ (irb):15 NoMethodError: undefined method `sings' for #<Duck:0x00007fb2fd8371d0>.
108
+ (irb):6 Success: expected "Swoosh..." to be an instance of String.
109
+ (irb):11 Success: undefined method `speaks' for #<Duck:0x00007fb2fcc79258>.
83
110
  ```
84
111
 
85
112
  ## Contact
86
113
 
87
- * Home page: https://fixrb.dev/
88
- * Source code: https://github.com/fixrb/fix
114
+ * Home page: [https://fixrb.dev/](https://fixrb.dev/)
115
+ * Source code: [https://github.com/fixrb/fix](https://github.com/fixrb/fix)
116
+ * API Doc: [https://rubydoc.info/gems/fix](https://rubydoc.info/gems/fix)
117
+ * Twitter: [https://twitter.com/fix\_rb](https://twitter.com/fix\_rb)
89
118
 
90
119
  ## Versioning
91
120
 
@@ -93,20 +122,13 @@ __Fix__ follows [Semantic Versioning 2.0](https://semver.org/).
93
122
 
94
123
  ## License
95
124
 
96
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
125
+ The [gem](https://rubygems.org/gems/fix) is available as open source under the terms of the [MIT License](https://github.com/fixrb/fix/raw/main/LICENSE.md).
97
126
 
98
127
  ***
99
128
 
100
129
  <p>
101
130
  This project is sponsored by:<br />
102
131
  <a href="https://sashite.com/"><img
103
- src="https://github.com/fixrb/fix/raw/master/img/sashite.png"
132
+ src="https://github.com/fixrb/fix/raw/main/img/sashite.png"
104
133
  alt="Sashite" /></a>
105
134
  </p>
106
-
107
- [travis]: https://travis-ci.org/fixrb/fix
108
- [codeclimate]: https://codeclimate.com/github/fixrb/fix
109
- [gem]: https://rubygems.org/gems/fix
110
- [inchpages]: https://inch-ci.org/github/fixrb/fix
111
- [rubydoc]: https://rubydoc.info/gems/fix/frames
112
- [gitter]: https://gitter.im/fixrb/fix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge
data/lib/fix.rb CHANGED
@@ -1,25 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative File.join("fix", "set")
4
+
5
+ require_relative "kernel"
6
+
3
7
  # Namespace for the Fix framework.
4
8
  #
9
+ # @api public
5
10
  module Fix
6
- # Specs are built with this method.
11
+ # Test a built specification.
7
12
  #
8
- # @example 42 must be equal to 42
9
- # describe(42) do
10
- # it { MUST equal 42 }
11
- # end
13
+ # @example Run _Answer_ specification against `42`.
14
+ # Fix[:Answer].against(42)
12
15
  #
13
- # @param front_object [BasicObject] The front object.
14
- # @param options [Hash] Some options.
15
- # @param specs [Proc] The set of specs.
16
+ # @param name [String, Symbol] The constant name of the specifications.
16
17
  #
17
- # @raise [SystemExit] The result of the test.
18
- def self.describe(front_object, **lets, &block)
19
- c = Context.new(front_object, ::Defi.send(:itself), **lets)
20
- c.instance_eval(&block)
18
+ # @return [::Fix::Test] The specification document.
19
+ def self.[](name)
20
+ ::Fix::Set.load(name)
21
21
  end
22
22
  end
23
-
24
- require_relative 'kernel'
25
- require_relative File.join('fix', 'context')
data/lib/fix/doc.rb ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fix
4
+ # Module for storing spec documents.
5
+ #
6
+ # @api private
7
+ module Doc
8
+ # @param name [String, Symbol] The constant name of the specifications.
9
+ def self.fetch(name)
10
+ const_get("#{name}::CONTEXTS")
11
+ end
12
+
13
+ # @param contexts [Array<::Fix::Dsl>] The list of contexts document.
14
+ def self.specs(*contexts)
15
+ contexts.flat_map do |context|
16
+ env = context.new
17
+
18
+ env.public_methods(false).map do |public_method|
19
+ [env] + env.public_send(public_method)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
data/lib/fix/dsl.rb ADDED
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "defi"
4
+
5
+ require_relative "matcher"
6
+ require_relative "requirement"
7
+
8
+ module Fix
9
+ # Abstract class for handling the domain-specific language.
10
+ #
11
+ # @api private
12
+ class Dsl
13
+ extend Matcher
14
+ extend Requirement
15
+
16
+ # Sets a user-defined property.
17
+ #
18
+ # @example
19
+ # require "fix"
20
+ #
21
+ # Fix do
22
+ # let(:name) { "Bob" }
23
+ # end
24
+ #
25
+ # @param name [String, Symbol] The name of the property.
26
+ # @param block [Proc] The content of the method to define.
27
+ #
28
+ # @return [Symbol] A private method that define the block content.
29
+ #
30
+ # @api public
31
+ def self.let(name, &block)
32
+ private define_method(name, &block)
33
+ end
34
+
35
+ # Defines an example group with user-defined properties that describes a
36
+ # unit to be tested.
37
+ #
38
+ # @example
39
+ # require "fix"
40
+ #
41
+ # Fix do
42
+ # with :password, "secret" do
43
+ # it MUST be true
44
+ # end
45
+ # end
46
+ #
47
+ # @param kwargs [Hash] The list of propreties.
48
+ # @param block [Proc] The block to define the specs.
49
+ #
50
+ # @api public
51
+ def self.with(**kwargs, &block)
52
+ klass = ::Class.new(self)
53
+ klass.const_get(:CONTEXTS) << klass
54
+ kwargs.each { |name, value| klass.let(name) { value } }
55
+ klass.instance_eval(&block)
56
+ klass
57
+ end
58
+
59
+ # Defines an example group that describes a unit to be tested.
60
+ #
61
+ # @example
62
+ # require "fix"
63
+ #
64
+ # Fix do
65
+ # on :+, 2 do
66
+ # it MUST be 42
67
+ # end
68
+ # end
69
+ #
70
+ # @param method_name [String, Symbol] The method to send to the subject.
71
+ # @param block [Proc] The block to define the specs.
72
+ #
73
+ # @api public
74
+ def self.on(method_name, *args, **kwargs, &block)
75
+ klass = ::Class.new(self)
76
+ klass.const_get(:CONTEXTS) << klass
77
+
78
+ const_set("Child#{block.object_id}", klass)
79
+
80
+ klass.define_singleton_method(:challenges) do
81
+ challenge = ::Defi.send(method_name, *args, **kwargs)
82
+ super() + [challenge]
83
+ end
84
+
85
+ klass.instance_eval(&block)
86
+ klass
87
+ end
88
+
89
+ # Defines a concrete spec definition.
90
+ #
91
+ # @example
92
+ # require "fix"
93
+ #
94
+ # Fix { it MUST be 42 }
95
+ #
96
+ # @api public
97
+ def self.it(requirement)
98
+ location = caller_locations(1, 1).fetch(0)
99
+ location = [location.path, location.lineno].join(":")
100
+
101
+ define_method("test_#{requirement.object_id}") do
102
+ [location, requirement, self.class.challenges]
103
+ end
104
+ end
105
+
106
+ # The list of challenges to be addressed to the object to be tested.
107
+ #
108
+ # @return [Array<Defi::Challenge>] A list of challenges.
109
+ def self.challenges
110
+ []
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "matchi"
4
+
5
+ module Fix
6
+ # Collection of expectation matchers.
7
+ #
8
+ # @api private
9
+ module Matcher
10
+ # Equivalence matcher
11
+ #
12
+ # @example
13
+ # matcher = eq("foo")
14
+ # matcher.matches? { "foo" } # => true
15
+ # matcher.matches? { "bar" } # => false
16
+ #
17
+ # @param expected [#eql?] An expected equivalent object.
18
+ #
19
+ # @return [#matches?] An equivalence matcher.
20
+ #
21
+ # @api public
22
+ def eq(expected)
23
+ ::Matchi::Eq.new(expected)
24
+ end
25
+
26
+ alias eql eq
27
+
28
+ # Identity matcher
29
+ #
30
+ # @example
31
+ # object = "foo"
32
+ # matcher = be(object)
33
+ # matcher.matches? { object } # => true
34
+ # matcher.matches? { "foo" } # => false
35
+ #
36
+ # @param expected [#equal?] The expected identical object.
37
+ #
38
+ # @return [#matches?] An identity matcher.
39
+ #
40
+ # @api public
41
+ def be(expected)
42
+ ::Matchi::Be.new(expected)
43
+ end
44
+
45
+ alias equal be
46
+
47
+ # Comparisons matcher
48
+ #
49
+ # @example
50
+ # matcher = be_within(1).of(41)
51
+ # matcher.matches? { 42 } # => true
52
+ # matcher.matches? { 43 } # => false
53
+ #
54
+ # @param delta [Numeric] A numeric value.
55
+ #
56
+ # @return [#matches?] A comparison matcher.
57
+ #
58
+ # @api public
59
+ def be_within(delta)
60
+ ::Matchi::BeWithin.new(delta)
61
+ end
62
+
63
+ # Regular expressions matcher
64
+ #
65
+ # @example
66
+ # matcher = match(/^foo$/)
67
+ # matcher.matches? { "foo" } # => true
68
+ # matcher.matches? { "bar" } # => false
69
+ #
70
+ # @param expected [#match] A regular expression.
71
+ #
72
+ # @return [#matches?] A regular expression matcher.
73
+ #
74
+ # @api public
75
+ def match(expected)
76
+ ::Matchi::Match.new(expected)
77
+ end
78
+
79
+ # Expecting errors matcher
80
+ #
81
+ # @example
82
+ # matcher = raise_exception(NameError)
83
+ # matcher.matches? { Boom } # => true
84
+ # matcher.matches? { true } # => false
85
+ #
86
+ # @param expected [Exception, #to_s] The expected exception name.
87
+ #
88
+ # @return [#matches?] An error matcher.
89
+ #
90
+ # @api public
91
+ def raise_exception(expected)
92
+ ::Matchi::RaiseException.new(expected)
93
+ end
94
+
95
+ # True matcher
96
+ #
97
+ # @example
98
+ # matcher = be_true
99
+ # matcher.matches? { true } # => true
100
+ # matcher.matches? { false } # => false
101
+ # matcher.matches? { nil } # => false
102
+ # matcher.matches? { 4 } # => false
103
+ #
104
+ # @return [#matches?] A `true` matcher.
105
+ #
106
+ # @api public
107
+ def be_true
108
+ be(true)
109
+ end
110
+
111
+ # False matcher
112
+ #
113
+ # @example
114
+ # matcher = be_false
115
+ # matcher.matches? { false } # => true
116
+ # matcher.matches? { true } # => false
117
+ # matcher.matches? { nil } # => false
118
+ # matcher.matches? { 4 } # => false
119
+ #
120
+ # @return [#matches?] A `false` matcher.
121
+ #
122
+ # @api public
123
+ def be_false
124
+ be(false)
125
+ end
126
+
127
+ # Nil matcher
128
+ #
129
+ # @example
130
+ # matcher = be_nil
131
+ # matcher.matches? { nil } # => true
132
+ # matcher.matches? { false } # => false
133
+ # matcher.matches? { true } # => false
134
+ # matcher.matches? { 4 } # => false
135
+ #
136
+ # @return [#matches?] A `nil` matcher.
137
+ #
138
+ # @api public
139
+ def be_nil
140
+ be(nil)
141
+ end
142
+
143
+ # Type/class matcher
144
+ #
145
+ # @example
146
+ # matcher = be_an_instance_of(String)
147
+ # matcher.matches? { "foo" } # => true
148
+ # matcher.matches? { 4 } # => false
149
+ #
150
+ # @param expected [Class, #to_s] The expected class name.
151
+ #
152
+ # @return [#matches?] A type/class matcher.
153
+ #
154
+ # @api public
155
+ def be_an_instance_of(expected)
156
+ ::Matchi::BeAnInstanceOf.new(expected)
157
+ end
158
+
159
+ # Change matcher
160
+ #
161
+ # @example
162
+ # object = []
163
+ # matcher = change(object, :length).by(1)
164
+ # matcher.matches? { object << 1 } # => true
165
+ #
166
+ # object = []
167
+ # matcher = change(object, :length).by_at_least(1)
168
+ # matcher.matches? { object << 1 } # => true
169
+ #
170
+ # object = []
171
+ # matcher = change(object, :length).by_at_most(1)
172
+ # matcher.matches? { object << 1 } # => true
173
+ #
174
+ # object = "foo"
175
+ # matcher = change(object, :to_s).from("foo").to("FOO")
176
+ # matcher.matches? { object.upcase! } # => true
177
+ #
178
+ # object = "foo"
179
+ # matcher = change(object, :to_s).to("FOO")
180
+ # matcher.matches? { object.upcase! } # => true
181
+ #
182
+ # @param object [#object_id] An object.
183
+ # @param method [Symbol] The name of a method.
184
+ # @param args [Array] A list of arguments.
185
+ # @param kwargs [Hash] A list of keyword arguments.
186
+ #
187
+ # @return [#matches?] A change matcher.
188
+ #
189
+ # @api public
190
+ def change(object, method, *args, **kwargs, &block)
191
+ ::Matchi::Change.new(object, method, *args, **kwargs, &block)
192
+ end
193
+
194
+ # Satisfy matcher
195
+ #
196
+ # @example
197
+ # matcher = satisfy { |value| value == 42 }
198
+ # matcher.matches? { 42 } # => true
199
+ #
200
+ # @param expected [Proc] A block of code.
201
+ #
202
+ # @return [#matches?] A satisfy matcher.
203
+ #
204
+ # @api public
205
+ def satisfy(&expected)
206
+ ::Matchi::Satisfy.new(&expected)
207
+ end
208
+
209
+ private
210
+
211
+ # Predicate matcher, or default method missing behavior.
212
+ #
213
+ # @example Empty predicate matcher
214
+ # matcher = be_empty
215
+ # matcher.matches? { [] } # => true
216
+ # matcher.matches? { [4] } # => false
217
+ def method_missing(name, *args, **kwargs, &block)
218
+ return super unless predicate_matcher_name?(name)
219
+
220
+ ::Matchi::Predicate.new(name, *args, **kwargs, &block)
221
+ end
222
+
223
+ # :nocov:
224
+
225
+ # Hook method to return whether the obj can respond to id method or not.
226
+ def respond_to_missing?(name, include_private = false)
227
+ predicate_matcher_name?(name) || super
228
+ end
229
+
230
+ # :nocov:
231
+
232
+ # Predicate matcher name detector.
233
+ #
234
+ # @param name [Array, Symbol] The name of a potential predicate matcher.
235
+ #
236
+ # @return [Boolean] Indicates if it is a predicate matcher name or not.
237
+ def predicate_matcher_name?(name)
238
+ name.start_with?("be_", "have_") && !name.end_with?("!", "?")
239
+ end
240
+ end
241
+ end