roqua-support 0.1.26 → 0.1.27

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ecfd7d74378220979e7a8839f07ea9793a37265c
4
- data.tar.gz: 8dfb4a419c0d3bc0222a2f743fcad30fc0e43141
3
+ metadata.gz: 4bda6e60469417b289cc6b362240df7943ef3a32
4
+ data.tar.gz: 8b5a13cfbeddc4eb24f7d1cca3fe5c828df019c5
5
5
  SHA512:
6
- metadata.gz: d69f343ba3c51be195a532a892d8839eea4bea7cea5ee54f23e790f6fb56c2299c3aa1f94de49a1cad22f342c0feb78d5878d3a0bffed9be143d719224b7497f
7
- data.tar.gz: 76a3f96b867e176bf948ea7bab491c1abd319b7eb02faf8dbb1f0aed73eb9c7251cef075ee5f51fca81b6383c35f1c05408aded3fda050bd339e77ddab60a668
6
+ metadata.gz: 8dbd51457aa872e2684042494741aadc952814266fde183c09a7bf48f8a6d969df495fe3f708c39aa5d6fb3d4ecbcf6ad716037177b69fe65b3da5b205ec60e2
7
+ data.tar.gz: 7f39013acd584c9faead2d9475c0f8ed8fc74b455b4eb621a0596a2fef41482f0b59b7e5fa41c7bb0b8757ed2f1c44bd24875f6bfcc0b02b9084bdca23851779
data/.gitignore CHANGED
@@ -3,6 +3,7 @@
3
3
  .bundle
4
4
  .config
5
5
  .yardoc
6
+ .lock*
6
7
  InstalledFiles
7
8
  _yardoc
8
9
  coverage
data/Gemfile.lock CHANGED
@@ -12,6 +12,7 @@ PATH
12
12
  active_interaction (~> 3.0)
13
13
  activesupport (>= 3.2, < 6)
14
14
  naught (~> 1.0)
15
+ with_advisory_lock (~> 3.2)
15
16
 
16
17
  GEM
17
18
  remote: https://rubygems.org/
@@ -168,6 +169,8 @@ GEM
168
169
  tzinfo (1.2.2)
169
170
  thread_safe (~> 0.1)
170
171
  unicode-display_width (1.3.0)
172
+ with_advisory_lock (3.2.0)
173
+ activerecord (>= 3.2)
171
174
 
172
175
  PLATFORMS
173
176
  ruby
data/README.md CHANGED
@@ -110,6 +110,35 @@ Enable it in a project by calling:
110
110
  Roqua::Probes::DelayedJobProbe.enable
111
111
  ```
112
112
 
113
+ # Scheduling
114
+
115
+ For running code in a recurring fashion (for example hourly or daily) a scheduling mechanism is included in roqua-support.
116
+
117
+ To configure the jobs that need to run you need to create the file `config/schedule.rb` in the Rails project that you want
118
+ to schedule jobs for. An example configuration:
119
+
120
+ ```ruby
121
+ Roqua::Scheduling::Schedule.setup do |cron|
122
+ cron.add_task 'hourly', run_at: Roqua::Scheduling::Schedule::BEGINNING_OF_EVERY_HOUR do
123
+ # Put code here that you want to run at the first minute of every hour
124
+ end
125
+
126
+ cron.add_task 'daily', run_at: Roqua::Scheduling::Schedule::BEGINNING_OF_EVERY_DAY do
127
+ # Put code here that you want to run every day at 0:00
128
+ end
129
+
130
+ cron.add_task 'every_10_minutes', run_at: proc { 10.minutes.from_now } do
131
+ # Put code here that you want to approximately run every 10 minutes. Because the run_at is calculated after a job
132
+ # is finished the time between jobs will be 10 minutes plus the time the job itself takes.
133
+ end
134
+ end
135
+ ```
136
+
137
+ Normally the RoQua infrastructure should be prepared so the jobs automatically are run if the application is correctly
138
+ deployed. An AppSignal probe can be used as a fallback for job triggering. It can be enabled by placing the following
139
+ code in a Rails initializer:
140
+ `Roqua::Probes::SchedulingProbe.enable`
141
+
113
142
  ### ActiveInteraction extensions
114
143
 
115
144
  ```
