fix 1.0.0.beta12 → 1.0.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.
data/lib/fix/spec.rb ADDED
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "aw"
4
+ require "defi"
5
+
6
+ require_relative "dsl"
7
+ require_relative "result"
8
+
9
+ module Fix
10
+ # The lib directories of fix and of the gems it runs, each found by a
11
+ # method or a module its root file defines, not by the name of a file,
12
+ # which another file could bear: their frames are left out of a backtrace
13
+ # printed by {Spec#test}.
14
+ LIBRARIES = ([
15
+ ::Aw.method(:fork!).source_location,
16
+ ::Object.const_source_location("Defi"),
17
+ ::Object.const_source_location("Matchi"),
18
+ ::Spectus.method(:must).source_location
19
+ ].map { |location| "#{::File.dirname(location.fetch(0))}/" } << "#{::File.expand_path("..", __dir__)}/").freeze
20
+ private_constant :LIBRARIES
21
+
22
+ # A specification: its name and its contexts, built once by {Fix.spec} and
23
+ # never changed. Tested against a subject with {#run} or {#test}, it is
24
+ # also a matcher: {#match?} tests a value, as `it MUST fix(:Name)` does.
25
+ #
26
+ # @!attribute [r] name
27
+ # @return [Symbol, nil] the name, nil for an anonymous specification
28
+ # @!attribute [r] nodes
29
+ # @return [Array<Fix::Node>] the contexts, each before its children
30
+ # @api private
31
+ Spec = ::Data.define(:name, :nodes) do
32
+ # Tests a subject against every expectation, each context in a child
33
+ # process, and gives the results. The block builds the subject: it runs
34
+ # once per expectation, where the lets live.
35
+ #
36
+ # @param fork [Boolean] whether each context runs in a child process;
37
+ # `false` for a debugger only
38
+ # @yieldreturn [Object] the subject
39
+ # @return [Array<Fix::Result>] in the order of the contexts, then of
40
+ # their expectations
41
+ # @raise [ArgumentError] without a block
42
+ # @raise [NotImplementedError] when this Ruby cannot fork
43
+ # @raise [Aw::ChildError] when a child died or delivered no result
44
+ # @raise [TypeError] when the `to_s` of a matcher is not a String; what a
45
+ # child raised outside an expectation is raised here again
46
+ def run(fork: true, &subject)
47
+ raise ::ArgumentError, "a block must be given" unless subject
48
+
49
+ nodes.flat_map { |node| results(node, fork, subject) }
50
+ end
51
+
52
+ # Like {#run}, writing each result on the standard output as its
53
+ # context ends, one line each, with the backtrace of an error below.
54
+ #
55
+ # @param fork [Boolean] as for {#run}
56
+ # @yieldreturn [Object] the subject
57
+ # @return [Boolean] whether every expectation is met
58
+ # @raise (see #run)
59
+ def test(fork: true, &subject) # rubocop:disable Naming/PredicateMethod
60
+ raise ::ArgumentError, "a block must be given" unless subject
61
+
62
+ nodes.map do |node|
63
+ results(node, fork, subject).each { |result| $stdout.puts(lines(result)) }.all?(&:passed?)
64
+ end.all?
65
+ end
66
+
67
+ # Tests a value, as a matcher does: the block is called once, and its
68
+ # value is the subject of every expectation.
69
+ #
70
+ # @yieldreturn [Object] the value
71
+ # @return [Boolean] whether every expectation is met
72
+ # @raise (see #run)
73
+ def match?
74
+ raise ::ArgumentError, "a block must be given" unless block_given?
75
+
76
+ value = yield
77
+ run { value }.all?(&:passed?)
78
+ end
79
+
80
+ # @return [String] `fix NAME`, or `fix an anonymous specification`
81
+ def to_s
82
+ "fix #{name || "an anonymous specification"}"
83
+ end
84
+
85
+ alias_method :inspect, :to_s
86
+
87
+ private
88
+
89
+ # The results of a context, from a child process or not.
90
+ def results(node, fork, subject)
91
+ return [] if node.expectations.empty?
92
+
93
+ work = -> { examine(node, subject) }
94
+ spectus = fork ? ::Aw.fork!(&work) : work.call
95
+ node.expectations.zip(spectus).map do |expectation, result|
96
+ Result.new(location: expectation.location, chain: node.chain, result:)
97
+ end
98
+ end
99
+
100
+ # The results of the expectations of a context, in the process that works
101
+ # on them. A process the subject forked and let run on, instead of ending
102
+ # it with exit!, is not that one: it leaves here without a word.
103
+ def examine(node, subject)
104
+ pid = ::Process.pid
105
+ node.expectations.map do |expectation|
106
+ result = judge(node, expectation, subject)
107
+ ::Process.exit! unless ::Process.pid == pid
108
+
109
+ result
110
+ end
111
+ end
112
+
113
+ # One expectation, in a new environment: the subject is built, the
114
+ # chain is sent, and the requirement judges what came out. A method of
115
+ # the chain missing on its receiver makes a MAY an info, as a method
116
+ # missing on the actual value does for Spectus, when the error that
117
+ # came out is a NoMethodError by its class name: an exception of a
118
+ # subclass, which Ruby does not raise for a missing method, stays an
119
+ # error.
120
+ def judge(node, expectation, subject)
121
+ environment = Environment.new(node.lets)
122
+ missing = false
123
+ result = expectation.requirement.call do
124
+ value, missing = actual(node, environment, subject)
125
+ value.call
126
+ end
127
+ omission = missing && expectation.requirement.level.equal?(:MAY) && result.error_class == "NoMethodError"
128
+ omission ? result.with(outcome: :info) : result
129
+ end
130
+
131
+ # The subject through the chain: a value, and whether the chain stopped
132
+ # on a method missing on its receiver.
133
+ def actual(node, environment, subject)
134
+ object = environment.instance_exec(&subject)
135
+ node.challenges.each do |challenge|
136
+ value = challenge.to(object)
137
+ return [value, missing?(value.object, object, challenge)] if value.raised?
138
+
139
+ object = value.object
140
+ end
141
+ [::Defi::Value.new(object, false), false]
142
+ end
143
+
144
+ # Whether an exception is a NoMethodError for the method of the
145
+ # challenge on the very object it was sent to, read as NameError and
146
+ # BasicObject do, whatever the objects say. A NoMethodError built by
147
+ # hand without a receiver raises ArgumentError when asked for it: it
148
+ # names no object, so the method is not missing on one.
149
+ def missing?(exception, object, challenge)
150
+ return false unless ::NoMethodError === exception
151
+
152
+ receiver = ::NameError.instance_method(:receiver).bind_call(exception)
153
+ ::BasicObject.instance_method(:equal?).bind_call(receiver, object) &&
154
+ challenge.name.equal?(::NameError.instance_method(:name).bind_call(exception))
155
+ rescue ::ArgumentError
156
+ false
157
+ end
158
+
159
+ # The line of a result, then the frames of an error that are not ours,
160
+ # each on one line too.
161
+ def lines(result)
162
+ frames = result.outcome == :error ? (result.result.backtrace || []) : []
163
+ frames = frames.reject { |frame| frame.start_with?(*LIBRARIES) }
164
+ [result.to_s] + frames.map { |frame| " #{frame.gsub(/\R/) { "\\n" }}" }
165
+ end
166
+ end
167
+ end
data/lib/fix.rb CHANGED
@@ -1,22 +1,52 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative File.join("fix", "set")
4
-
3
+ require_relative "fix/spec"
5
4
  require_relative "kernel"
