police_state 0.3.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +147 -0
- data/Rakefile +26 -0
- data/lib/police_state.rb +38 -0
- data/lib/police_state/transition_helpers.rb +71 -0
- data/lib/police_state/transition_validator.rb +32 -0
- data/lib/police_state/validation_helpers.rb +34 -0
- data/lib/police_state/version.rb +3 -0
- data/lib/tasks/police_state_tasks.rake +4 -0
- metadata +95 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2cb2c1110ee730d95c0736792fe123449bb87b02
|
4
|
+
data.tar.gz: 7a757181a7fb2ebf15f4d98930c56c6249f3a238
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2a14b69fa95a25bb675c66d4bb2b9fc1a0ad6caa310343eefc1d04a3fac46df5229894a88721378d9e4dcc218d916ab7d676ac21591613b92e5179c121708ddb
|
7
|
+
data.tar.gz: fa2f01744990415a13690664aef9d4cd652b5c03c94291a94fab1da7af7e560f284b111f045e36c7a5f7784320486dfa43c1ccb31008853568e7b12a1ed12444
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2017 Ian Purvis
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
[](https://travis-ci.org/ianpurvis/police_state)
|
2
|
+
[](http://inch-ci.org/github/ianpurvis/police_state)
|
3
|
+
|
4
|
+
# Police State
|
5
|
+
Lightweight state machine for Active Record and Active Model.
|
6
|
+
|
7
|
+
|
8
|
+
## Background
|
9
|
+
After experimenting with state machines in a recent project, I became interested in a workflow that felt more natural for rails. In particular, I wanted to reduce architectural overlap incurred by flow control, guard, and callback workflows.
|
10
|
+
|
11
|
+
The goal of Police State is to let you easily work with state machines based on `ActiveModel::Dirty`, `ActiveModel::Validation`, and `ActiveModel::Callbacks`
|
12
|
+
|
13
|
+
|
14
|
+
## Usage
|
15
|
+
Police State revolves around the use of `TransitionValidator` and two helper methods, `attribute_transitioning?` and `attribute_transitioned?`.
|
16
|
+
|
17
|
+
To get started, just include `PoliceState` in your model and define a set of valid transitions:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
class Model < ApplicationRecord
|
21
|
+
include PoliceState
|
22
|
+
|
23
|
+
enum status: {
|
24
|
+
queued: 0,
|
25
|
+
active: 1,
|
26
|
+
complete: 2,
|
27
|
+
failed: 3
|
28
|
+
}
|
29
|
+
|
30
|
+
validates :status, transition: { from: nil, to: :queued }
|
31
|
+
validates :status, transition: { from: :queued, to: :active }
|
32
|
+
validates :status, transition: { from: :active, to: :complete }
|
33
|
+
validates :status, transition: { from: [:queued, :active], to: :failed }
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
### Committing a Transition
|
38
|
+
One aspect of Police State that will feel different than other ruby state machines is the idea that in-memory state has not fully transitioned until it is persisted to the database. This lets you operate within a traditional Active Record workflow:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
model = Model.new(status: :complete)
|
42
|
+
# => #<Model:0x007fa94844d088 @status=:complete>
|
43
|
+
|
44
|
+
model.status_transitioning?(from: nil)
|
45
|
+
# => true
|
46
|
+
|
47
|
+
model.status_transitioning?(to: :complete)
|
48
|
+
# => true
|
49
|
+
|
50
|
+
model.valid?
|
51
|
+
# => false
|
52
|
+
|
53
|
+
model.errors.to_hash
|
54
|
+
# => {:status=>["can't transition to complete"]}
|
55
|
+
|
56
|
+
model.save
|
57
|
+
# => false
|
58
|
+
|
59
|
+
model.save!
|
60
|
+
# => ActiveRecord::RecordInvalid: Validation failed: Status can't transition to complete
|
61
|
+
|
62
|
+
model.status = :queued
|
63
|
+
# => :queued
|
64
|
+
|
65
|
+
model.valid?
|
66
|
+
# => true
|
67
|
+
|
68
|
+
model.save
|
69
|
+
# => true
|
70
|
+
|
71
|
+
model.status_transitioned?(from: nil, to: :queued)
|
72
|
+
# => true
|
73
|
+
|
74
|
+
```
|
75
|
+
|
76
|
+
|
77
|
+
### Guard Conditions
|
78
|
+
Guard conditions can be introduced for a state by adding a conditional ActiveRecord validation:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
validates :another_field, :presence, if: -> { queued? }
|
82
|
+
```
|
83
|
+
|
84
|
+
### Callbacks
|
85
|
+
|
86
|
+
Callbacks can be attached to specific transitions by adding a condition on `attribute_transitioned?`. If the callback needs to occur before persistence, `attribute_transitioning?` can also be used.
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
after_commit :notify, if: -> { status_transitioned?(to: :complete) }
|
90
|
+
after_commit :alert, if: -> { status_transitioned?(from: :active, to: :failed) }
|
91
|
+
after_commit :log, if: -> { status_transitioned? }
|
92
|
+
```
|
93
|
+
|
94
|
+
### Events
|
95
|
+
Explicit event languge can be added to models by wrapping `update` and / or `update!`
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
def run
|
99
|
+
update(status: :active)
|
100
|
+
end
|
101
|
+
|
102
|
+
def run!
|
103
|
+
update!(status: :active)
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
The bang methods defined by `ActiveRecord::Enum` work as well:
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
model.active!
|
111
|
+
# => ActiveRecord::RecordInvalid: Validation failed: Status can't transition to active
|
112
|
+
```
|
113
|
+
|
114
|
+
### Validation Logic
|
115
|
+
One important note about `TransitionValidator` is that it performs a unidirectional validation. For example, the following ensures that the `active` state can only be reached from the `queued` state:
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
validates :status, transition: { from: :queued, to: :active }
|
119
|
+
```
|
120
|
+
|
121
|
+
However, this does not prevent `queued` from transitioning to other states. Those states must be controlled by their own validators.
|
122
|
+
|
123
|
+
|
124
|
+
### Active Model
|
125
|
+
If you are using Active Model, make sure your class correctly implements `ActiveModel::Dirty`. For an example, check out [spec/test_model.rb](spec/test_model.rb)
|
126
|
+
|
127
|
+
|
128
|
+
## Installation
|
129
|
+
Add this line to your application's Gemfile:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
gem 'police_state'
|
133
|
+
```
|
134
|
+
|
135
|
+
And then execute:
|
136
|
+
```bash
|
137
|
+
$ bundle
|
138
|
+
```
|
139
|
+
|
140
|
+
Or install it yourself as:
|
141
|
+
```bash
|
142
|
+
$ gem install police_state
|
143
|
+
```
|
144
|
+
|
145
|
+
|
146
|
+
## License
|
147
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'PoliceState'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'bundler/gem_tasks'
|
18
|
+
|
19
|
+
begin
|
20
|
+
require 'rspec/core/rake_task'
|
21
|
+
RSpec::Core::RakeTask.new(:spec)
|
22
|
+
task default: :spec
|
23
|
+
rescue LoadError
|
24
|
+
puts 'Could not load RSpec'
|
25
|
+
end
|
26
|
+
|
data/lib/police_state.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2017 Ian Purvis
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
require "active_model"
|
25
|
+
require "active_support/core_ext/array/wrap.rb"
|
26
|
+
require "active_support/core_ext/hash/slice.rb"
|
27
|
+
require "police_state/transition_helpers"
|
28
|
+
require "police_state/transition_validator"
|
29
|
+
require "police_state/validation_helpers"
|
30
|
+
|
31
|
+
module PoliceState
|
32
|
+
extend ActiveSupport::Concern
|
33
|
+
|
34
|
+
included do
|
35
|
+
include PoliceState::TransitionHelpers
|
36
|
+
extend PoliceState::ValidationHelpers
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module PoliceState
|
2
|
+
|
3
|
+
# == Police State Transition Helpers
|
4
|
+
#
|
5
|
+
# Provides a way to monitor attribute state transitions for Active Record and Active Model objects.
|
6
|
+
#
|
7
|
+
# Note: If using with Active Model, make sure that your class implements +ActiveModel::Dirty+.
|
8
|
+
module TransitionHelpers
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
include ActiveModel::AttributeMethods
|
11
|
+
|
12
|
+
# :stopdoc:
|
13
|
+
included do
|
14
|
+
raise ArgumentError, "Including class must implement ActiveModel::Dirty" unless include?(ActiveModel::Dirty)
|
15
|
+
attribute_method_suffix "_transitioned?", "_transitioning?"
|
16
|
+
end
|
17
|
+
# :startdoc:
|
18
|
+
|
19
|
+
# Returns +true+ if +attribute+ transitioned at the last save event, otherwise +false+.
|
20
|
+
#
|
21
|
+
# You can specify origin and destination states using the +:from+ and +:to+
|
22
|
+
# options. If +attribute+ is an +ActiveRecord::Enum+, these may be
|
23
|
+
# specified as either symbol or their native enum value.
|
24
|
+
#
|
25
|
+
# model = Model.create!(status: :complete)
|
26
|
+
# # => #<Model:0x007fa94844d088 @status=:complete>
|
27
|
+
#
|
28
|
+
# model.status_transitioned? # => true
|
29
|
+
# model.status_transitioned?(from: nil) # => true
|
30
|
+
# model.status_transitioned?(to: :complete) # => true
|
31
|
+
# model.status_transitioned?(to: "complete") # => true
|
32
|
+
# model.status_transitioned?(from: nil, to: :complete) # => true
|
33
|
+
def attribute_transitioned?(attr, options={})
|
34
|
+
options = _transform_options_for_attribute(attr, options)
|
35
|
+
!!previous_changes_include?(attr) &&
|
36
|
+
(!options.include?(:to) || options[:to] == previous_changes[attr].last) &&
|
37
|
+
(!options.include?(:from) || options[:from] == previous_changes[attr].first)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns +true+ if +attribute+ is currently transitioning but not saved, otherwise +false+.
|
41
|
+
#
|
42
|
+
# You can specify origin and destination states using the +:from+ and +:to+
|
43
|
+
# options. If +attribute+ is an +ActiveRecord::Enum+, these may be
|
44
|
+
# specified as either symbol or their native enum value.
|
45
|
+
#
|
46
|
+
# model = Model.new(status: :complete)
|
47
|
+
# # => #<Model:0x007fa94844d088 @status=:complete>
|
48
|
+
#
|
49
|
+
# model.status_transitioning? # => true
|
50
|
+
# model.status_transitioning?(from: nil) # => true
|
51
|
+
# model.status_transitioning?(to: :complete) # => true
|
52
|
+
# model.status_transitioning?(to: "complete") # => true
|
53
|
+
# model.status_transitioning?(from: nil, to: :complete) # => true
|
54
|
+
def attribute_transitioning?(attr, options={})
|
55
|
+
options = _transform_options_for_attribute(attr, options)
|
56
|
+
attribute_changed?(attr, options)
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# Facilitates easier change checking for ActiveRecord::Enum attributes
|
63
|
+
# by casting any symbolized :to and :from values into their native strings.
|
64
|
+
def _transform_options_for_attribute(attr, options={})
|
65
|
+
return options unless self.class.respond_to?(:attribute_types)
|
66
|
+
options.transform_values {|value|
|
67
|
+
self.class.attribute_types.with_indifferent_access[attr].cast(value)
|
68
|
+
}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module PoliceState
|
2
|
+
class TransitionValidator < ActiveModel::EachValidator # :nodoc:
|
3
|
+
|
4
|
+
def check_validity!
|
5
|
+
raise ArgumentError, "Options must include :from" unless options.include?(:from)
|
6
|
+
raise ArgumentError, "Options must include :to" unless options.include?(:to)
|
7
|
+
raise ArgumentError, "Options cannot specify array for :to" if options[:to].is_a?(Array)
|
8
|
+
end
|
9
|
+
|
10
|
+
def validate_each(record, attr_name, value)
|
11
|
+
unless transition_allowed?(record, attr_name)
|
12
|
+
record.errors.add(attr_name, "can't transition to #{value}", options)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def destination
|
20
|
+
options[:to]
|
21
|
+
end
|
22
|
+
|
23
|
+
def origins
|
24
|
+
options[:from].instance_eval {nil? ? [nil] : Array.wrap(self)}
|
25
|
+
end
|
26
|
+
|
27
|
+
def transition_allowed?(record, attr_name)
|
28
|
+
!record.attribute_transitioning?(attr_name, to: destination) ||
|
29
|
+
origins.any? {|origin| record.attribute_transitioning?(attr_name, from: origin)}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module PoliceState
|
2
|
+
|
3
|
+
# == Police State Validation Helpers
|
4
|
+
module ValidationHelpers
|
5
|
+
|
6
|
+
# Validates that the specified attribute is transitioning to a
|
7
|
+
# destination state from one or more origin states.
|
8
|
+
#
|
9
|
+
# class Model < ActiveRecord::Base
|
10
|
+
# validates_transition_of :status, from: nil, to: :queued
|
11
|
+
# validates_transition_of :status, from: :queued, to: :active
|
12
|
+
# validates_transition_of :status, from: :active, to: :complete
|
13
|
+
# validates_transition_of :status, from: [:queued, :active], to: :failed
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# Note: This check is only performed if the attribute is transitioning to the
|
17
|
+
# destination state.
|
18
|
+
#
|
19
|
+
# You can specify +nil+ states for nullable attributes.
|
20
|
+
#
|
21
|
+
# Configuration options:
|
22
|
+
#
|
23
|
+
# * <tt>:from</tt> - The origin state(s) of the attribute. This can be supplied as a
|
24
|
+
# single value or an array.
|
25
|
+
# * <tt>:to</tt> - The destination state of the attribute.
|
26
|
+
#
|
27
|
+
# There is also a list of default options supported by every validator:
|
28
|
+
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
|
29
|
+
# See <tt>ActiveModel::Validation#validates</tt> for more information
|
30
|
+
def validates_transition_of(*attr_names)
|
31
|
+
validates_with PoliceState::TransitionValidator, _merge_attributes(attr_names)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: police_state
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ian Purvis
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-09-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.0.2
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 5.0.2
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec-rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sqlite3
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- ian@purvisresearch.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- MIT-LICENSE
|
63
|
+
- README.md
|
64
|
+
- Rakefile
|
65
|
+
- lib/police_state.rb
|
66
|
+
- lib/police_state/transition_helpers.rb
|
67
|
+
- lib/police_state/transition_validator.rb
|
68
|
+
- lib/police_state/validation_helpers.rb
|
69
|
+
- lib/police_state/version.rb
|
70
|
+
- lib/tasks/police_state_tasks.rake
|
71
|
+
homepage:
|
72
|
+
licenses:
|
73
|
+
- MIT
|
74
|
+
metadata: {}
|
75
|
+
post_install_message:
|
76
|
+
rdoc_options: []
|
77
|
+
require_paths:
|
78
|
+
- lib
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
requirements: []
|
90
|
+
rubyforge_project:
|
91
|
+
rubygems_version: 2.6.13
|
92
|
+
signing_key:
|
93
|
+
specification_version: 4
|
94
|
+
summary: Lightweight state machine for Active Record and Active Model.
|
95
|
+
test_files: []
|