stoplight 6.0.0.rc3 → 6.0.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.
- checksums.yaml +4 -4
- data/README.md +8 -8
- data/UPGRADING.md +23 -0
- data/lib/stoplight/data_store.rb +7 -0
- data/lib/stoplight/error.rb +1 -1
- data/lib/stoplight/infrastructure/redis/storage/scripting.rb +9 -3
- data/lib/stoplight/infrastructure/redis/storage/state/state_snapshot.lua +9 -0
- data/lib/stoplight/infrastructure/redis/storage/state/transition_to_red.lua +2 -1
- data/lib/stoplight/infrastructure/redis/storage/state.rb +8 -8
- data/lib/stoplight/infrastructure/redis/storage/window_metrics/_evict.lua +4 -3
- data/lib/stoplight/version.rb +1 -1
- data/lib/stoplight/wiring/light_factory.rb +10 -1
- data/lib/stoplight.rb +0 -2
- data/sig/stoplight/data_store/redis.rbs +3 -0
- data/sig/stoplight/infrastructure/redis/storage/scripting.rbs +2 -1
- data/sig/stoplight/infrastructure/redis/storage/state.rbs +2 -2
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e9e835f7351f236b8965d8179da39eff4eaf768bbf066c94cb35f8ed6f0360f6
|
|
4
|
+
data.tar.gz: ea64aa21f4874b40843e2f18716da346b4e0a4fbf77f57ca777bafa8e396ae5b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3bfa3fb97ed1f8f2743930257fefb4eba4f380b9b7eea122af9ca792206b43c27b42fb35292d95ab6c608ad70e3f99f6cf3ca0eb319e2d31c2010943e50a9730
|
|
7
|
+
data.tar.gz: 330fce49dcde86b0d0a9864143523db53caa20f64105cd40f85fcc4930769e3cd35ddb1f199aefce184b6c08eed69259acf0d697a9c4dc4e8245f482fc4a2474
|
data/README.md
CHANGED
|
@@ -80,8 +80,8 @@ After the last failure, the light turns red. The next call will raise a `Stoplig
|
|
|
80
80
|
executing the block:
|
|
81
81
|
|
|
82
82
|
```ruby
|
|
83
|
-
light.run { 1 / 0 } #=> raises Stoplight::Error::RedLight:
|
|
84
|
-
light.color # =>
|
|
83
|
+
light.run { 1 / 0 } #=> raises Stoplight::Error::RedLight: Light `Example` is red - traffic stopped until recovery.
|
|
84
|
+
light.color # => :red
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
The `Stoplight::Error::RedLight` provides metadata about the error:
|
|
@@ -108,7 +108,7 @@ light.run { 1 / 1 } #=> 1
|
|
|
108
108
|
If the test probe succeeds, the light turns green again. If it fails, the light turns red again.
|
|
109
109
|
|
|
110
110
|
```ruby
|
|
111
|
-
light.color #=>
|
|
111
|
+
light.color #=> :green
|
|
112
112
|
```
|
|
113
113
|
|
|
114
114
|
### Using Fallbacks
|
|
@@ -388,7 +388,7 @@ By default, Stoplight uses an in-memory data store:
|
|
|
388
388
|
|
|
389
389
|
```ruby
|
|
390
390
|
require "stoplight"
|
|
391
|
-
Stoplight::Default::DATA_STORE
|
|
391
|
+
Stoplight::Wiring::Default::DATA_STORE
|
|
392
392
|
# => #<Stoplight::DataStore::Memory:...>
|
|
393
393
|
```
|
|
394
394
|
|
|
@@ -678,8 +678,8 @@ Example: "Ruby 3.3 reaches end-of-life in March 2027, so Stoplight 7.0 will requ
|
|
|
678
678
|
## Development
|
|
679
679
|
|
|
680
680
|
After checking out the repo, run `bundle install` to install dependencies. Run tests with `bundle exec rspec` and check
|
|
681
|
-
code style with `bundle exec standardrb`.
|
|
682
|
-
|
|
681
|
+
code style with `bundle exec standardrb`. All work happens on `main` - see [CONTRIBUTING.md] for branch naming,
|
|
682
|
+
releases, and the contribution workflow.
|
|
683
683
|
|
|
684
684
|
## Credits
|
|
685
685
|
|
|
@@ -700,7 +700,7 @@ Fowler’s [CircuitBreaker][] article.
|
|
|
700
700
|
[stoplight-sentry]: https://github.com/bolshakov/stoplight-sentry
|
|
701
701
|
[stoplight-honeybadger]: https://github.com/qoqa/stoplight-honeybadger
|
|
702
702
|
[stoplight-statsd]: https://github.com/bolshakov/stoplight-statsd
|
|
703
|
-
[notifier interface documentation]: https://github.com/bolshakov/stoplight/blob/main/
|
|
703
|
+
[notifier interface documentation]: https://github.com/bolshakov/stoplight/blob/main/sig/stoplight/domain/ports/state_transition_notifier.rbs
|
|
704
704
|
[camdez]: https://github.com/camdez
|
|
705
705
|
[tfausak]: https://github.com/tfausak
|
|
706
706
|
[bolshakov]: https://github.com/bolshakov
|
|
@@ -708,7 +708,7 @@ Fowler’s [CircuitBreaker][] article.
|
|
|
708
708
|
[complete list of contributors]: https://github.com/bolshakov/stoplight/graphs/contributors
|
|
709
709
|
[CircuitBreaker]: http://martinfowler.com/bliki/CircuitBreaker.html
|
|
710
710
|
[Redis]: https://redis.io/
|
|
711
|
-
[
|
|
711
|
+
[CONTRIBUTING.md]: CONTRIBUTING.md
|
|
712
712
|
[Valkey]: https://valkey.io/
|
|
713
713
|
[Ruby Maintenance Branches]: https://www.ruby-lang.org/en/downloads/branches/
|
|
714
714
|
[Redis's support policy]: https://redis.io/about/releases/
|
data/UPGRADING.md
CHANGED
|
@@ -8,6 +8,7 @@ Here's what you'll want to tackle during your upgrade, roughly ordered from the
|
|
|
8
8
|
|
|
9
9
|
- [ ] Make every `Stoplight("name", ...)` call site for the same name pass identical settings
|
|
10
10
|
- [ ] Move `data_store`, `notifiers`, and `error_notifier` from individual lights to `Stoplight.configure`
|
|
11
|
+
- [ ] Make sure that you run at least Ruby 3.3+, Redis 7.4+ (or Valkey 7.2+)
|
|
11
12
|
- [ ] Replace the `with_*` builder methods with settings passed at creation time
|
|
12
13
|
- [ ] Replace proc and anonymous-class error matchers with named classes or modules
|
|
13
14
|
- [ ] Account for Stoplight state reset after deployment
|
|
@@ -16,6 +17,7 @@ Here's what you'll want to tackle during your upgrade, roughly ordered from the
|
|
|
16
17
|
- [ ] Round any fractional `cool_off_time` up to a whole number of seconds, at least 1
|
|
17
18
|
- [ ] Drop `warn_on_clock_skew` from your Redis data store setup
|
|
18
19
|
- [ ] Compare `light.color` and `light.state` against symbols, not strings, and pass a symbol to `light.lock`
|
|
20
|
+
- [ ] Check anything that parses or matches `Stoplight::Error::RedLight` messages - the message format changed
|
|
19
21
|
- [ ] Test thoroughly in a staging environment
|
|
20
22
|
|
|
21
23
|
### Lights Are Registered and Reused
|
|
@@ -87,6 +89,13 @@ end
|
|
|
87
89
|
If you genuinely need more than one data store - separate Redis instances per tenant, for example - use named
|
|
88
90
|
systems, which own their own infrastructure. See [Systems: Namespace Isolation & Configuration][systems] for setup.
|
|
89
91
|
|
|
92
|
+
### Ruby 3.3 and Redis 7.4 Are the new Minimum
|
|
93
|
+
|
|
94
|
+
Stoplight 6 requires Ruby 3.3 or newer.
|
|
95
|
+
We also require Redis 7.4 or newer and Valkey 7.2 or newer and no longer tested or supported earlier versions.
|
|
96
|
+
|
|
97
|
+
See the [maintenance policy] for the versions under test.
|
|
98
|
+
|
|
90
99
|
### The Light Builder API Is Gone
|
|
91
100
|
|
|
92
101
|
`Light#with` and the whole `with_*` family are removed: `with_data_store`, `with_cool_off_time`, `with_threshold`,
|
|
@@ -276,6 +285,19 @@ light.lock(Stoplight::Color::GREEN)
|
|
|
276
285
|
Symbols are the idiomatic Ruby choice for a closed set of values. Anything that serializes them - logs, metrics tags,
|
|
277
286
|
the Admin dashboard's JSON - renders the same text as before.
|
|
278
287
|
|
|
288
|
+
### The `RedLight` Exception Message Changed
|
|
289
|
+
|
|
290
|
+
The message of `Stoplight::Error::RedLight` used to be the bare light name. It is now a sentence that names the light
|
|
291
|
+
and says what happened, so a log line or an error tracker (e.g., in Sentry) entry is readable on its own:
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
# 5.x
|
|
295
|
+
Stoplight::Error::RedLight: Payment Service
|
|
296
|
+
|
|
297
|
+
# 6.0
|
|
298
|
+
Stoplight::Error::RedLight: Light `Payment Service` is red - traffic stopped until recovery.
|
|
299
|
+
```
|
|
300
|
+
|
|
279
301
|
### Getting Help
|
|
280
302
|
|
|
281
303
|
If you run into anything this guide doesn't cover, post to our [Discussions forum]. The configuration errors in 6.0
|
|
@@ -582,6 +604,7 @@ Nothing. Stoplight will function as usual.
|
|
|
582
604
|
[Community-supported notifiers]: https://github.com/bolshakov/stoplight/tree/master#community-supported-notifiers
|
|
583
605
|
[How to implement your own notifier?]: https://github.com/bolshakov/stoplight/blob/master/lib/stoplight/notifier/generic.rb
|
|
584
606
|
[dropped notifiers]: https://github.com/bolshakov/stoplight/tree/v3.0.1/lib/stoplight/notifier
|
|
607
|
+
[maintenance policy]: https://github.com/bolshakov/stoplight#maintenance-policy
|
|
585
608
|
[without passing an empty block]: https://github.com/bolshakov/stoplight-admin/blob/9c9848eb94410e46b20972548f0863db224cb6da/lib/sinatra/stoplight_admin.rb#L30
|
|
586
609
|
[sliding window]: https://github.com/bolshakov/stoplight#custom-window-size
|
|
587
610
|
[systems]: https://github.com/bolshakov/stoplight/blob/master/docs/systems.md
|
data/lib/stoplight/data_store.rb
CHANGED
|
@@ -30,6 +30,13 @@ module Stoplight
|
|
|
30
30
|
# @see Stoplight::Infrastructure::Redis::Key
|
|
31
31
|
def self.key_space = KEY_SPACE
|
|
32
32
|
def key_space = self.class.key_space
|
|
33
|
+
|
|
34
|
+
# Lua scripts are loaded into Redis once per server, so every light shares a single script cache.
|
|
35
|
+
#
|
|
36
|
+
# @api private
|
|
37
|
+
def __stoplight__scripting
|
|
38
|
+
@scripting ||= Infrastructure::Redis::Storage::Scripting.new(redis:)
|
|
39
|
+
end
|
|
33
40
|
end
|
|
34
41
|
|
|
35
42
|
class Memory < Base
|
data/lib/stoplight/error.rb
CHANGED
|
@@ -18,7 +18,7 @@ module Stoplight
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
class RedLight < Base
|
|
21
|
-
EXCEPTION_MESSAGE =
|
|
21
|
+
EXCEPTION_MESSAGE = "Light `%{light_name}` is red - traffic stopped until recovery."
|
|
22
22
|
private_constant :EXCEPTION_MESSAGE
|
|
23
23
|
|
|
24
24
|
# @!attribute light_name
|
|
@@ -21,6 +21,7 @@ module Stoplight
|
|
|
21
21
|
@shas = Hash.new do |hash, script_name|
|
|
22
22
|
hash[script_name] = Digest::SHA1.hexdigest(resolve_source(script_name))
|
|
23
23
|
end
|
|
24
|
+
@invoked = Set.new
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
def call(script_name, keys: [], args: [])
|
|
@@ -33,18 +34,23 @@ module Stoplight
|
|
|
33
34
|
rescue ::Redis::CommandError => error
|
|
34
35
|
if error.message.include?("NOSCRIPT") && !retried
|
|
35
36
|
retried = true
|
|
36
|
-
|
|
37
|
+
if @invoked.include?(script_name) # skip warning for the first script invocation on app boot
|
|
38
|
+
warn "Stoplight is unable to find the script '#{script_name}' from Redis's script cache. " \
|
|
39
|
+
"Reloading and retrying..."
|
|
40
|
+
end
|
|
41
|
+
load_script(script_name)
|
|
37
42
|
retry
|
|
38
43
|
else
|
|
39
44
|
raise error
|
|
40
45
|
end
|
|
46
|
+
ensure
|
|
47
|
+
@invoked << script_name
|
|
41
48
|
end
|
|
42
49
|
end
|
|
43
50
|
|
|
44
51
|
private
|
|
45
52
|
|
|
46
|
-
def
|
|
47
|
-
warn "Stoplight is unable to find the script '#{script_name}' in Redis's script cache. Reloading and retrying..."
|
|
53
|
+
def load_script(script_name)
|
|
48
54
|
@redis.then { |client| client.script(:load, resolve_source(script_name)) }
|
|
49
55
|
end
|
|
50
56
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
-- @include now
|
|
2
2
|
|
|
3
3
|
local meta_key = KEYS[1]
|
|
4
|
-
local
|
|
4
|
+
local cool_off_time = tonumber(ARGV[1])
|
|
5
5
|
local current_ts = now() / 1000
|
|
6
|
+
local recovery_scheduled_after_ts = current_ts + cool_off_time
|
|
6
7
|
|
|
7
8
|
-- 1 if the field is a new field in the hash and the value was set
|
|
8
9
|
local became_red = redis.call('HSETNX', meta_key, 'breached_at', current_ts)
|
|
@@ -57,16 +57,19 @@ module Stoplight
|
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def state_snapshot
|
|
60
|
-
breached_at_raw, locked_state, recovery_scheduled_after_raw, recovery_started_at_raw =
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
breached_at_raw, locked_state, recovery_scheduled_after_raw, recovery_started_at_raw, now_ms =
|
|
61
|
+
scripting.call(
|
|
62
|
+
"state/state_snapshot",
|
|
63
|
+
args: [],
|
|
64
|
+
keys: [state_key]
|
|
65
|
+
)
|
|
63
66
|
|
|
64
67
|
Domain::StateSnapshot.new(
|
|
65
68
|
breached_at: breached_at_raw && clock.at(breached_at_raw.to_f),
|
|
66
69
|
locked_state: locked_state&.to_sym || Stoplight::State::UNLOCKED,
|
|
67
70
|
recovery_scheduled_after: recovery_scheduled_after_raw && clock.at(recovery_scheduled_after_raw.to_f),
|
|
68
71
|
recovery_started_at: recovery_started_at_raw && clock.at(recovery_started_at_raw.to_f),
|
|
69
|
-
time: clock.
|
|
72
|
+
time: clock.at(now_ms.fdiv(1000))
|
|
70
73
|
)
|
|
71
74
|
end
|
|
72
75
|
|
|
@@ -123,12 +126,9 @@ module Stoplight
|
|
|
123
126
|
# Transitions to RED state and ensures only one notification
|
|
124
127
|
#
|
|
125
128
|
def transition_to_red
|
|
126
|
-
current_ts = clock.current_time.to_f
|
|
127
|
-
recovery_scheduled_after_ts = current_ts + cool_off_time
|
|
128
|
-
|
|
129
129
|
became_red = scripting.call(
|
|
130
130
|
"state/transition_to_red",
|
|
131
|
-
args: [
|
|
131
|
+
args: [cool_off_time],
|
|
132
132
|
keys: [state_key]
|
|
133
133
|
)
|
|
134
134
|
|
|
@@ -18,6 +18,8 @@ local function evict_buckets(metrics_key, ts_index_key, window_start)
|
|
|
18
18
|
return
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
local failures, successes = 0, 0
|
|
22
|
+
|
|
21
23
|
for from = 1, #buckets, EVICT_BATCH_SIZE do
|
|
22
24
|
local to = math.min(from + EVICT_BATCH_SIZE - 1, #buckets)
|
|
23
25
|
|
|
@@ -29,16 +31,15 @@ local function evict_buckets(metrics_key, ts_index_key, window_start)
|
|
|
29
31
|
|
|
30
32
|
local values = redis.call('HMGET', metrics_key, unpack(fields))
|
|
31
33
|
|
|
32
|
-
local failures, successes = 0, 0
|
|
33
34
|
for i = 1, #values, 2 do
|
|
34
35
|
failures = failures + (tonumber(values[i]) or 0)
|
|
35
36
|
successes = successes + (tonumber(values[i + 1]) or 0)
|
|
36
37
|
end
|
|
37
38
|
|
|
38
|
-
redis.call('HINCRBY', metrics_key, 'total_failures', -failures)
|
|
39
|
-
redis.call('HINCRBY', metrics_key, 'total_successes', -successes)
|
|
40
39
|
redis.call('HDEL', metrics_key, unpack(fields))
|
|
41
40
|
end
|
|
42
41
|
|
|
42
|
+
redis.call('HINCRBY', metrics_key, 'total_failures', -failures)
|
|
43
|
+
redis.call('HINCRBY', metrics_key, 'total_successes', -successes)
|
|
43
44
|
redis.call('ZREMRANGEBYSCORE', ts_index_key, '-inf', window_start)
|
|
44
45
|
end
|
data/lib/stoplight/version.rb
CHANGED
|
@@ -72,7 +72,16 @@ module Stoplight
|
|
|
72
72
|
attr_reader :system_name
|
|
73
73
|
|
|
74
74
|
def recovery_lock_store = storage_set.recovery_lock_store
|
|
75
|
-
|
|
75
|
+
|
|
76
|
+
def storage_scripting
|
|
77
|
+
case data_store_config
|
|
78
|
+
when DataStore::Redis
|
|
79
|
+
data_store_config.__stoplight__scripting
|
|
80
|
+
else
|
|
81
|
+
raise T.absurd(data_store_config)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
76
85
|
def failover_system = T.must(@failover_system)
|
|
77
86
|
|
|
78
87
|
def key_space
|
data/lib/stoplight.rb
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
require "concurrent/map"
|
|
4
4
|
require "zeitwerk"
|
|
5
5
|
|
|
6
|
-
# steep:ignore:start
|
|
7
6
|
loader = Zeitwerk::Loader.for_gem
|
|
8
7
|
loader.inflector.inflect("io" => "IO")
|
|
9
8
|
loader.do_not_eager_load(
|
|
@@ -14,7 +13,6 @@ loader.do_not_eager_load(
|
|
|
14
13
|
loader.ignore("#{__dir__}/generators")
|
|
15
14
|
loader.ignore("#{__dir__}/stoplight/rspec.rb", "#{__dir__}/stoplight/rspec")
|
|
16
15
|
loader.setup
|
|
17
|
-
# steep:ignore:end
|
|
18
16
|
|
|
19
17
|
module Stoplight # rubocop:disable Style/Documentation
|
|
20
18
|
T = Types
|
|
@@ -7,9 +7,12 @@ module Stoplight
|
|
|
7
7
|
KEY_SPACE: Infrastructure::Redis::Key
|
|
8
8
|
|
|
9
9
|
attr_reader redis: Infrastructure::redis
|
|
10
|
+
@scripting: Infrastructure::Redis::Storage::Scripting?
|
|
10
11
|
|
|
11
12
|
def initialize: (Infrastructure::redis) -> void
|
|
12
13
|
|
|
14
|
+
def __stoplight__scripting: -> Infrastructure::Redis::Storage::Scripting
|
|
15
|
+
|
|
13
16
|
def self.key_space: -> Infrastructure::Redis::Key
|
|
14
17
|
def key_space: -> Infrastructure::Redis::Key
|
|
15
18
|
end
|
|
@@ -11,6 +11,7 @@ module Stoplight
|
|
|
11
11
|
|
|
12
12
|
@scripts_path: Array[String]
|
|
13
13
|
@shas: Hash[String, String]
|
|
14
|
+
@invoked: Set[String]
|
|
14
15
|
@redis: redis
|
|
15
16
|
|
|
16
17
|
def initialize: (
|
|
@@ -24,7 +25,7 @@ module Stoplight
|
|
|
24
25
|
?args: Array[_ToS]
|
|
25
26
|
) -> untyped
|
|
26
27
|
|
|
27
|
-
private def
|
|
28
|
+
private def load_script: (String script_name) -> void
|
|
28
29
|
|
|
29
30
|
private def resolve_source: (String script_name) -> String
|
|
30
31
|
end
|
|
@@ -9,7 +9,7 @@ module Stoplight
|
|
|
9
9
|
attr_reader scripting: Scripting
|
|
10
10
|
attr_reader key_space: Key
|
|
11
11
|
attr_reader clock: Domain::_Clock
|
|
12
|
-
attr_reader cool_off_time: Domain::
|
|
12
|
+
attr_reader cool_off_time: Domain::duration_seconds
|
|
13
13
|
attr_reader state_key: String
|
|
14
14
|
|
|
15
15
|
def initialize: (
|
|
@@ -17,7 +17,7 @@ module Stoplight
|
|
|
17
17
|
redis: redis,
|
|
18
18
|
scripting: Scripting,
|
|
19
19
|
key_space: Key,
|
|
20
|
-
cool_off_time: Domain::
|
|
20
|
+
cool_off_time: Domain::duration_seconds
|
|
21
21
|
) -> void
|
|
22
22
|
|
|
23
23
|
def transition_to_green: -> bool
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: stoplight
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 6.0.0
|
|
4
|
+
version: 6.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tëma Bolshakov
|
|
@@ -145,6 +145,7 @@ files:
|
|
|
145
145
|
- lib/stoplight/infrastructure/redis/storage/registry.rb
|
|
146
146
|
- lib/stoplight/infrastructure/redis/storage/scripting.rb
|
|
147
147
|
- lib/stoplight/infrastructure/redis/storage/state.rb
|
|
148
|
+
- lib/stoplight/infrastructure/redis/storage/state/state_snapshot.lua
|
|
148
149
|
- lib/stoplight/infrastructure/redis/storage/state/transition_to_green.lua
|
|
149
150
|
- lib/stoplight/infrastructure/redis/storage/state/transition_to_red.lua
|
|
150
151
|
- lib/stoplight/infrastructure/redis/storage/state/transition_to_yellow.lua
|
|
@@ -344,7 +345,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
344
345
|
- !ruby/object:Gem::Version
|
|
345
346
|
version: '0'
|
|
346
347
|
requirements: []
|
|
347
|
-
rubygems_version: 4.0.
|
|
348
|
+
rubygems_version: 4.0.20
|
|
348
349
|
specification_version: 4
|
|
349
350
|
summary: Traffic control for code.
|
|
350
351
|
test_files: []
|