6
5
 
7
- # Namespace for the Fix framework.
6
+ # Specifications with the MUST, SHOULD and MAY of RFC 2119. A specification
7
+ # says what a value must, should or may do, in a document free of logic;
8
+ # any subject is then tested against it, each context in a process of its
9
+ # own.
10
+ #
11
+ # @example
12
+ # Fix :Duck do
13
+ # on(:swims) { it MUST eq "Swoosh..." }
14
+ # on(:sings) { it MAY eq "♪... ♫..." }
15
+ # end
8
16
  #
9
- # @api public
17
+ # Fix[:Duck].test { Duck.new } # => true
10
18
  module Fix
11
- # Test a built specification.
12
- #
13
- # @example Run _Answer_ specification against `42`.
14
- # Fix[:Answer].test { 42 }
15
- #
16
- # @param name [String, Symbol] The constant name of the specifications.
19
+ @specs = {}.freeze
20
+
21
+ # Builds a specification, and registers it under its name once it is
22
+ # built: a block that raises registers nothing.
17
23
  #
18
- # @return [::Fix::Test] The specification document.
24
+ # @param name [Symbol, nil] the name, or nil for an anonymous specification
25
+ # @yield the document, written with {Fix::Dsl}
26
+ # @return [Fix::Spec]
27
+ # @raise [ArgumentError] without a block, with a name that is not a Symbol
28
+ # or that is already registered, or for a mistake in a word of the
29
+ # document
30
+ # @raise [NameError] for a let named in an expectation
31
+ # @raise [KeyError] for a `fix(:Name)` not registered yet
32
+ def self.spec(name = nil, &block)
33
+ raise ::ArgumentError, "a block must be given" unless block
34
+ raise ::ArgumentError, "the name must be a Symbol" unless name.nil? || ::Symbol === name
35
+
36
+ spec = Spec.new(name:, nodes: Dsl.build(&block))
37
+ return spec if name.nil?
38
+ raise ::ArgumentError, "#{name.inspect} is already a specification" if @specs.key?(name)
39
+
40
+ @specs = @specs.merge(name => spec).freeze
41
+ spec
42
+ end
43
+
44
+ # @param name [Symbol] the name of a registered specification
45
+ # @return [Fix::Spec]
46
+ # @raise [KeyError] when no specification has that name
19
47
  def self.[](name)