data/lib/roqua-support.rb CHANGED
@@ -1,13 +1,23 @@
1
1
  require "roqua-support/version"
2
+ require "roqua-support/railtie" if defined?(Rails)
2
3
  require "roqua/support"
3
4
 
4
5
  module Roqua
5
6
  module Probes
6
7
  autoload :DelayedJobProbe, 'roqua/probes/delayed_job_probe'
8
+ autoload :SchedulingProbe, 'roqua/probes/scheduling_probe'
7
9
  end
8
10
 
9
11
  module Responders
10
12
  autoload :ApiErrorsResponder, 'roqua/responders/api_errors_responder'
11
13
  autoload :ActiveInteractionAwareResponder, 'roqua/responders/active_interaction_aware_responder'
12
14
  end
15
+
16
+ module Scheduling
17
+ autoload :CronJob, 'roqua/scheduling/cron_job'
18
+ autoload :CronJobTable, 'roqua/scheduling/cron_job_table'
19
+ autoload :Scheduler, 'roqua/scheduling/scheduler'
20
+ autoload :Schedule, 'roqua/scheduling/schedule'
21
+ autoload :Task, 'roqua/scheduling/task'
22
+ end
13
23
  end
@@ -0,0 +1,16 @@
1
+ module Roqua
2
+ # Include global rake tasks
3
+ class Railtie < Rails::Railtie
4
+ rake_tasks do
5
+ namespace :roqua do
6
+ desc 'Check if cron needs to run on this instance of the application'
7
+ task :cron_ping do
8
+ ActiveRecord::Base.establish_connection
9
+ require_relative Rails.root.join('config', 'schedule.rb')
10
+ Roqua::Scheduling::Scheduler.new.ping
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+
@@ -1,5 +1,5 @@
1
1
  module Roqua
2
2
  module Support
3
- VERSION = '0.1.26'.freeze
3
+ VERSION = '0.1.27'.freeze
4
4
  end
5
5
  end
