r_spec 1.0.0.beta1 → 1.0.0.beta2
Sign up to get free protection for your applications and to get access to all the features.
- 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"
|