rufus-scheduler 2.0.24 → 3.1.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/CHANGELOG.txt +76 -0
- data/CREDITS.txt +23 -0
- data/LICENSE.txt +1 -1
- data/README.md +1439 -0
- data/Rakefile +1 -5
- data/TODO.txt +149 -55
- data/lib/rufus/{sc → scheduler}/cronline.rb +167 -53
- data/lib/rufus/scheduler/job_array.rb +92 -0
- data/lib/rufus/scheduler/jobs.rb +633 -0
- data/lib/rufus/scheduler/locks.rb +95 -0
- data/lib/rufus/scheduler/util.rb +306 -0
- data/lib/rufus/scheduler/zones.rb +174 -0
- data/lib/rufus/scheduler/zotime.rb +154 -0
- data/lib/rufus/scheduler.rb +608 -27
- data/rufus-scheduler.gemspec +6 -4
- data/spec/basics_spec.rb +54 -0
- data/spec/cronline_spec.rb +479 -152
- data/spec/error_spec.rb +139 -0
- data/spec/job_array_spec.rb +39 -0
- data/spec/job_at_spec.rb +58 -0
- data/spec/job_cron_spec.rb +128 -0
- data/spec/job_every_spec.rb +104 -0
- data/spec/job_in_spec.rb +20 -0
- data/spec/job_interval_spec.rb +68 -0
- data/spec/job_repeat_spec.rb +357 -0
- data/spec/job_spec.rb +498 -109
- data/spec/lock_custom_spec.rb +47 -0
- data/spec/lock_flock_spec.rb +47 -0
- data/spec/lock_lockfile_spec.rb +61 -0
- data/spec/lock_spec.rb +59 -0
- data/spec/parse_spec.rb +263 -0
- data/spec/schedule_at_spec.rb +158 -0
- data/spec/schedule_cron_spec.rb +66 -0
- data/spec/schedule_every_spec.rb +109 -0
- data/spec/schedule_in_spec.rb +80 -0
- data/spec/schedule_interval_spec.rb +128 -0
- data/spec/scheduler_spec.rb +928 -124
- data/spec/spec_helper.rb +126 -0
- data/spec/threads_spec.rb +96 -0
- data/spec/zotime_spec.rb +396 -0
- metadata +56 -33
- data/README.rdoc +0 -661
- data/lib/rufus/otime.rb +0 -3
- data/lib/rufus/sc/jobqueues.rb +0 -160
- data/lib/rufus/sc/jobs.rb +0 -471
- data/lib/rufus/sc/rtime.rb +0 -363
- data/lib/rufus/sc/scheduler.rb +0 -636
- data/lib/rufus/sc/version.rb +0 -32
- data/spec/at_in_spec.rb +0 -47
- data/spec/at_spec.rb +0 -125
- data/spec/blocking_spec.rb +0 -64
- data/spec/cron_spec.rb +0 -134
- data/spec/every_spec.rb +0 -304
- data/spec/exception_spec.rb +0 -113
- data/spec/in_spec.rb +0 -150
- data/spec/mutex_spec.rb +0 -159
- data/spec/rtime_spec.rb +0 -137
- data/spec/schedulable_spec.rb +0 -97
- data/spec/spec_base.rb +0 -87
- data/spec/stress_schedule_unschedule_spec.rb +0 -159
- data/spec/timeout_spec.rb +0 -148
- data/test/kjw.rb +0 -113
- data/test/t.rb +0 -20
data/spec/error_spec.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
|
2
|
+
#
|
3
|
+
# Specifying rufus-scheduler
|
4
|
+
#
|
5
|
+
# Fri Aug 9 07:10:18 JST 2013
|
6
|
+
#
|
7
|
+
|
8
|
+
require 'spec_helper'
|
9
|
+
|
10
|
+
|
11
|
+
describe Rufus::Scheduler do
|
12
|
+
|
13
|
+
before :each do
|
14
|
+
|
15
|
+
@taoe = Thread.abort_on_exception
|
16
|
+
Thread.abort_on_exception = false
|
17
|
+
|
18
|
+
@ose = $stderr
|
19
|
+
$stderr = StringIO.new
|
20
|
+
|
21
|
+
@scheduler = Rufus::Scheduler.new
|
22
|
+
end
|
23
|
+
|
24
|
+
after :each do
|
25
|
+
|
26
|
+
@scheduler.shutdown
|
27
|
+
|
28
|
+
Thread.abort_on_exception = @taoe
|
29
|
+
|
30
|
+
$stderr = @ose
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'error in block' do
|
34
|
+
|
35
|
+
it 'intercepts the error and describes it on $stderr' do
|
36
|
+
|
37
|
+
counter = 0
|
38
|
+
|
39
|
+
@scheduler.every('0.5s') do
|
40
|
+
counter += 1
|
41
|
+
fail 'argh'
|
42
|
+
end
|
43
|
+
|
44
|
+
sleep 2
|
45
|
+
|
46
|
+
expect(counter).to be > 2
|
47
|
+
expect($stderr.string).to match(/argh/)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'error in callable' do
|
52
|
+
|
53
|
+
class MyFailingHandler
|
54
|
+
attr_reader :counter
|
55
|
+
def initialize
|
56
|
+
@counter = 0
|
57
|
+
end
|
58
|
+
def call(job, time)
|
59
|
+
@counter = @counter + 1
|
60
|
+
fail 'ouch'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'intercepts the error and describes it on $stderr' do
|
65
|
+
|
66
|
+
mfh = MyFailingHandler.new
|
67
|
+
|
68
|
+
@scheduler.every('0.5s', mfh)
|
69
|
+
|
70
|
+
sleep 2
|
71
|
+
|
72
|
+
expect(mfh.counter).to be > 2
|
73
|
+
expect($stderr.string).to match(/ouch/)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'Rufus::Scheduler#stderr=' do
|
78
|
+
|
79
|
+
it 'lets divert error information to custom files' do
|
80
|
+
|
81
|
+
@scheduler.stderr = StringIO.new
|
82
|
+
|
83
|
+
@scheduler.in('0s') do
|
84
|
+
fail 'miserably'
|
85
|
+
end
|
86
|
+
|
87
|
+
sleep 0.5
|
88
|
+
|
89
|
+
expect(@scheduler.stderr.string).to match(/intercepted an error/)
|
90
|
+
expect(@scheduler.stderr.string).to match(/miserably/)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
context 'error information' do
|
95
|
+
|
96
|
+
it 'contains information about the error, the job and the scheduler' do
|
97
|
+
|
98
|
+
@scheduler.stderr = StringIO.new
|
99
|
+
|
100
|
+
@scheduler.in('0s') do
|
101
|
+
fail 'miserably'
|
102
|
+
end
|
103
|
+
|
104
|
+
sleep 0.5
|
105
|
+
|
106
|
+
s = @scheduler.stderr.string
|
107
|
+
#puts s
|
108
|
+
|
109
|
+
expect(s).to match(/ENV\['TZ'\]:/)
|
110
|
+
expect(s).to match(/down\?: false/)
|
111
|
+
expect(s).to match(/work_threads: 1/)
|
112
|
+
expect(s).to match(/running_jobs: 1/)
|
113
|
+
expect(s).to match(/uptime: \d/)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context 'Rufus::Scheduler#on_error(&block)' do
|
118
|
+
|
119
|
+
it 'intercepts all StandardError instances' do
|
120
|
+
|
121
|
+
$message = nil
|
122
|
+
|
123
|
+
def @scheduler.on_error(job, err)
|
124
|
+
$message = "#{job.class} #{job.original} #{err.message}"
|
125
|
+
rescue
|
126
|
+
p $!
|
127
|
+
end
|
128
|
+
|
129
|
+
@scheduler.in('0s') do
|
130
|
+
fail 'miserably'
|
131
|
+
end
|
132
|
+
|
133
|
+
sleep 0.5
|
134
|
+
|
135
|
+
expect($message).to eq('Rufus::Scheduler::InJob 0s miserably')
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
#
|
3
|
+
# Specifying rufus-scheduler
|
4
|
+
#
|
5
|
+
# Wed Apr 17 06:00:59 JST 2013
|
6
|
+
#
|
7
|
+
|
8
|
+
require 'spec_helper'
|
9
|
+
|
10
|
+
|
11
|
+
describe Rufus::Scheduler::JobArray do
|
12
|
+
|
13
|
+
class DummyJob < Struct.new(:id, :next_time); end
|
14
|
+
|
15
|
+
before(:each) do
|
16
|
+
@array = Rufus::Scheduler::JobArray.new
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#push' do
|
20
|
+
|
21
|
+
it 'pushes jobs' do
|
22
|
+
|
23
|
+
@array.push(DummyJob.new('a', Time.local(0)))
|
24
|
+
|
25
|
+
expect(@array.to_a.collect(&:id)).to eq(%w[ a ])
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'pushes and remove duplicates' do
|
29
|
+
|
30
|
+
j = DummyJob.new('a', Time.local(0))
|
31
|
+
|
32
|
+
@array.push(j)
|
33
|
+
@array.push(j)
|
34
|
+
|
35
|
+
expect(@array.to_a.collect(&:id)).to eq(%w[ a ])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
data/spec/job_at_spec.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
|
2
|
+
#
|
3
|
+
# Specifying rufus-scheduler
|
4
|
+
#
|
5
|
+
# Wed Apr 17 06:00:59 JST 2013
|
6
|
+
#
|
7
|
+
|
8
|
+
require 'spec_helper'
|
9
|
+
|
10
|
+
|
11
|
+
describe Rufus::Scheduler::AtJob do
|
12
|
+
|
13
|
+
before :each do
|
14
|
+
@scheduler = Rufus::Scheduler.new
|
15
|
+
end
|
16
|
+
after :each do
|
17
|
+
@scheduler.shutdown
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#unschedule' do
|
21
|
+
|
22
|
+
it 'unschedules the job' do
|
23
|
+
|
24
|
+
job = @scheduler.at(Time.now + 3600, :job => true) do
|
25
|
+
end
|
26
|
+
|
27
|
+
job.unschedule
|
28
|
+
|
29
|
+
sleep 0.4
|
30
|
+
|
31
|
+
expect(@scheduler.jobs.size).to eq(0)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe '#scheduled_at' do
|
36
|
+
|
37
|
+
it 'returns the Time at which the job got scheduled' do
|
38
|
+
|
39
|
+
job = @scheduler.schedule_at((t = Time.now) + 3600) {}
|
40
|
+
|
41
|
+
expect(job.scheduled_at.to_i).to be >= t.to_i - 1
|
42
|
+
expect(job.scheduled_at.to_i).to be <= t.to_i + 1
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '#time' do
|
47
|
+
|
48
|
+
it 'returns the time at which the job will trigger' do
|
49
|
+
|
50
|
+
t = Time.now + 3600
|
51
|
+
|
52
|
+
job = @scheduler.schedule_at t do; end
|
53
|
+
|
54
|
+
expect(job.time).to eq(t)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
@@ -0,0 +1,128 @@
|
|
1
|
+
|
2
|
+
#
|
3
|
+
# Specifying rufus-scheduler
|
4
|
+
#
|
5
|
+
# Wed Apr 17 06:00:59 JST 2013
|
6
|
+
#
|
7
|
+
|
8
|
+
require 'spec_helper'
|
9
|
+
|
10
|
+
|
11
|
+
describe Rufus::Scheduler::CronJob do
|
12
|
+
|
13
|
+
before :each do
|
14
|
+
@scheduler = Rufus::Scheduler.new
|
15
|
+
end
|
16
|
+
after :each do
|
17
|
+
@scheduler.shutdown
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'normal' do
|
21
|
+
|
22
|
+
it 'triggers near the zero second' do
|
23
|
+
|
24
|
+
job = @scheduler.schedule_cron '* * * * *' do; end
|
25
|
+
|
26
|
+
sleep_until_next_minute
|
27
|
+
|
28
|
+
expect(job.last_time.to_i % 10).to eq(0)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
#context 'sub-minute' do
|
33
|
+
#
|
34
|
+
# it 'triggers near the zero second' do
|
35
|
+
#
|
36
|
+
# job = @scheduler.schedule_cron '* * * * * *' do; end
|
37
|
+
#
|
38
|
+
# sleep 1.5
|
39
|
+
#
|
40
|
+
# p job.last_time
|
41
|
+
# p job.last_time.to_f
|
42
|
+
# end
|
43
|
+
#end
|
44
|
+
|
45
|
+
context 'first_at/in' do
|
46
|
+
|
47
|
+
it 'does not trigger before first_at is reached' do
|
48
|
+
|
49
|
+
t = Time.now
|
50
|
+
|
51
|
+
job =
|
52
|
+
@scheduler.schedule_cron '* * * * * *', :first_in => '3s' do
|
53
|
+
triggered = Time.now
|
54
|
+
end
|
55
|
+
|
56
|
+
sleep 1
|
57
|
+
|
58
|
+
#p [ t, t.to_f ]
|
59
|
+
#p [ job.last_time, job.last_time.to_f ]
|
60
|
+
#p [ job.first_at, job.first_at.to_f ]
|
61
|
+
|
62
|
+
expect(job.first_at).to be_within_1s_of(t + 3)
|
63
|
+
expect(job.last_time).to eq(nil)
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'triggers for the first time at first_at' do
|
67
|
+
|
68
|
+
first_time = nil
|
69
|
+
t = Time.now
|
70
|
+
|
71
|
+
job = @scheduler.schedule_cron '* * * * * *', :first_in => '3s' do
|
72
|
+
first_time ||= Time.now
|
73
|
+
end
|
74
|
+
sleep 4.5
|
75
|
+
|
76
|
+
expect(job.first_at).to be_within_1s_of(t + 3)
|
77
|
+
expect(first_time).to be_within_1s_of(job.first_at)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'scheduling the cron itself' do
|
82
|
+
|
83
|
+
# for https://github.com/jmettraux/rufus-scheduler/issues/95
|
84
|
+
#
|
85
|
+
# schedule_cron takes more than 30 seconds, blocking...
|
86
|
+
#
|
87
|
+
it 'does not sit scheduling and blocking...' do
|
88
|
+
|
89
|
+
n = Time.now
|
90
|
+
first = nil
|
91
|
+
|
92
|
+
job = @scheduler.schedule_cron '*/2 * * * * *' do
|
93
|
+
first ||= Time.now
|
94
|
+
end
|
95
|
+
|
96
|
+
expect(Time.now - n).to be < 1.0
|
97
|
+
|
98
|
+
loop do
|
99
|
+
next unless first
|
100
|
+
expect(first - n).to be < 4.0
|
101
|
+
break
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe '.next_time' do
|
107
|
+
|
108
|
+
it 'returns the next trigger time' do
|
109
|
+
|
110
|
+
n = Time.now
|
111
|
+
nt = Time.parse("#{n.year}-#{n.month + 1}-01")
|
112
|
+
|
113
|
+
expect(
|
114
|
+
@scheduler.schedule_cron('* * 1 * *', lambda {}).next_time
|
115
|
+
).to eq(nt)
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'returns the next trigger time (first_at => Time)' do
|
119
|
+
|
120
|
+
ft = Time.parse('2100-12-31')
|
121
|
+
|
122
|
+
job = @scheduler.schedule_cron('* * 1 * *', :first_at => ft) {}
|
123
|
+
|
124
|
+
expect(job.next_time).to eq(ft)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
@@ -0,0 +1,104 @@
|
|
1
|
+
|
2
|
+
#
|
3
|
+
# Specifying rufus-scheduler
|
4
|
+
#
|
5
|
+
# Wed Apr 17 06:00:59 JST 2013
|
6
|
+
#
|
7
|
+
|
8
|
+
require 'spec_helper'
|
9
|
+
|
10
|
+
|
11
|
+
describe Rufus::Scheduler::EveryJob do
|
12
|
+
|
13
|
+
before :each do
|
14
|
+
@scheduler = Rufus::Scheduler.new
|
15
|
+
end
|
16
|
+
after :each do
|
17
|
+
@scheduler.shutdown
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'triggers as expected' do
|
21
|
+
|
22
|
+
counter = 0
|
23
|
+
|
24
|
+
@scheduler.every '1s' do
|
25
|
+
counter = counter + 1
|
26
|
+
end
|
27
|
+
|
28
|
+
sleep 3.5
|
29
|
+
|
30
|
+
expect([ 2, 3 ]).to include(counter)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'lets its @next_time change in-flight' do
|
34
|
+
|
35
|
+
times = []
|
36
|
+
|
37
|
+
@scheduler.every '1s' do |job|
|
38
|
+
times << Time.now
|
39
|
+
job.next_time = Time.now + 3 if times.count == 2
|
40
|
+
end
|
41
|
+
|
42
|
+
sleep 0.3 while times.count < 3
|
43
|
+
|
44
|
+
#p [ times[1] - times[0], times[2] - times[1] ]
|
45
|
+
|
46
|
+
expect(times[1] - times[0]).to be > 1.0
|
47
|
+
expect(times[1] - times[0]).to be < 1.4
|
48
|
+
expect(times[2] - times[1]).to be > 3.0
|
49
|
+
expect(times[2] - times[1]).to be < 3.4
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'summer time' do
|
53
|
+
|
54
|
+
it 'triggers correctly through a DST transition' do
|
55
|
+
|
56
|
+
job = Rufus::Scheduler::EveryJob.new(@scheduler, '1m', {}, lambda {})
|
57
|
+
t1 = ltz('America/Los_Angeles', 2015, 3, 8, 1, 55)
|
58
|
+
t2 = ltz('America/Los_Angeles', 2015, 3, 8, 3, 05)
|
59
|
+
job.next_time = t1
|
60
|
+
occurrences = job.occurrences(t1, t2)
|
61
|
+
|
62
|
+
expect(occurrences.length).to eq(11)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'first_at/in' do
|
67
|
+
|
68
|
+
it 'triggers for the first time at first_at' do
|
69
|
+
|
70
|
+
t = Time.now
|
71
|
+
|
72
|
+
job = @scheduler.schedule_every '3s', :first_in => '1s' do; end
|
73
|
+
|
74
|
+
sleep 2
|
75
|
+
|
76
|
+
#p [ t, t.to_f ]
|
77
|
+
#p [ job.last_time, job.last_time.to_f ]
|
78
|
+
#p [ job.first_at, job.first_at.to_f ]
|
79
|
+
|
80
|
+
expect(job.first_at).to be_within_1s_of(t + 2)
|
81
|
+
expect(job.last_time).to be_within_1s_of(job.first_at)
|
82
|
+
end
|
83
|
+
|
84
|
+
describe '#first_at=' do
|
85
|
+
|
86
|
+
it 'alters @next_time' do
|
87
|
+
|
88
|
+
job = @scheduler.schedule_every '3s', :first_in => '10s' do; end
|
89
|
+
|
90
|
+
fa0 = job.first_at
|
91
|
+
nt0 = job.next_time
|
92
|
+
|
93
|
+
job.first_at = Time.now + 3
|
94
|
+
|
95
|
+
fa1 = job.first_at
|
96
|
+
nt1 = job.next_time
|
97
|
+
|
98
|
+
expect(nt0).to eq(fa0)
|
99
|
+
expect(nt1).to eq(fa1)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
data/spec/job_in_spec.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
|
2
|
+
#
|
3
|
+
# Specifying rufus-scheduler
|
4
|
+
#
|
5
|
+
# Wed Apr 17 06:00:59 JST 2013
|
6
|
+
#
|
7
|
+
|
8
|
+
require 'spec_helper'
|
9
|
+
|
10
|
+
|
11
|
+
describe Rufus::Scheduler::InJob do
|
12
|
+
|
13
|
+
before :each do
|
14
|
+
@scheduler = Rufus::Scheduler.new
|
15
|
+
end
|
16
|
+
after :each do
|
17
|
+
@scheduler.shutdown
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,68 @@
|
|
1
|
+
|
2
|
+
#
|
3
|
+
# Specifying rufus-scheduler
|
4
|
+
#
|
5
|
+
# Wed Apr 17 06:00:59 JST 2013
|
6
|
+
#
|
7
|
+
|
8
|
+
require 'spec_helper'
|
9
|
+
|
10
|
+
|
11
|
+
describe Rufus::Scheduler::IntervalJob do
|
12
|
+
|
13
|
+
before :each do
|
14
|
+
@scheduler = Rufus::Scheduler.new
|
15
|
+
end
|
16
|
+
after :each do
|
17
|
+
@scheduler.shutdown
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#interval' do
|
21
|
+
|
22
|
+
it 'returns the scheduled interval' do
|
23
|
+
|
24
|
+
job = @scheduler.schedule_interval('1h') do; end
|
25
|
+
|
26
|
+
expect(job.interval).to eq(3600)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'first_at/in' do
|
31
|
+
|
32
|
+
it 'triggers for the first time at first_at' do
|
33
|
+
|
34
|
+
t = Time.now
|
35
|
+
|
36
|
+
job = @scheduler.schedule_interval '3s', :first_in => '1s' do; end
|
37
|
+
|
38
|
+
sleep 2
|
39
|
+
|
40
|
+
#p [ t, t.to_f ]
|
41
|
+
#p [ job.last_time, job.last_time.to_f ]
|
42
|
+
#p [ job.first_at, job.first_at.to_f ]
|
43
|
+
|
44
|
+
expect(job.first_at).to be_within_1s_of(t + 2)
|
45
|
+
expect(job.last_time).to be_within_1s_of(job.first_at)
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '#first_at=' do
|
49
|
+
|
50
|
+
it 'alters @next_time' do
|
51
|
+
|
52
|
+
job = @scheduler.schedule_interval '3s', :first_in => '10s' do; end
|
53
|
+
|
54
|
+
fa0 = job.first_at
|
55
|
+
nt0 = job.next_time
|
56
|
+
|
57
|
+
job.first_at = Time.now + 3
|
58
|
+
|
59
|
+
fa1 = job.first_at
|
60
|
+
nt1 = job.next_time
|
61
|
+
|
62
|
+
expect(nt0).to eq(fa0)
|
63
|
+
expect(nt1).to eq(fa1)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|