@@ -0,0 +1,15 @@
1
+ require_relative 'base_probe'
2
+
3
+ module Roqua
4
+ module Probes
5
+ class SchedulingProbe
6
+ extend BaseProbe
7
+
8
+ def call
9
+ ActiveRecord::Base.establish_connection
10
+ require_relative Rails.root.join('config', 'schedule.rb')
11
+ Roqua::Scheduling::Scheduler.new.ping
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ class Roqua::Scheduling::CronJob < ActiveRecord::Base
2
+ self.table_name = 'roqua_scheduling_cron_jobs'
3
+ end
@@ -0,0 +1,12 @@
1
+ module Roqua::Scheduling::CronJobTable
2
+ def self.create_cron_jobs_table(schema)
3
+ schema.create_table :roqua_scheduling_cron_jobs, force: true do |t|
4
+ t.string :name, null: false
5
+ t.datetime :next_run_at, null: false
6
+ t.datetime :completed_at
7
+ t.timestamps null: false
8
+ end
9
+
10
+ schema.add_index :roqua_scheduling_cron_jobs, :name, unique: true
11
+ end
12
+ end
@@ -0,0 +1,34 @@
1
+ class Roqua::Scheduling::Schedule
2
+ attr_reader :tasks
3
+
4
+ BEGINNING_OF_EVERY_DAY = proc { Time.now.beginning_of_day + 1.day }
5
+ BEGINNING_OF_EVERY_HOUR = proc { Time.now.beginning_of_hour + 1.hour }
6
+
7
+ def initialize
8
+ @tasks = {}
9
+ end
10
+
11
+ def add_task(name, options, &block)
12
+ @tasks[name] = Roqua::Scheduling::Task.new(name, options, block)
13
+ end
14
+
15
+ def self.setup
16
+ @schedule = Roqua::Scheduling::Schedule.new.tap do |new_schedule|
17
+ yield(new_schedule)
18
+ new_schedule.initialize_cronjob_table
19
+ end
20
+ end
21
+
22
+ def self.current_schedule
23
+ @schedule ||= Roqua::Scheduling::Schedule.new
24
+ end
25
+
26
+ def initialize_cronjob_table
27
+ Roqua::Scheduling::CronJob.where('name NOT IN (?)', tasks.values.map(&:name)).delete_all
28
+
29
+ tasks.each_value do |task|
30
+ cron_job = Roqua::Scheduling::CronJob.find_or_initialize_by(name: task.name)
31
+ cron_job.update next_run_at: task.next_run_at unless cron_job.persisted?
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,47 @@
1
+ require 'with_advisory_lock'
2
+
3
+ class Roqua::Scheduling::Scheduler
4
+ def ping
5
+ with_advisory_lock do
6
+ jobs_to_run.each do |cron_job|
7
+ begin
8
+ run_task cron_job
9
+ rescue Exception => ex
10
+ Roqua::Support::Errors.report(ex)
11
+ raise ex if Rails.env.test?
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ def jobs_to_run
18
+ Roqua::Scheduling::CronJob.where('next_run_at <= ?', Time.now)
19
+ end
20
+
21
+ def tasks
22
+ schedule.tasks
23
+ end
24
+
25
+ def schedule
26
+ Roqua::Scheduling::Schedule.current_schedule
27
+ end
28
+
29
+ def advisory_lock_name
30
+ "#{ActiveRecord::Base.connection_config[:database]}_cron_lock"
31
+ end
32
+
33
+ private
34
+
35
+ def with_advisory_lock
36
+ ActiveRecord::Base.with_advisory_lock(advisory_lock_name, timeout_seconds: 0) do
37
+ yield
38
+ end
39
+ end
40
+
41
+ def run_task(cron_job)
42
+ task = schedule.tasks[cron_job.name]
43
+ task.run
44
+
45
+ cron_job.update completed_at: Time.now, next_run_at: task.next_run_at
46
+ end
47
+ end
@@ -0,0 +1,23 @@
1
+ module Roqua::Scheduling
2
+ class Task
3
+ attr_reader :name, :options
4
+
5
+ def initialize(name, options, callback)
6
+ @name = name
7
+ @options = options
8
+ @callback = callback
9
+ end
10
+
11
+ def run_at
12
+ options[:run_at]
13
+ end
14
+
15
+ def next_run_at
16
+ run_at.call
17
+ end
18
+
19
+ def run
20
+ @callback.call
21
+ end
22
+ end
23
+ end
@@ -18,15 +18,16 @@ Gem::Specification.new do |gem|
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
19
  gem.require_paths = ["lib"]
20
20
 
21
+ gem.add_dependency 'active_interaction', '~> 3.0'
21
22
  gem.add_dependency 'activesupport', '>= 3.2', '< 6'
22
23
  gem.add_dependency 'naught', '~> 1.0'
23
- gem.add_dependency 'active_interaction', '~> 3.0'
24
+ gem.add_dependency 'with_advisory_lock', '~> 3.2'
24
25
 
26
+ gem.add_development_dependency 'appsignal', '>= 2.3.1'
25
27
  gem.add_development_dependency 'bundler', '~> 1.0'
28
+ gem.add_development_dependency 'delayed_job_active_record'
26
29
  gem.add_development_dependency 'rake'
27
30
  gem.add_development_dependency 'rspec', '>= 2.12.0', '< 4.0'
28
- gem.add_development_dependency 'appsignal', '>= 2.3.1'
29
- gem.add_development_dependency 'delayed_job_active_record'
30
31
  gem.add_development_dependency 'sqlite3'
31
32
  gem.add_development_dependency 'timecop'
32
33
  end