20
- ::Fix::Set.load(name)
48
+ @specs.fetch(name) do
49
+ raise ::KeyError.new("no specification named #{name.inspect}", receiver: @specs, key: name)
50
+ end
21
51
  end
22
52
  end
data/lib/kernel.rb CHANGED
@@ -1,38 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative File.join("fix", "doc")
4
- require_relative File.join("fix", "dsl")
5
- require_relative File.join("fix", "set")
6
-
7
- # The Kernel module.
3
+ # The `Fix` method, private: callable at the top level of a file, not on an
4
+ # object.
8
5
  module Kernel
9
- # rubocop:disable Naming/MethodName
6
+ private
10
7
 
11
- # Specifications are built with this method.
12
- #
13
- # @example Require an answer equal to 42.
14
- # # The spec
15
- # Fix :Answer do
16
- # it MUST equal 42
17
- # end
18
- #
19
- # # A test
20
- # Fix[:Answer].test { 42 }
8
+ # Builds a specification: `Fix :Name do … end`, or `Fix do … end` for an
9
+ # anonymous one. See {Fix.spec}.
21
10
  #
22
- # @param name [String, Symbol] The constant name of the specifications.
23
- # @yield The specifications block that defines the test requirements
24
- # @yieldreturn [void]
25
- #
26
- # @return [#test] The collection of specifications.
27
- #
28
- # @api public
29
- def Fix(name = nil, &)
30
- klass = ::Class.new(::Fix::Dsl)
31
- klass.const_set(:CONTEXTS, [klass])
32
- klass.instance_eval(&)
33
- ::Fix::Doc.const_set(name, klass) unless name.nil?
34
- ::Fix::Set.new(*klass.const_get(:CONTEXTS))
11
+ # @param name [Symbol, nil] the name
12
+ # @yield the document, written with {Fix::Dsl}
13
+ # @return [Fix::Spec]
14
+ def Fix(name = nil, &) # rubocop:disable Naming/MethodName
15
+ ::Fix.spec(name, &)
35
16
  end
36
-
37
- # rubocop:enable Naming/MethodName
38
17
  end
metadata CHANGED
@@ -1,57 +1,73 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fix
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.beta12
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2024-12-31 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: aw
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.0'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: defi
14
28
  requirement: !ruby/object:Gem::Requirement
15
29
  requirements:
16
30
  - - "~>"
17
31
  - !ruby/object:Gem::Version
18
- version: 3.0.0
32
+ version: '4.0'
19
33
  type: :runtime
20
34
  prerelease: false
21
35
  version_requirements: !ruby/object:Gem::Requirement
22
36
  requirements:
23
37
  - - "~>"
24
38
  - !ruby/object:Gem::Version
25
- version: 3.0.0
39
+ version: '4.0'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: matchi
28
42
  requirement: !ruby/object:Gem::Requirement
29
43
  requirements:
30
44
  - - "~>"
31
45
  - !ruby/object:Gem::Version
32
- version: 4.1.0
46
+ version: '6.0'
33
47
  type: :runtime
34
48
  prerelease: false
35
49
  version_requirements: !ruby/object:Gem::Requirement
36
50
  requirements:
37
51
  - - "~>"
38
52
  - !ruby/object:Gem::Version
39
- version: 4.1.0
53
+ version: '6.0'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: spectus
42
56
  requirement: !ruby/object:Gem::Requirement
43
57
  requirements:
44
58
  - - "~>"
45
59
  - !ruby/object:Gem::Version
46
- version: 5.0.1
60
+ version: '6.0'
47
61
  type: :runtime
48
62
  prerelease: false
49
63
  version_requirements: !ruby/object:Gem::Requirement
50
64
  requirements:
51
65
  - - "~>"
52
66
  - !ruby/object:Gem::Version
