sidekiq-scheduler 5.0.3 → 5.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +8 -3
- data/lib/sidekiq-scheduler/config.rb +11 -0
- data/lib/sidekiq-scheduler/extensions/web.rb +19 -6
- data/lib/sidekiq-scheduler/job_presenter.rb +1 -1
- data/lib/sidekiq-scheduler/manager.rb +9 -0
- data/lib/sidekiq-scheduler/redis_manager.rb +3 -3
- data/lib/sidekiq-scheduler/scheduler.rb +18 -2
- data/lib/sidekiq-scheduler/sidekiq_adapter.rb +1 -0
- data/lib/sidekiq-scheduler/utils.rb +34 -0
- data/lib/sidekiq-scheduler/version.rb +1 -1
- data/web/locales/cs.yml +1 -0
- data/web/locales/de.yml +1 -0
- data/web/locales/en.yml +1 -0
- data/web/locales/es.yml +1 -0
- data/web/locales/fr.yml +1 -0
- data/web/locales/gd.yml +1 -0
- data/web/locales/it.yml +1 -0
- data/web/locales/ja.yml +1 -0
- data/web/locales/nl.yml +1 -0
- data/web/locales/pl.yml +1 -0
- data/web/locales/pt-BR.yml +2 -1
- data/web/locales/ru.yml +1 -0
- data/web/locales/sv.yml +1 -0
- data/web/locales/zh-cn.yml +1 -0
- data/web/views/recurring_jobs.erb +3 -38
- metadata +12 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 17e9ebdeaa24ce323d7b3f94c7481d741e0f0f6d3daeded5eaef94f8b1e09f37
|
4
|
+
data.tar.gz: c8e1d62c30406b7928444bdd05a0668aac6bbb47c1bc6a752ff795dbf67b8bb2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f94c5008ef8a2ec39837adc8a25145b27c15abf2514ba22f9c2341b1306431aecfcceb877f8b07fe05c0daa09e26e6b4df3f6e10c118893fe78b11ca6b6751cb
|
7
|
+
data.tar.gz: 61e7f37615f1c575d51c0e55593c4a82362829e21533182e60ec7a125fe76273e44bc055f6449a43185c083370eb1e416a0f1031671e537b6b85cec0ff3a218b
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,19 @@
|
|
1
|
+
# 5.0.4
|
2
|
+
- [**FIX**] Ensure rufus-scheduler has a default rufus_scheduler_options value [#434](https://github.com/sidekiq-scheduler/sidekiq-scheduler/issues/426)
|
3
|
+
- [**ENHANCEMENT**] Remove code related to sidekiq < 6 [#443](https://github.com/sidekiq-scheduler/sidekiq-scheduler/pull/443)
|
4
|
+
- [**ENHANCEMENT**] Change cache-control to `private` [#446](https://github.com/sidekiq-scheduler/sidekiq-scheduler/pull/446)
|
5
|
+
- [**ENHANCEMENT**] Increase compatibility range with tilt dependency [#458](https://github.com/sidekiq-scheduler/sidekiq-scheduler/pull/458)
|
6
|
+
- [**ENHANCEMENT**] Ensure we support Ruby 3.3 [#461](https://github.com/sidekiq-scheduler/sidekiq-scheduler/pull/461)
|
7
|
+
- [**ENHANCEMENT**] Use Redis MULTI command (https://github.com/sidekiq-scheduler/sidekiq-scheduler/pull/464)
|
8
|
+
- [**ENHANCEMENT**] Don't attempt to set jon next_time when job is nil [#466](https://github.com/sidekiq-scheduler/sidekiq-scheduler/pull/466)
|
9
|
+
- [**ENHANCEMENT**] Improvements to prevent jobs been enqueued multiple times due to a delay in job execution [#463](https://github.com/sidekiq-scheduler/sidekiq-scheduler/pull/463)
|
10
|
+
- [**FIX**] Prevent stack level too deep error by implementing `to_hash` method [#470](https://github.com/sidekiq-scheduler/sidekiq-scheduler/pull/470)
|
11
|
+
- [**ENHANCEMENT**] Support new Sidekiq model for registering UI plugins [#472](https://github.com/sidekiq-scheduler/sidekiq-scheduler/pull/472)
|
12
|
+
- [**ENHANCEMENT**] Stop testing against Ruby 2.7 and 3.0 [#472](https://github.com/sidekiq-scheduler/sidekiq-scheduler/pull/472#discussion_r1663197863)
|
13
|
+
- [**ENHANCEMENT**] Display `at` and `in` in the dashboard [#291](https://github.com/sidekiq-scheduler/sidekiq-scheduler/pull/291)
|
14
|
+
- [**ENHANCEMENT**] Docs enhancements [#442](https://github.com/sidekiq-scheduler/sidekiq-scheduler/pull/442), [#449](https://github.com/sidekiq-scheduler/sidekiq-scheduler/pull/449), [#457](https://github.com/sidekiq-scheduler/sidekiq-scheduler/pull/457), [#465](https://github.com/sidekiq-scheduler/sidekiq-scheduler/pull/465), [58e1835](https://github.com/sidekiq-scheduler/sidekiq-scheduler/commit/58e18351054fc3c264b2b5a684173316f674c386)
|
15
|
+
|
16
|
+
|
1
17
|
# 5.0.3
|
2
18
|
|
3
19
|
- [**FIX**] Fix "uppercase character in header name: Cache-Control" [#432](https://github.com/sidekiq-scheduler/sidekiq-scheduler/pull/432)
|
data/README.md
CHANGED
@@ -45,6 +45,8 @@ class HelloWorld
|
|
45
45
|
end
|
46
46
|
```
|
47
47
|
|
48
|
+
__Note:__ In Sidekiq v6.3 `Sidekiq::Job` was introduced as an alias for `Sidekiq::Worker`. `Sidekiq::Worker` has been officially deprecated in Sidekiq v7 although it still exists for backwards compatibility. It is therefore recommended to use `include Sidekiq::Job` in the above example unless an older version of Sidekiq is required.
|
49
|
+
|
48
50
|
``` yaml
|
49
51
|
# config/sidekiq.yml
|
50
52
|
|
@@ -55,6 +57,9 @@ end
|
|
55
57
|
class: HelloWorld
|
56
58
|
```
|
57
59
|
|
60
|
+
> [!NOTE]
|
61
|
+
> sidekiq-scheduler uses [fugit](https://github.com/floraison/fugit) under the hood, which supports up to six arguments as the cron string, [see](https://github.com/floraison/fugit?tab=readme-ov-file#the-second-extension).
|
62
|
+
|
58
63
|
Run sidekiq:
|
59
64
|
|
60
65
|
``` sh
|
@@ -122,7 +127,7 @@ The schedule is configured through the `:scheduler:` -> `:schedule` config entry
|
|
122
127
|
|
123
128
|
# Deconstructs a hash defined as the `args` to keyword arguments.
|
124
129
|
#
|
125
|
-
# `
|
130
|
+
# `false` by default.
|
126
131
|
#
|
127
132
|
# Example
|
128
133
|
#
|
@@ -208,7 +213,7 @@ At, and in types push jobs only once. `at` schedules in a point in time:
|
|
208
213
|
at: '3001/01/01'
|
209
214
|
```
|
210
215
|
|
211
|
-
You can specify any string that `DateTime.parse` and `Chronic` understand. To enable Chronic
|
216
|
+
You can specify any string that `DateTime.parse` and `Chronic` understand. To enable [Chronic](https://github.com/mojombo/chronic)
|
212
217
|
strings, you must add it as a dependency.
|
213
218
|
|
214
219
|
`in` triggers after a time duration has elapsed:
|
@@ -359,7 +364,7 @@ MyRegularJob:
|
|
359
364
|
Then we can conditionally load it via an initializer:
|
360
365
|
|
361
366
|
```ruby
|
362
|
-
# config/
|
367
|
+
# config/initializers/sidekiq.rb
|
363
368
|
if ENV.fetch("IS_SCHEDULER", false)
|
364
369
|
Sidekiq.configure_server do |config|
|
365
370
|
config.on(:startup) do
|
@@ -59,6 +59,17 @@ module SidekiqScheduler
|
|
59
59
|
SidekiqScheduler::SidekiqAdapter.sidekiq_queues(sidekiq_config)
|
60
60
|
end
|
61
61
|
|
62
|
+
def to_hash
|
63
|
+
{
|
64
|
+
enabled: enabled?,
|
65
|
+
dynamic: dynamic?,
|
66
|
+
dynamic_every: dynamic_every?,
|
67
|
+
shedule: schedule,
|
68
|
+
listened_queues_only: listened_queues_only?,
|
69
|
+
rufus_scheduler_options: rufus_scheduler_options
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
62
73
|
private
|
63
74
|
|
64
75
|
attr_reader :scheduler_config
|
@@ -1,14 +1,27 @@
|
|
1
1
|
require 'sidekiq/web' unless defined?(Sidekiq::Web)
|
2
2
|
|
3
|
-
|
3
|
+
if SidekiqScheduler::SidekiqAdapter::SIDEKIQ_GTE_7_3_0
|
4
4
|
|
5
|
-
|
6
|
-
Sidekiq::Web.
|
7
|
-
|
5
|
+
# Locale and asset cache is configured in `.regiester`
|
6
|
+
Sidekiq::Web.register(SidekiqScheduler::Web,
|
7
|
+
name: "recurring_jobs",
|
8
|
+
tab: ["Recurring Jobs"],
|
9
|
+
index: ["recurring-jobs"],
|
10
|
+
root_dir: File.expand_path("../../../web", File.dirname(__FILE__)),
|
11
|
+
asset_paths: ["stylesheets-scheduler"]) do |app|
|
12
|
+
# add middleware or additional settings here
|
13
|
+
end
|
14
|
+
|
15
|
+
else
|
16
|
+
|
17
|
+
ASSETS_PATH = File.expand_path('../../../web/assets', __dir__)
|
18
|
+
|
19
|
+
Sidekiq::Web.register(SidekiqScheduler::Web)
|
20
|
+
Sidekiq::Web.tabs['recurring_jobs'] = 'recurring-jobs'
|
21
|
+
Sidekiq::Web.locales << File.expand_path("#{File.dirname(__FILE__)}/../../../web/locales")
|
8
22
|
|
9
|
-
if Sidekiq::VERSION >= '6.0.0'
|
10
23
|
Sidekiq::Web.use Rack::Static, urls: ['/stylesheets-scheduler'],
|
11
24
|
root: ASSETS_PATH,
|
12
25
|
cascade: true,
|
13
|
-
header_rules: [[:all, { 'cache-control' => '
|
26
|
+
header_rules: [[:all, { 'cache-control' => 'private, max-age=86400' }]]
|
14
27
|
end
|
@@ -38,7 +38,7 @@ module SidekiqScheduler
|
|
38
38
|
#
|
39
39
|
# @return [String] with the job's interval
|
40
40
|
def interval
|
41
|
-
@attributes['cron'] || @attributes['interval'] || @attributes['every']
|
41
|
+
@attributes['cron'] || @attributes['interval'] || @attributes['every'] || @attributes['at'] || @attributes['in']
|
42
42
|
end
|
43
43
|
|
44
44
|
# Returns the queue of the job
|
@@ -24,6 +24,15 @@ module SidekiqScheduler
|
|
24
24
|
@scheduler_instance.load_schedule!
|
25
25
|
end
|
26
26
|
|
27
|
+
# This method is needed to avoid exposing unnecessary information.
|
28
|
+
# Because ActiveSupport's `as_json` traverses instance values to convert the object to a hash
|
29
|
+
# unless it responds to `to_hash`.
|
30
|
+
def to_hash
|
31
|
+
{
|
32
|
+
scheduler: @scheduler_instance.to_hash
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
27
36
|
private
|
28
37
|
|
29
38
|
def set_current_scheduler_options(config)
|
@@ -130,9 +130,9 @@ module SidekiqScheduler
|
|
130
130
|
def self.register_job_instance(job_name, time)
|
131
131
|
job_key = pushed_job_key(job_name)
|
132
132
|
registered, _ = Sidekiq.redis do |r|
|
133
|
-
r.
|
134
|
-
|
135
|
-
|
133
|
+
r.multi do |m|
|
134
|
+
m.zadd(job_key, time.to_i, time.to_i)
|
135
|
+
m.expire(job_key, REGISTERED_JOBS_THRESHOLD_IN_SECONDS)
|
136
136
|
end
|
137
137
|
end
|
138
138
|
|
@@ -58,7 +58,7 @@ module SidekiqScheduler
|
|
58
58
|
self.dynamic = config.dynamic?
|
59
59
|
self.dynamic_every = config.dynamic_every?
|
60
60
|
self.listened_queues_only = config.listened_queues_only?
|
61
|
-
self.rufus_scheduler_options = config.rufus_scheduler_options
|
61
|
+
self.rufus_scheduler_options = config.rufus_scheduler_options || {}
|
62
62
|
end
|
63
63
|
|
64
64
|
# the Rufus::Scheduler jobs that are scheduled
|
@@ -128,6 +128,8 @@ module SidekiqScheduler
|
|
128
128
|
schedule, options = SidekiqScheduler::RufusUtils.normalize_schedule_options(config_interval_type)
|
129
129
|
|
130
130
|
rufus_job = new_job(name, interval_type, config, schedule, options)
|
131
|
+
return unless rufus_job
|
132
|
+
|
131
133
|
@scheduled_jobs[name] = rufus_job
|
132
134
|
SidekiqScheduler::Utils.update_job_next_time(name, rufus_job.next_time)
|
133
135
|
|
@@ -248,6 +250,12 @@ module SidekiqScheduler
|
|
248
250
|
end
|
249
251
|
end
|
250
252
|
|
253
|
+
def to_hash
|
254
|
+
{
|
255
|
+
scheduler_config: @scheduler_config.to_hash
|
256
|
+
}
|
257
|
+
end
|
258
|
+
|
251
259
|
private
|
252
260
|
|
253
261
|
attr_reader :scheduler_config
|
@@ -256,7 +264,15 @@ module SidekiqScheduler
|
|
256
264
|
options = options.merge({ :job => true, :tags => [name] })
|
257
265
|
|
258
266
|
rufus_scheduler.send(interval_type, schedule, options) do |job, time|
|
259
|
-
|
267
|
+
if job_enabled?(name)
|
268
|
+
conf = SidekiqScheduler::Utils.sanitize_job_config(config)
|
269
|
+
|
270
|
+
if job.is_a?(Rufus::Scheduler::CronJob)
|
271
|
+
idempotent_job_enqueue(name, SidekiqScheduler::Utils.calc_cron_run_time(job.cron_line, time.utc), conf)
|
272
|
+
else
|
273
|
+
idempotent_job_enqueue(name, time, conf)
|
274
|
+
end
|
275
|
+
end
|
260
276
|
end
|
261
277
|
end
|
262
278
|
|
@@ -4,6 +4,7 @@ module SidekiqScheduler
|
|
4
4
|
class SidekiqAdapter
|
5
5
|
SIDEKIQ_GTE_6_5_0 = Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new('6.5.0')
|
6
6
|
SIDEKIQ_GTE_7_0_0 = Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new('7.0.0')
|
7
|
+
SIDEKIQ_GTE_7_3_0 = Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new('7.3.0')
|
7
8
|
|
8
9
|
def self.fetch_scheduler_config_from_sidekiq(sidekiq_config)
|
9
10
|
return {} if sidekiq_config.nil?
|
@@ -141,5 +141,39 @@ module SidekiqScheduler
|
|
141
141
|
def self.update_job_last_time(name, last_time)
|
142
142
|
SidekiqScheduler::RedisManager.set_job_last_time(name, last_time) if last_time
|
143
143
|
end
|
144
|
+
|
145
|
+
# Try to figure out when the cron job was supposed to run.
|
146
|
+
#
|
147
|
+
# Rufus calls the scheduler block with the current time and not the time the block was scheduled to run.
|
148
|
+
# This means under certain conditions you could have a job get scheduled multiple times because `time.to_i` is used
|
149
|
+
# to key the job in redis. If one server is under load and Rufus tries to run the jobs 1 seconds after the other
|
150
|
+
# server then the job will be queued twice.
|
151
|
+
# This method essentially makes a best guess at when this job was supposed to run and return that.
|
152
|
+
#
|
153
|
+
# @param [Fugit::Cron] cron
|
154
|
+
# @param [Time] time
|
155
|
+
#
|
156
|
+
# @return [Time]
|
157
|
+
def self.calc_cron_run_time(cron, time)
|
158
|
+
time = time.round # remove sub seconds to prevent rounding errors.
|
159
|
+
next_t = cron.next_time(time).utc
|
160
|
+
previous_t = cron.previous_time(time).utc
|
161
|
+
# The `time` var is some point between `previous_t` and `next_t`.
|
162
|
+
# Figure out how far off we are from each side in seconds.
|
163
|
+
next_diff = next_t - time
|
164
|
+
previous_diff = time - previous_t
|
165
|
+
|
166
|
+
if next_diff == previous_diff
|
167
|
+
# In the event `time` is exactly between `previous_t` and `next_t` the diff will not be equal to
|
168
|
+
# `cron.rough_frequency`. In that case we round down.
|
169
|
+
cron.rough_frequency == next_diff ? time : previous_t
|
170
|
+
elsif next_diff > previous_diff
|
171
|
+
# We are closer to the previous run time so return that.
|
172
|
+
previous_t
|
173
|
+
else
|
174
|
+
# We are closer to the next run time so return that.
|
175
|
+
next_t
|
176
|
+
end
|
177
|
+
end
|
144
178
|
end
|
145
179
|
end
|
data/web/locales/cs.yml
CHANGED
data/web/locales/de.yml
CHANGED
data/web/locales/en.yml
CHANGED
data/web/locales/es.yml
CHANGED
data/web/locales/fr.yml
CHANGED
data/web/locales/gd.yml
CHANGED
data/web/locales/it.yml
CHANGED
data/web/locales/ja.yml
CHANGED
data/web/locales/nl.yml
CHANGED
data/web/locales/pl.yml
CHANGED
data/web/locales/pt-BR.yml
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
pt-BR:
|
2
|
+
"Recurring Jobs": Jobs Recorrentes
|
2
3
|
recurring_jobs: Jobs Recorrentes
|
3
4
|
name: Nome
|
4
5
|
description: Descrição
|
@@ -6,7 +7,7 @@ pt-BR:
|
|
6
7
|
class: Classe
|
7
8
|
queue: Fila
|
8
9
|
arguments: Argumentos
|
9
|
-
enqueue_now: Enfileirar agora
|
10
|
+
enqueue_now: Enfileirar agora
|
10
11
|
last_time: Última execução
|
11
12
|
next_time: Próxima execução
|
12
13
|
no_next_time: Não há mais execuçoẽs
|
data/web/locales/ru.yml
CHANGED
data/web/locales/sv.yml
CHANGED
data/web/locales/zh-cn.yml
CHANGED
@@ -1,42 +1,7 @@
|
|
1
|
-
<% if
|
2
|
-
|
1
|
+
<% if SidekiqScheduler::SidekiqAdapter::SIDEKIQ_GTE_7_3_0 %>
|
2
|
+
<%= style_tag "stylesheets-scheduler/recurring_jobs.css" %>
|
3
3
|
<% else %>
|
4
|
-
|
5
|
-
.recurring-jobs { border-top-left-radius: 4px; border-top-right-radius: 4px; }
|
6
|
-
.recurring-jobs .title { margin-bottom: 5px; }
|
7
|
-
.recurring-jobs .title .name { font-weight: bold;}
|
8
|
-
.recurring-jobs .info,
|
9
|
-
.recurring-jobs .description { margin-bottom: 5px; }
|
10
|
-
.recurring-jobs .actions { margin-bottom: 5px; }
|
11
|
-
.recurring-jobs .status,
|
12
|
-
.recurring-jobs .description { font-size: 12px; }
|
13
|
-
.recurring-jobs .enqueue { margin-bottom: 0.5rem }
|
14
|
-
|
15
|
-
.list-group-item {
|
16
|
-
background-color: #f3f3f3;
|
17
|
-
color: #585454;
|
18
|
-
border: 1px solid rgba(0, 0, 0, 0.1);
|
19
|
-
}
|
20
|
-
|
21
|
-
.list-group-item-disabled {
|
22
|
-
background-color: #f3d3d3;
|
23
|
-
}
|
24
|
-
|
25
|
-
.toggle-all-buttons {
|
26
|
-
margin-top: 20px;
|
27
|
-
margin-bottom: 10px;
|
28
|
-
line-height: 45px;
|
29
|
-
text-align: right;
|
30
|
-
}
|
31
|
-
|
32
|
-
@media (max-width: 768px) {
|
33
|
-
.toggle-all-buttons {
|
34
|
-
margin-top: 0;
|
35
|
-
text-align: left;
|
36
|
-
line-height: inherit;
|
37
|
-
}
|
38
|
-
}
|
39
|
-
</style>
|
4
|
+
<link href="<%= root_path %>stylesheets-scheduler/recurring_jobs.css" media="screen" rel="stylesheet" type="text/css" />
|
40
5
|
<% end %>
|
41
6
|
|
42
7
|
<div class="row">
|
metadata
CHANGED
@@ -1,16 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sidekiq-scheduler
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.0.
|
4
|
+
version: 5.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Morton Jonuschat
|
8
8
|
- Moove-it
|
9
9
|
- Marcelo Lauxen
|
10
|
-
autorequire:
|
10
|
+
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2024-07-04 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: sidekiq
|
@@ -53,6 +53,9 @@ dependencies:
|
|
53
53
|
- - ">="
|
54
54
|
- !ruby/object:Gem::Version
|
55
55
|
version: 1.4.0
|
56
|
+
- - "<"
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '3'
|
56
59
|
type: :runtime
|
57
60
|
prerelease: false
|
58
61
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -60,6 +63,9 @@ dependencies:
|
|
60
63
|
- - ">="
|
61
64
|
- !ruby/object:Gem::Version
|
62
65
|
version: 1.4.0
|
66
|
+
- - "<"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3'
|
63
69
|
- !ruby/object:Gem::Dependency
|
64
70
|
name: rake
|
65
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -234,7 +240,7 @@ homepage: https://sidekiq-scheduler.github.io/sidekiq-scheduler/
|
|
234
240
|
licenses:
|
235
241
|
- MIT
|
236
242
|
metadata: {}
|
237
|
-
post_install_message:
|
243
|
+
post_install_message:
|
238
244
|
rdoc_options: []
|
239
245
|
require_paths:
|
240
246
|
- lib
|
@@ -249,8 +255,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
249
255
|
- !ruby/object:Gem::Version
|
250
256
|
version: '0'
|
251
257
|
requirements: []
|
252
|
-
rubygems_version: 3.
|
253
|
-
signing_key:
|
258
|
+
rubygems_version: 3.5.3
|
259
|
+
signing_key:
|
254
260
|
specification_version: 4
|
255
261
|
summary: Light weight job scheduling extension for Sidekiq
|
256
262
|
test_files: []
|