@@ -0,0 +1,106 @@
1
+ require 'spec_helper'
2
+ require 'timecop'
3
+
4
+ describe Roqua::Scheduling::Scheduler do
5
+ before do
6
+ Timecop.freeze
7
+ Roqua::Scheduling::CronJob.delete_all
8
+ end
9
+
10
+ after { Timecop.return }
11
+
12
+ subject { described_class.new }
13
+
14
+ def setup_daily_and_hourly_schedule
15
+ Roqua::Scheduling::Schedule.setup do |cron|
16
+ cron.add_task 'hourly', run_at: Roqua::Scheduling::Schedule::BEGINNING_OF_EVERY_HOUR do
17
+ end
18
+
19
+ cron.add_task 'daily', run_at: Roqua::Scheduling::Schedule::BEGINNING_OF_EVERY_DAY do
20
+ end
21
+ end
22
+ end
23
+
24
+ def setup_hourly_schedule
25
+ Roqua::Scheduling::Schedule.setup do |cron|
26
+ cron.add_task 'hourly', run_at: Roqua::Scheduling::Schedule::BEGINNING_OF_EVERY_HOUR do
27
+ end
28
+ end
29
+ end
30
+
31
+ it 'creates the correct table rows' do
32
+ expect { setup_daily_and_hourly_schedule }
33
+ .to change { Roqua::Scheduling::CronJob.all.pluck(:name, :next_run_at, :completed_at) }
34
+ .from(
35
+ []
36
+ )
37
+ .to(
38
+ [
39
+ ['hourly', Time.now.beginning_of_hour + 1.hour, nil],
40
+ ['daily', Time.now.beginning_of_day + 1.day, nil]
41
+ ]
42
+ )
43
+ end
44
+
45
+ it 'removes unused table rows' do
46
+ setup_daily_and_hourly_schedule
47
+
48
+ expect { setup_hourly_schedule }
49
+ .to change { Roqua::Scheduling::CronJob.all.pluck(:name).sort }
50
+ .from(['daily', 'hourly']).to(['hourly'])
51
+ end
52
+
53
+ it 'generates a database specific advisory lock name' do
54
+ expect(ActiveRecord::Base.connection_config[:database]).to eql(':memory:')
55
+ expect(subject.advisory_lock_name).to eql ':memory:_cron_lock'
56
+ end
57
+
58
+ describe 'with a run_at in the future' do
59
+ before do
60
+ Roqua::Scheduling::Schedule.setup do |cron|
61
+ cron.add_task 'hourly', run_at: proc { 1.minute.from_now } do
62
+ end
63
+ end
64
+ end
65
+
66
+ it 'the task will not be run' do
67
+ expect(subject.schedule.tasks['hourly']).to_not receive(:run)
68
+ subject.ping
69
+ end
70
+ end
71
+
72
+ describe 'callbacks' do
73
+ let(:callback_spy) { spy(:callback) }
74
+
75
+ it 'get executed' do
76
+ Roqua::Scheduling::Schedule.setup do |cron|
77
+ cron.add_task 'hourly', run_at: proc { 1.minute.ago } do
78
+ callback_spy.execute
79
+ end
80
+ end
81
+
82
+ expect(callback_spy).to receive(:execute)
83
+ subject.ping
84
+ end
85
+ end
86
+
87
+ describe 'when running multiple times' do
88
+ before do
89
+ Timecop.freeze(60.seconds.ago)
90
+
91
+ Roqua::Scheduling::Schedule.setup do |cron|
92
+ cron.add_task 'task', run_at: proc { 30.seconds.from_now } do
93
+ end
94
+ end
95
+ end
96
+
97
+ let(:schedule) { Roqua::Scheduling::Schedule.current_schedule }
98
+
99
+ it 'the task is only called once' do
100
+ expect(schedule.tasks['task']).to receive(:run).once
101
+
102
+ Timecop.return
103
+ 3.times { Roqua::Scheduling::Scheduler.new.ping }
104
+ end
105
+ end
106
+ end
data/spec/spec_helper.rb CHANGED
@@ -19,9 +19,9 @@ ActiveRecord::Base.logger = Delayed::Worker.logger
19
19
  ActiveRecord::Migration.verbose = false
20
20
 
21
21
  ActiveRecord::Schema.define do