53
- version: 5.0.1
54
- description: Specing framework.
67
+ version: '6.0'
68
+ description: A specification says what a subject MUST, SHOULD or MAY do, and nothing
69
+ about how it is made. Any subject is then tested against it, each context in a child
70
+ process, and every expectation gets a result.
55
71
  email: contact@cyril.email
56
72
  executables: []
57
73
  extensions: []
@@ -60,22 +76,22 @@ files:
60
76
  - LICENSE.md
61
77
  - README.md
62
78
  - lib/fix.rb
63
- - lib/fix/doc.rb
64
79
  - lib/fix/dsl.rb
80
+ - lib/fix/environment.rb
81
+ - lib/fix/level.rb
65
82
  - lib/fix/matcher.rb
66
- - lib/fix/requirement.rb
67
- - lib/fix/run.rb
68
- - lib/fix/set.rb
83
+ - lib/fix/node.rb
84
+ - lib/fix/result.rb
85
+ - lib/fix/spec.rb
69
86
  - lib/kernel.rb
70
- homepage: https://fixrb.dev/
87
+ homepage: https://fixrb.dev/gems/fix/
71
88
  licenses:
72
89
  - MIT
73
90
  metadata:
74
91
  bug_tracker_uri: https://github.com/fixrb/fix/issues
75
92
  documentation_uri: https://rubydoc.info/gems/fix
76
- source_code_uri: https://github.com/fixrb/fix
77
- wiki_uri: https://github.com/fixrb/fix/wiki
78
93
  rubygems_mfa_required: 'true'
94
+ source_code_uri: https://github.com/fixrb/fix
79
95
  rdoc_options: []
80
96
  require_paths:
81
97
  - lib
@@ -83,14 +99,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
83
99
  requirements:
84
100
  - - ">="
85
101
  - !ruby/object:Gem::Version
86
- version: 3.2.0
102
+ version: 3.3.0
87
103
  required_rubygems_version: !ruby/object:Gem::Requirement
88
104
  requirements:
89
105
  - - ">="
90
106
  - !ruby/object:Gem::Version
91
107
  version: '0'
92
108
  requirements: []
93
- rubygems_version: 3.6.2
109
+ rubygems_version: 4.0.23
94
110
  specification_version: 4
95
- summary: Specing framework.
111
+ summary: Specifications in the words of RFC 2119, tested against a subject
96
112
  test_files: []
