r_spec 1.0.0.beta1 → 1.0.0.beta2
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 +4 -4
- data/README.md +32 -19
- data/lib/r_spec.rb +13 -2
- data/lib/r_spec/dsl.rb +47 -13
- data/lib/r_spec/expectation_target.rb +93 -0
- data/lib/r_spec/log.rb +24 -0
- data/lib/r_spec/pending.rb +18 -2
- data/lib/r_spec/test.rb +2 -5
- metadata +4 -5
- data/lib/r_spec/exam.rb +0 -29
- data/lib/r_spec/expect.rb +0 -19
- data/lib/r_spec/requirement.rb +0 -90
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d95780dc86d2b0c2295f22fe18ed889298716d8a817ba4a74fbd1c9c1ec71c03
|
4
|
+
data.tar.gz: a954787c7ad5a50c1d3a49d5b1be21515dc0cad0ecd5f7fa618ee736014add2f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 338b22198f2f09e9ae3a36184027f23f728ebdc7b691e0ef3f05d111bf3d753c829d21388dba52fdc8be0e8ba452deea5cc08d38c848e932d67fb1e448769600
|
7
|
+
data.tar.gz: 6601841bac1ed4603c327539fc5b123b9990cfa483b42504ed18dc8680d9c304636dbff10a05ebdc6f76de6d11ac3190b9ad77453aaeb978f853360f9330e04e
|
data/README.md
CHANGED
@@ -18,10 +18,10 @@ This clone attempts to provide most of RSpec's DSL without magic power, so that
|
|
18
18
|
|
19
19
|
* Less features and an implementation with much less code complexity.
|
20
20
|
* Spec files can also be executed directly with the `ruby` executable.
|
21
|
-
* There cannot be more than one expectation per example.
|
22
21
|
* There is no option to activate monkey-patching.
|
23
22
|
* Does not rely on hacks such as `at_exit` hook to trigger the tests.
|
24
23
|
* Built-in matchers do not trust _actual_ and do not send it any message.
|
24
|
+
* The subject must be explicitly defined, otherwise it is not implemented.
|
25
25
|
|
26
26
|
## Important ⚠️
|
27
27
|
|
@@ -40,7 +40,7 @@ Following [RubyGems naming conventions](https://guides.rubygems.org/name-your-ge
|
|
40
40
|
Add this line to your application's Gemfile:
|
41
41
|
|
42
42
|
```ruby
|
43
|
-
gem "r_spec", ">= 1.0.0.
|
43
|
+
gem "r_spec", ">= 1.0.0.beta2"
|
44
44
|
```
|
45
45
|
|
46
46
|
And then execute:
|
@@ -57,22 +57,32 @@ gem install r_spec --pre
|
|
57
57
|
|
58
58
|
## Usage
|
59
59
|
|
60
|
-
|
60
|
+
Let's test an array:
|
61
61
|
|
62
62
|
```ruby
|
63
|
-
|
63
|
+
# array_spec.rb
|
64
64
|
|
65
|
-
|
65
|
+
require "r_spec"
|
66
66
|
|
67
|
-
RSpec.describe
|
68
|
-
|
69
|
-
|
70
|
-
it { expect(greeting).to eql "Hello, Alice!" }
|
67
|
+
RSpec.describe Array do
|
68
|
+
before do
|
69
|
+
@elements = described_class.new
|
71
70
|
end
|
72
71
|
|
73
|
-
|
74
|
-
|
75
|
-
|
72
|
+
describe "#count" do
|
73
|
+
subject do
|
74
|
+
@elements.count
|
75
|
+
end
|
76
|
+
|
77
|
+
it { is_expected.to be 0 }
|
78
|
+
|
79
|
+
context "when a new element is added" do
|
80
|
+
before do
|
81
|
+
@elements << 1
|
82
|
+
end
|
83
|
+
|
84
|
+
it { is_expected.to be 1 }
|
85
|
+
end
|
76
86
|
end
|
77
87
|
end
|
78
88
|
```
|
@@ -80,22 +90,25 @@ end
|
|
80
90
|
It can be tested in the console with the command:
|
81
91
|
|
82
92
|
```sh
|
83
|
-
ruby
|
93
|
+
ruby array_spec.rb
|
84
94
|
```
|
85
95
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
96
|
+
array_spec.rb:15 Success: expected to be 0.
|
97
|
+
array_spec.rb:22 Success: expected to be 1.
|
98
|
+
|
99
|
+
## Test suite
|
100
|
+
|
101
|
+
__RSpec clone__'s specifications are self-described here: [spec/](https://github.com/cyril/r_spec.rb/blob/main/spec/)
|
90
102
|
|
91
103
|
## Contact
|
92
104
|
|
93
|
-
* Home page: https://r-spec.dev
|
105
|
+
* Home page: https://r-spec.dev
|
94
106
|
* Source code: https://github.com/cyril/r_spec.rb
|
107
|
+
* Twitter: https://twitter.com/cyri_
|
95
108
|
|
96
109
|
## Versioning
|
97
110
|
|
98
|
-
|
111
|
+
__RSpec clone__ follows [Semantic Versioning 2.0](https://semver.org/).
|
99
112
|
|
100
113
|
## License
|
101
114
|
|
data/lib/r_spec.rb
CHANGED
@@ -1,11 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
3
|
+
# Top level namespace for the RSpec clone.
|
4
4
|
#
|
5
|
-
# @
|
5
|
+
# @example
|
6
|
+
# require "r_spec"
|
7
|
+
#
|
8
|
+
# RSpec.describe Integer do
|
9
|
+
# it { expect(41.next).to be 42 }
|
10
|
+
# end
|
6
11
|
#
|
12
|
+
# @api public
|
7
13
|
module RSpec
|
8
14
|
# Specs are built with this method.
|
15
|
+
#
|
16
|
+
# @param const [Module] A module to include in block context.
|
17
|
+
# @param block [Proc] The block to define the specs.
|
18
|
+
#
|
19
|
+
# @api public
|
9
20
|
def self.describe(const, &block)
|
10
21
|
raise ::TypeError, const.class.inspect unless const.is_a?(::Module)
|
11
22
|
|
data/lib/r_spec/dsl.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "expresenter"
|
4
3
|
require "matchi/rspec"
|
5
4
|
require "securerandom"
|
6
5
|
|
7
6
|
module RSpec
|
7
|
+
# Abstract class for handling the domain-specific language.
|
8
8
|
class DSL
|
9
|
+
# @param block [Proc] The content to execute at the class initialization.
|
9
10
|
def self.before(&block)
|
10
11
|
define_method(:initialize) do |*args, **kwargs|
|
11
12
|
super()
|
@@ -13,14 +14,22 @@ module RSpec
|
|
13
14
|
end
|
14
15
|
end
|
15
16
|
|
17
|
+
# @param block [Proc] The content of the method to define.
|
18
|
+
# @return [Symbol] A protected method that define the block content.
|
16
19
|
def self.let(name, &block)
|
17
20
|
protected define_method(name.to_sym, &block)
|
18
21
|
end
|
19
22
|
|
23
|
+
# @param block [Proc] The subject to set.
|
24
|
+
# @return [Symbol] A `subject` method that define the block content.
|
20
25
|
def self.subject(&block)
|
21
26
|
let(__method__, &block)
|
22
27
|
end
|
23
28
|
|
29
|
+
# Describe a set of expectations.
|
30
|
+
#
|
31
|
+
# @param const [Module, #object_id] A module to include in block context.
|
32
|
+
# @param block [Proc] The block to define the specs.
|
24
33
|
def self.describe(const, &block)
|
25
34
|
desc = Test.const_set("Test#{random_str}", ::Class.new(self))
|
26
35
|
|
@@ -35,46 +44,71 @@ module RSpec
|
|
35
44
|
|
36
45
|
singleton_class.send(:alias_method, :context, :describe)
|
37
46
|
|
38
|
-
|
39
|
-
|
47
|
+
# Evaluate an expectation.
|
48
|
+
#
|
49
|
+
# @param block [Proc] An expectation to evaluate.
|
50
|
+
#
|
51
|
+
# @raise (see ExpectationTarget#result)
|
52
|
+
# @return (see ExpectationTarget#result)
|
53
|
+
def self.it(_name = nil, &block)
|
54
|
+
raise ::ArgumentError, "Missing block" unless block
|
40
55
|
|
41
|
-
|
42
|
-
print "\e[3m#{path_info}\e[23m "
|
56
|
+
puts "\e[37m#{block.source_location.join(':')}\e[0m"
|
43
57
|
|
44
58
|
i = example.new
|
45
59
|
i.instance_eval(&block)
|
46
60
|
end
|
47
61
|
|
62
|
+
# @private
|
63
|
+
#
|
64
|
+
# @return [Class<DSL>] The class of the example to be tested.
|
48
65
|
private_class_method def self.example
|
49
66
|
::Class.new(self) do
|
50
67
|
include ::Matchi::Helper
|
51
68
|
|
52
69
|
private
|
53
70
|
|
71
|
+
# Wraps the target of an expectation with the actual value.
|
72
|
+
#
|
73
|
+
# @param actual [#object_id] The actual value.
|
74
|
+
#
|
75
|
+
# @return [ExpectationTarget] The target of the expectation.
|
54
76
|
def expect(actual)
|
55
|
-
|
56
|
-
undef is_expected
|
57
|
-
|
58
|
-
Expect.new(actual)
|
77
|
+
ExpectationTarget.new(actual)
|
59
78
|
end
|
60
79
|
|
61
|
-
#
|
80
|
+
# Wraps the target of an expectation with the subject as actual value.
|
81
|
+
#
|
82
|
+
# @return [ExpectationTarget] (see #expect)
|
62
83
|
def is_expected
|
63
84
|
expect(subject)
|
64
85
|
end
|
65
|
-
# rubocop:enable Naming/PredicateName
|
66
86
|
|
87
|
+
def log(message)
|
88
|
+
Log.result(message)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Indicate that an example is disabled pending some action.
|
92
|
+
#
|
93
|
+
# @param description [String] The reason why the example is pending.
|
94
|
+
#
|
95
|
+
# @return [nil] Write a message to STDOUT.
|
67
96
|
def pending(description)
|
68
|
-
|
97
|
+
Pending.result(description)
|
69
98
|
end
|
70
99
|
end
|
71
100
|
end
|
72
101
|
|
102
|
+
# @private
|
103
|
+
#
|
104
|
+
# @return [String] A random string.
|
73
105
|
private_class_method def self.random_str
|
74
106
|
::SecureRandom.alphanumeric(5)
|
75
107
|
end
|
76
108
|
end
|
77
109
|
end
|
78
110
|
|
79
|
-
require_relative "
|
111
|
+
require_relative "expectation_target"
|
112
|
+
require_relative "log"
|
113
|
+
require_relative "pending"
|
80
114
|
require_relative "test"
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "expresenter"
|
4
|
+
|
5
|
+
module RSpec
|
6
|
+
# Wraps the target of an expectation.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# expect(something) # => ExpectationTarget wrapping something
|
10
|
+
#
|
11
|
+
# # used with `to`
|
12
|
+
# expect(actual).to be(42)
|
13
|
+
#
|
14
|
+
# # with `not_to`
|
15
|
+
# expect(actual).not_to be(4)
|
16
|
+
#
|
17
|
+
# @note `ExpectationTarget` is not intended to be instantiated directly by
|
18
|
+
# users. Use `expect` instead.
|
19
|
+
class ExpectationTarget
|
20
|
+
# Instantiate a new expectation target.
|
21
|
+
#
|
22
|
+
# @param actual [#object_id] The actual value.
|
23
|
+
#
|
24
|
+
# @api private
|
25
|
+
def initialize(actual)
|
26
|
+
@actual = actual
|
27
|
+
end
|
28
|
+
|
29
|
+
# Runs the given expectation, passing if `matcher` returns true.
|
30
|
+
#
|
31
|
+
# @example _Absolute requirement_ definition
|
32
|
+
# expect("foo".upcase).to eq("foo")
|
33
|
+
#
|
34
|
+
# @param matcher [#matches?] The matcher.
|
35
|
+
#
|
36
|
+
# @raise (see #result)
|
37
|
+
# @return (see #result)
|
38
|
+
def to(matcher)
|
39
|
+
absolute_requirement(matcher: matcher, negate: false)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Runs the given expectation, passing if `matcher` returns false.
|
43
|
+
#
|
44
|
+
# @example _Absolute prohibition_ definition
|
45
|
+
# expect("foo".size).not_to be(4)
|
46
|
+
#
|
47
|
+
# @param (see #to)
|
48
|
+
#
|
49
|
+
# @raise (see #result)
|
50
|
+
# @return (see #result)
|
51
|
+
def not_to(matcher)
|
52
|
+
absolute_requirement(matcher: matcher, negate: true)
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
# @param matcher [#matches?] The matcher.
|
58
|
+
# @param negate [Boolean] Positive or negative assertion?
|
59
|
+
#
|
60
|
+
# @raise (see #result)
|
61
|
+
# @return (see #result)
|
62
|
+
def absolute_requirement(matcher:, negate:)
|
63
|
+
result(
|
64
|
+
matcher: matcher,
|
65
|
+
negate: negate,
|
66
|
+
passed: negate ^ matcher.matches? { @actual }
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
# @param matcher [#matches?] The matcher.
|
71
|
+
# @param negate [Boolean] Positive or negative assertion?
|
72
|
+
# @param passed [Boolean] The result of an expectation.
|
73
|
+
#
|
74
|
+
# @return [nil] Write a message to STDOUT.
|
75
|
+
#
|
76
|
+
# @raise [SystemExit] Terminate execution immediately by calling
|
77
|
+
# `Kernel.exit(false)` with a failure message written to STDERR.
|
78
|
+
def result(matcher:, negate:, passed:)
|
79
|
+
puts " " + ::Expresenter.call(passed).with(
|
80
|
+
actual: @actual,
|
81
|
+
error: nil,
|
82
|
+
expected: matcher.expected,
|
83
|
+
got: passed,
|
84
|
+
negate: negate,
|
85
|
+
valid: passed,
|
86
|
+
matcher: matcher.class.to_sym,
|
87
|
+
level: :MUST
|
88
|
+
).colored_string
|
89
|
+
rescue ::Expresenter::Fail => e
|
90
|
+
abort " #{e.colored_string}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/r_spec/log.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "expresenter"
|
4
|
+
|
5
|
+
module RSpec
|
6
|
+
# Exception for debugging purpose.
|
7
|
+
class Log < ::NoMethodError
|
8
|
+
# @param message [String] A message to log to the console.
|
9
|
+
#
|
10
|
+
# @return [nil] Write a log message to STDOUT.
|
11
|
+
def self.result(message)
|
12
|
+
puts " " + ::Expresenter.call(true).with(
|
13
|
+
actual: nil,
|
14
|
+
error: new(message),
|
15
|
+
expected: 42,
|
16
|
+
got: nil,
|
17
|
+
matcher: :be,
|
18
|
+
negate: false,
|
19
|
+
level: :MAY,
|
20
|
+
valid: false
|
21
|
+
).colored_string
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/r_spec/pending.rb
CHANGED
@@ -1,8 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "expresenter"
|
4
|
+
|
3
5
|
module RSpec
|
4
|
-
#
|
6
|
+
# Exception for pending expectations.
|
5
7
|
class Pending < ::NotImplementedError
|
8
|
+
# @param message [String] The not implemented expectation description.
|
9
|
+
#
|
10
|
+
# @return [nil] Write a pending expectation to STDOUT.
|
11
|
+
def self.result(message)
|
12
|
+
warn " " + ::Expresenter.call(true).with(
|
13
|
+
actual: new(message),
|
14
|
+
error: nil,
|
15
|
+
expected: self,
|
16
|
+
got: false,
|
17
|
+
matcher: :raise_exception,
|
18
|
+
negate: true,
|
19
|
+
level: :SHOULD,
|
20
|
+
valid: false
|
21
|
+
).colored_string
|
22
|
+
end
|
6
23
|
end
|
7
|
-
# rubocop:enable Lint/InheritException
|
8
24
|
end
|
data/lib/r_spec/test.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: r_spec
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.
|
4
|
+
version: 1.0.0.beta2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-06-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: expresenter
|
@@ -174,10 +174,9 @@ files:
|
|
174
174
|
- README.md
|
175
175
|
- lib/r_spec.rb
|
176
176
|
- lib/r_spec/dsl.rb
|
177
|
-
- lib/r_spec/
|
178
|
-
- lib/r_spec/
|
177
|
+
- lib/r_spec/expectation_target.rb
|
178
|
+
- lib/r_spec/log.rb
|
179
179
|
- lib/r_spec/pending.rb
|
180
|
-
- lib/r_spec/requirement.rb
|
181
180
|
- lib/r_spec/test.rb
|
182
181
|
homepage: https://r-spec.dev/
|
183
182
|
licenses:
|
data/lib/r_spec/exam.rb
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module RSpec
|
4
|
-
# This class evaluate the expectation with the given value.
|
5
|
-
class Exam
|
6
|
-
# Execute the untested code from the given value against the matcher.
|
7
|
-
#
|
8
|
-
# @param actual [#object_id] The actual object to test.
|
9
|
-
# @param negate [Boolean] Positive or negative assertion?
|
10
|
-
# @param matcher [#matches?] The matcher.
|
11
|
-
def initialize(actual:, negate:, matcher:)
|
12
|
-
@actual = actual
|
13
|
-
@got = negate ^ matcher.matches? { actual }
|
14
|
-
end
|
15
|
-
# @return [#object_id] The actual value.
|
16
|
-
attr_reader :actual
|
17
|
-
|
18
|
-
# @return [Boolean] Report to the spec requirement level if the
|
19
|
-
# expectation is true or false.
|
20
|
-
attr_reader :got
|
21
|
-
|
22
|
-
# Report to the spec requirement if the test pass or fail.
|
23
|
-
#
|
24
|
-
# @return [Boolean] Report if the test pass or fail?
|
25
|
-
def valid?
|
26
|
-
got
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
data/lib/r_spec/expect.rb
DELETED
@@ -1,19 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module RSpec
|
4
|
-
class Expect
|
5
|
-
def initialize(actual)
|
6
|
-
@actual = actual
|
7
|
-
end
|
8
|
-
|
9
|
-
def to(matcher)
|
10
|
-
Requirement.new(actual: @actual, matcher: matcher, negate: false).call
|
11
|
-
end
|
12
|
-
|
13
|
-
def not_to(matcher)
|
14
|
-
Requirement.new(actual: @actual, matcher: matcher, negate: true).call
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
require_relative "requirement"
|
data/lib/r_spec/requirement.rb
DELETED
@@ -1,90 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "expresenter"
|
4
|
-
|
5
|
-
module RSpec
|
6
|
-
class Requirement
|
7
|
-
def self.pending(description)
|
8
|
-
::Expresenter.call(true).new(
|
9
|
-
actual: nil,
|
10
|
-
error: Pending.new(description),
|
11
|
-
expected: nil,
|
12
|
-
got: nil,
|
13
|
-
matcher: :eql,
|
14
|
-
negate: false,
|
15
|
-
level: :MAY,
|
16
|
-
valid: false
|
17
|
-
)
|
18
|
-
end
|
19
|
-
|
20
|
-
# Initialize the requirement class.
|
21
|
-
#
|
22
|
-
# @param actual [#object_id] The actual object to test.
|
23
|
-
# @param matcher [#matches?] The matcher.
|
24
|
-
# @param negate [Boolean] Positive or negative assertion?
|
25
|
-
def initialize(actual:, matcher:, negate:)
|
26
|
-
@exam = Exam.new(actual: actual, negate: negate, matcher: matcher)
|
27
|
-
@matcher = matcher
|
28
|
-
@negate = negate
|
29
|
-
@result = expectation_result
|
30
|
-
end
|
31
|
-
|
32
|
-
# @return [Exam] The exam.
|
33
|
-
attr_reader :exam
|
34
|
-
|
35
|
-
# @return [#matches?] The matcher that performed a boolean comparison
|
36
|
-
# between the actual value and the expected value.
|
37
|
-
attr_reader :matcher
|
38
|
-
|
39
|
-
# @return [Expresenter::Fail, Expresenter::Pass] The test result.
|
40
|
-
attr_reader :result
|
41
|
-
|
42
|
-
# Evaluate the expectation.
|
43
|
-
#
|
44
|
-
# @return [Boolean] Report if the expectation pass or fail?
|
45
|
-
def pass?
|
46
|
-
exam.valid?
|
47
|
-
end
|
48
|
-
|
49
|
-
# The consequence of the expectation.
|
50
|
-
#
|
51
|
-
# @return [nil] The test passed.
|
52
|
-
# @raise [SystemExit] The test failed.
|
53
|
-
def call
|
54
|
-
if result.passed?
|
55
|
-
puts result.colored_string
|
56
|
-
else
|
57
|
-
abort result.colored_string
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
protected
|
62
|
-
|
63
|
-
# @note The boolean comparison between the actual value and the expected
|
64
|
-
# value can be evaluated to a negative assertion.
|
65
|
-
#
|
66
|
-
# @return [Boolean] Positive or negative assertion?
|
67
|
-
def negate?
|
68
|
-
@negate
|
69
|
-
end
|
70
|
-
|
71
|
-
# The result of the expectation.
|
72
|
-
#
|
73
|
-
# @return [Expresenter::Fail, Expresenter::Pass] The test result.
|
74
|
-
def expectation_result
|
75
|
-
::Expresenter.call(pass?).new(
|
76
|
-
actual: exam.actual,
|
77
|
-
error: nil,
|
78
|
-
expected: matcher.expected,
|
79
|
-
got: exam.got,
|
80
|
-
negate: negate?,
|
81
|
-
valid: exam.valid?,
|
82
|
-
matcher: matcher.class.to_sym,
|
83
|
-
level: :MUST
|
84
|
-
)
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
require_relative "exam"
|
90
|
-
require_relative "pending"
|