22
- create_table :delayed_jobs, :force => true do |t|
23
- t.integer :priority, :default => 0
24
- t.integer :attempts, :default => 0
22
+ create_table :delayed_jobs, force: true do |t|
23
+ t.integer :priority, default: 0
24
+ t.integer :attempts, default: 0
25
25
  t.text :handler
26
26
  t.text :last_error
27
27
  t.datetime :run_at
@@ -33,5 +33,7 @@ ActiveRecord::Schema.define do
33
33
  t.timestamps null: false
34
34
  end
35
35
 
36
- add_index :delayed_jobs, [:priority, :run_at], :name => 'delayed_jobs_priority'
36
+ add_index :delayed_jobs, [:priority, :run_at], name: 'delayed_jobs_priority'
37
+
38
+ Roqua::Scheduling::CronJobTable.create_cron_jobs_table(self)
37
39
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roqua-support
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.26
4
+ version: 0.1.27
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marten Veldthuis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-03-12 00:00:00.000000000 Z
11
+ date: 2018-04-04 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: active_interaction
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: activesupport
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -45,95 +59,95 @@ dependencies:
45
59
  - !ruby/object:Gem::Version
46
60
  version: '1.0'
47
61
  - !ruby/object:Gem::Dependency
48
- name: active_interaction
62
+ name: with_advisory_lock
49
63
  requirement: !ruby/object:Gem::Requirement
50
64
  requirements:
51
65
  - - "~>"
52
66
  - !ruby/object:Gem::Version
53
- version: '3.0'
67
+ version: '3.2'
54
68
  type: :runtime
55
69
  prerelease: false
56
70
  version_requirements: !ruby/object:Gem::Requirement
57
71
  requirements:
58
72
  - - "~>"
59
73
  - !ruby/object:Gem::Version
60
- version: '3.0'
74
+ version: '3.2'
61
75
  - !ruby/object:Gem::Dependency
62
- name: bundler
76
+ name: appsignal
63
77
  requirement: !ruby/object:Gem::Requirement
64
78
  requirements:
65
- - - "~>"
79
+ - - ">="
66
80
  - !ruby/object:Gem::Version
67
- version: '1.0'
81
+ version: 2.3.1
68
82
  type: :development
69
83
  prerelease: false
70
84
  version_requirements: !ruby/object:Gem::Requirement
71
85
  requirements:
72
- - - "~>"
86
+ - - ">="
73
87
  - !ruby/object:Gem::Version
74
- version: '1.0'
88
+ version: 2.3.1
75
89
  - !ruby/object:Gem::Dependency
76
- name: rake
90
+ name: bundler
77
91
  requirement: !ruby/object:Gem::Requirement
78
92
  requirements:
79
- - - ">="
93
+ - - "~>"
80
94
  - !ruby/object:Gem::Version
81
- version: '0'
95
+ version: '1.0'
82
96
  type: :development
83
97
  prerelease: false
84
98
  version_requirements: !ruby/object:Gem::Requirement
85
99
  requirements:
86
- - - ">="
100
+ - - "~>"
87
101
  - !ruby/object:Gem::Version
88
- version: '0'
102
+ version: '1.0'
89
103
  - !ruby/object:Gem::Dependency
90
- name: rspec
104
+ name: delayed_job_active_record
91
105
  requirement: !ruby/object:Gem::Requirement
92
106
  requirements:
93
107
  - - ">="
94
108
  - !ruby/object:Gem::Version
95
- version: 2.12.0
96
- - - "<"
97
- - !ruby/object:Gem::Version
98
- version: '4.0'
109
+ version: '0'
99
110
  type: :development
100
111
  prerelease: false
101
112
  version_requirements: !ruby/object:Gem::Requirement
102
113
  requirements:
103
114
  - - ">="
104
115
  - !ruby/object:Gem::Version
105
- version: 2.12.0
106
- - - "<"
107
- - !ruby/object:Gem::Version
108
- version: '4.0'
116
+ version: '0'
109
117
  - !ruby/object:Gem::Dependency