data/lib/fix/doc.rb DELETED
@@ -1,24 +0,0 @@
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
@@ -1,87 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "spectus/requirement/optional"
4
- require "spectus/requirement/recommended"
5
- require "spectus/requirement/required"
6
-
7
- module Fix
8
- # Collection of expectation matchers.
9
- #
10
- # @api private
11
- module Requirement
12
- # rubocop:disable Naming/MethodName
13
-
14
- # This method mean that the definition is an absolute requirement of the
15
- # specification.
16
- #
17
- # @param matcher [#match?] The matcher.
18
- #
19
- # @return [Requirement::Required] An absolute requirement level instance.
20
- #
21
- # @api public
22
- def MUST(matcher)
23
- ::Spectus::Requirement::Required.new(negate: false, matcher:)
24
- end
25
-
26
- # This method mean that the definition is an absolute prohibition of the specification.
27
- #
28
- # @param matcher [#match?] The matcher.
29
- #
30
- # @return [Requirement::Required] An absolute prohibition level instance.
31
- #
32
- # @api public
33
- def MUST_NOT(matcher)
34
- ::Spectus::Requirement::Required.new(negate: true, matcher:)
35
- end
36
-
37
- # This method mean that there may exist valid reasons in particular
38
- # circumstances to ignore a particular item, but the full implications must be
39
- # understood and carefully weighed before choosing a different course.
40
- #
41
- # @param matcher [#match?] The matcher.
42
- #
43
- # @return [Requirement::Recommended] A recommended requirement level instance.
44
- #
45
- # @api public
46
- def SHOULD(matcher)
47
- ::Spectus::Requirement::Recommended.new(negate: false, matcher:)
48
- end
49
-
50
- # This method mean that there may exist valid reasons in particular
51
- # circumstances when the particular behavior is acceptable or even useful, but
52
- # the full implications should be understood and the case carefully weighed
53
- # before implementing any behavior described with this label.
54
- #
55
- # @param matcher [#match?] The matcher.
56
- #
57
- # @return [Requirement::Recommended] A not recommended requirement level
58
- # instance.
59
- #
60
- # @api public
61
- def SHOULD_NOT(matcher)
62
- ::Spectus::Requirement::Recommended.new(negate: true, matcher:)
63
- end
64
-
65
- # This method mean that an item is truly optional.
66
- # One vendor may choose to include the item because a particular marketplace
67
- # requires it or because the vendor feels that it enhances the product while
68
- # another vendor may omit the same item. An implementation which does not
69
- # include a particular option must be prepared to interoperate with another
70
- # implementation which does include the option, though perhaps with reduced
71
- # functionality. In the same vein an implementation which does include a
72
- # particular option must be prepared to interoperate with another
73
- # implementation which does not include the option (except, of course, for the
74
- # feature the option provides).
75
- #
76
- # @param matcher [#match?] The matcher.
77
- #
78
- # @return [Requirement::Optional] An optional requirement level instance.
79
- #
80
- # @api public
81
- def MAY(matcher)
82
- ::Spectus::Requirement::Optional.new(negate: false, matcher:)
83
- end
84
-
85
- # rubocop:enable Naming/MethodName
86
- end
87
- end
data/lib/fix/run.rb DELETED
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "expresenter/fail"
4
-
5
- module Fix
6
- # Run class.
7
- #
8
- # @api private
9
- class Run
10
- # @return [::Fix::Dsl] A context instance.
11
- attr_reader :environment
12
-
13
- # @return [::Spectus::Requirement::Base] An expectation.
14
- attr_reader :requirement
15
-
16
- # @return [Array<::Defi::Method>] A list of challenges.
17
- attr_reader :challenges
18
-
19
- # @param environment [::Fix::Dsl] A context instance.
20
- # @param requirement [::Spectus::Requirement::Base] An expectation.
21
- # @param challenges [Array<::Defi::Method>] A list of challenges.
22
- def initialize(environment, requirement, *challenges)
23
- @environment = environment
24
- @requirement = requirement
25
- @challenges = challenges
26
- end
27
-
28
- # Verify if the object checks the condition.
29
- #
30
- # @param subject [Proc] The block of code to be tested.
31
- #
32
- # @raise [::Expresenter::Fail] A failed spec exception.
33
- # @return [::Expresenter::Pass] A passed spec instance.
34
- #
35
- # @see https://github.com/fixrb/expresenter
36
- def test(&subject)
37
- requirement.call { actual_value(&subject) }
38
- rescue ::Expresenter::Fail => e
39
- e
40
- end
41
-
42
- private
43
-
44
- # The test's actual value.
45
- #
46
- # @param subject [Proc] The block of code to be tested.
47
- #
48
- # @return [#object_id] The actual value to be tested.
49
- def actual_value(&subject)
50
- challenges.inject(environment.instance_eval(&subject)) do |obj, challenge|
51
- challenge.to(obj).call
52
- end
53
- end
54
- end
55
- end
data/lib/fix/set.rb DELETED
@@ -1,67 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "English"
4
-
5
- require_relative "doc"
6
- require_relative "run"
7
-
8
- module Fix
9
- # Collection of specifications.
10
- #
11
- # @api private
12
- class Set
13
- # @return [Array] A list of specifications.
14
- attr_reader :specs
15
-
16
- # Load specifications from a constant name.
17
- #
18
- # @param name [String, Symbol] The constant name of the specifications.
19
- # @return [Set] A new Set instance containing the loaded specifications.
20
- #
21
- # @api public
22
- def self.load(name)
23
- new(*Doc.fetch(name))
24
- end
25
-
26
- # Initialize a new Set with given contexts.
27
- #
28
- # @param contexts [Array<::Fix::Dsl>] The list of contexts document.
29
- def initialize(*contexts)
30
- @specs = Doc.specs(*contexts).shuffle
31
- end
32
-
33
- # Run the test suite against the provided subject.
34
- #
35
- # @yield The block of code to be tested
36
- # @yieldreturn [Object] The result of the code being tested
37
- # @return [Boolean] true if all tests pass, exits with false otherwise
38
- # @raise [::SystemExit] The test set failed!
39
- #
40
- # @api public
41
- def test(&)
42
- suite_passed?(&) || ::Kernel.exit(false)
43
- end
44
-
45
- private
46
-
47
- def suite_passed?(&subject)
48
- specs.all? { |spec| run_spec(*spec, &subject) }
49
- end
50
-
51
- def run_spec(env, location, requirement, challenges, &subject)
52
- ::Process.fork { lab(env, location, requirement, challenges, &subject) }
53
- ::Process.wait
54
- $CHILD_STATUS.success?
55
- end
56
-
57
- def lab(env, location, requirement, challenges, &)
58
- result = Run.new(env, requirement, *challenges).test(&)
59
- report!(location, result)
60
- ::Kernel.exit(result.passed?)
61
- end
62
-
63
- def report!(path, result)
64
- puts "#{path} #{result.colored_string}"
65
- end
66
- end
67
- end