110
- name: appsignal
118
+ name: rake
111
119
  requirement: !ruby/object:Gem::Requirement
112
120
  requirements:
113
121
  - - ">="
114
122
  - !ruby/object:Gem::Version
115
- version: 2.3.1
123
+ version: '0'
116
124
  type: :development
117
125
  prerelease: false
118
126
  version_requirements: !ruby/object:Gem::Requirement
119
127
  requirements:
120
128
  - - ">="
121
129
  - !ruby/object:Gem::Version
122
- version: 2.3.1
130
+ version: '0'
123
131
  - !ruby/object:Gem::Dependency
124
- name: delayed_job_active_record
132
+ name: rspec
125
133
  requirement: !ruby/object:Gem::Requirement
126
134
  requirements:
127
135
  - - ">="
128
136
  - !ruby/object:Gem::Version
129
- version: '0'
137
+ version: 2.12.0
138
+ - - "<"
139
+ - !ruby/object:Gem::Version
140
+ version: '4.0'
130
141
  type: :development
131
142
  prerelease: false
132
143
  version_requirements: !ruby/object:Gem::Requirement
133
144
  requirements:
134
145
  - - ">="
135
146
  - !ruby/object:Gem::Version
136
- version: '0'
147
+ version: 2.12.0
148
+ - - "<"
149
+ - !ruby/object:Gem::Version
150
+ version: '4.0'
137
151
  - !ruby/object:Gem::Dependency
138
152
  name: sqlite3
139
153
  requirement: !ruby/object:Gem::Requirement
@@ -186,6 +200,7 @@ files:
186
200
  - gemfiles/rails42.gemfile
187
201
  - gemfiles/rails50.gemfile
188
202
  - lib/roqua-support.rb
203
+ - lib/roqua-support/railtie.rb
189
204
  - lib/roqua-support/version.rb
190
205
  - lib/roqua/core_ext/active_interaction/filters/date_time_as_unix_extension.rb
191
206
  - lib/roqua/core_ext/active_interaction/filters/duration_filter.rb
@@ -197,8 +212,14 @@ files:
197
212
  - lib/roqua/core_ext/fixnum/clamp.rb
198
213
  - lib/roqua/probes/base_probe.rb
199
214
  - lib/roqua/probes/delayed_job_probe.rb
215
+ - lib/roqua/probes/scheduling_probe.rb
200
216
  - lib/roqua/responders/active_interaction_aware_responder.rb
201
217
  - lib/roqua/responders/api_errors_responder.rb
218
+ - lib/roqua/scheduling/cron_job.rb
219
+ - lib/roqua/scheduling/cron_job_table.rb
220
+ - lib/roqua/scheduling/schedule.rb
221
+ - lib/roqua/scheduling/scheduler.rb
222
+ - lib/roqua/scheduling/task.rb
202
223
  - lib/roqua/support.rb
203
224
  - lib/roqua/support/command_runner.rb
204
225
  - lib/roqua/support/errors.rb
@@ -220,6 +241,7 @@ files:
220
241
  - spec/roqua/probes/delayed_job_probe_spec.rb
221
242
  - spec/roqua/responders/active_interaction_aware_responder_spec.rb
222
243
  - spec/roqua/responders/api_errors_responder_spec.rb
244
+ - spec/roqua/scheduling/scheduler_spec.rb
223
245
  - spec/roqua/support/errors_spec.rb
224
246
  - spec/roqua/support/helpers_spec.rb
225
247
  - spec/roqua/support/logwrapper_spec.rb
@@ -265,6 +287,7 @@ test_files:
265
287
  - spec/roqua/probes/delayed_job_probe_spec.rb
266
288
  - spec/roqua/responders/active_interaction_aware_responder_spec.rb
267
289
  - spec/roqua/responders/api_errors_responder_spec.rb
290
+ - spec/roqua/scheduling/scheduler_spec.rb
268
291
  - spec/roqua/support/errors_spec.rb
269
292
  - spec/roqua/support/helpers_spec.rb
270
293
  - spec/roqua/support/logwrapper_spec.rb