rails_surrogate_key_logging 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +1 -0
- data/lib/rails_surrogate_key_logging.rb +3 -0
- data/lib/surrogate_key_logging/action_controller/log_subscriber.rb +16 -0
- data/lib/surrogate_key_logging/action_controller.rb +10 -0
- data/lib/surrogate_key_logging/action_dispatch/request.rb +15 -0
- data/lib/surrogate_key_logging/action_dispatch.rb +10 -0
- data/lib/surrogate_key_logging/active_record/attributes.rb +40 -0
- data/lib/surrogate_key_logging/active_record/log_subscriber.rb +73 -0
- data/lib/surrogate_key_logging/active_record.rb +11 -0
- data/lib/surrogate_key_logging/active_support.rb +36 -0
- data/lib/surrogate_key_logging/key_manager.rb +13 -0
- data/lib/surrogate_key_logging/middleware.rb +18 -0
- data/lib/surrogate_key_logging/railtie.rb +28 -0
- data/lib/surrogate_key_logging/version.rb +18 -0
- data/lib/surrogate_key_logging.rb +60 -0
- data/rails_surrogate_key_logging.gemspec +34 -0
- metadata +120 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d4508c5a982897fd625ce7ee7625cedd7a78691b42a6562f9e036e2577f54b91
|
4
|
+
data.tar.gz: 5d5b0395a7cb737fd5f1dc90f0dbbf74a493fcdb91ad8644a9e802584b649e9a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3201bc7f7e84b36813141a3c9e24883d1fd3a416ec4f05d6e6345fd46d063ebf494bcf39ca333f5292eed7ca845e0604d80f4907064b3db78aac6a496a477a08
|
7
|
+
data.tar.gz: 105d8ed9be27bf5e3f633f230b5836668793c45960628749b09e71570279d7f754207ce4275542fd886129975a6e342da0ac221c32c7ba4b81d27ddc49e12d68
|
data/README.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# rails-surrogate-key-logging
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'action_controller/log_subscriber'
|
4
|
+
|
5
|
+
module SurrogateKeyLogging
|
6
|
+
module ActionController
|
7
|
+
class LogSubscriber < ::ActionController::LogSubscriber
|
8
|
+
|
9
|
+
def start_processing(event)
|
10
|
+
event.payload[:params] = SurrogateKeyLogging.filter_parameters event.payload[:params]
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SurrogateKeyLogging
|
4
|
+
module ActionDispatch
|
5
|
+
module Request
|
6
|
+
|
7
|
+
def filtered_query_string
|
8
|
+
super.gsub(::ActionDispatch::Request::PAIR_RE) do |_|
|
9
|
+
SurrogateKeyLogging.filter_parameters(::Regexp.last_match(1) => ::Regexp.last_match(2)).first.join('=')
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
|
5
|
+
module SurrogateKeyLogging
|
6
|
+
module ActiveRecord
|
7
|
+
module Attributes
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
surrogate_parent_names model_name.singular, model_name.plural
|
12
|
+
end
|
13
|
+
|
14
|
+
class_methods do
|
15
|
+
def surrogate_parent_names(*names)
|
16
|
+
@surrogate_parent_names ||= []
|
17
|
+
names.each do |name|
|
18
|
+
@surrogate_parent_names << name.to_sym
|
19
|
+
surrogate_attributes.each do |attr|
|
20
|
+
SurrogateKeyLogging.add_param_to_filter(attr, name)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
@surrogate_parent_names
|
24
|
+
end
|
25
|
+
|
26
|
+
def surrogate_attributes(*attrs)
|
27
|
+
@surrogate_attributes ||= []
|
28
|
+
attrs.each do |attr|
|
29
|
+
@surrogate_attributes << attr.to_sym
|
30
|
+
surrogate_parent_names.each do |parent|
|
31
|
+
SurrogateKeyLogging.add_param_to_filter(attr, parent)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
@surrogate_attributes
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SurrogateKeyLogging
|
4
|
+
module ActiveRecord
|
5
|
+
class LogSubscriber < ::ActiveRecord::LogSubscriber
|
6
|
+
|
7
|
+
def sql(event) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
8
|
+
self.class.runtime += event.duration
|
9
|
+
return unless logger.debug?
|
10
|
+
|
11
|
+
payload = event.payload
|
12
|
+
|
13
|
+
return if IGNORE_PAYLOAD_NAMES.include?(payload[:name])
|
14
|
+
|
15
|
+
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
|
16
|
+
name = "CACHE #{name}" if payload[:cached]
|
17
|
+
sql = payload[:sql]
|
18
|
+
binds = nil
|
19
|
+
|
20
|
+
name_match = /([A-Za-z]+) (Load|Update|Cache)/.match(payload[:name])
|
21
|
+
model = if name_match && name_match[1] && ::ActiveRecord::Base.descendants.map(&:to_s).include?(name_match[1])
|
22
|
+
name_match[1].safe_constantize
|
23
|
+
end
|
24
|
+
|
25
|
+
if payload[:binds]&.any?
|
26
|
+
casted_params = type_casted_binds(payload[:type_casted_binds])
|
27
|
+
|
28
|
+
binds = []
|
29
|
+
payload[:binds].each_with_index do |attr, i|
|
30
|
+
binds << render_bind(attr, casted_params[i], payload, model)
|
31
|
+
end
|
32
|
+
binds = binds.inspect
|
33
|
+
binds.prepend(' ')
|
34
|
+
end
|
35
|
+
|
36
|
+
name = colorize_payload_name(name, payload[:name])
|
37
|
+
sql = color(sql, sql_color(sql), true) if colorize_logging
|
38
|
+
|
39
|
+
debug " #{name} #{sql}#{binds}"
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def basic_parameter_filter
|
45
|
+
@basic_parameter_filter ||= ::ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)
|
46
|
+
end
|
47
|
+
|
48
|
+
def type_casted_binds(casted_binds)
|
49
|
+
casted_binds.respond_to?(:call) ? casted_binds.call : casted_binds
|
50
|
+
end
|
51
|
+
|
52
|
+
def render_bind(attr, value, _payload, model) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
53
|
+
case attr
|
54
|
+
when ActiveModel::Attribute
|
55
|
+
value = "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>" if attr.type.binary? && attr.value
|
56
|
+
when Array
|
57
|
+
attr = attr.first
|
58
|
+
else
|
59
|
+
attr = nil
|
60
|
+
end
|
61
|
+
|
62
|
+
attr_name = attr&.name
|
63
|
+
if model && attr_name && model.respond_to?(:surrogate_attributes) && model.surrogate_attributes.include?(attr_name.to_sym)
|
64
|
+
value = SurrogateKeyLogging.key_manager.call(attr_name, value, model.to_s.underscore)
|
65
|
+
elsif attr_name
|
66
|
+
value = basic_parameter_filter.filter(attr_name => value)[attr_name]
|
67
|
+
end
|
68
|
+
[attr_name, value]
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/parameter_filter'
|
4
|
+
|
5
|
+
# Add ability for @mask to be a class/instance/lambda/proc
|
6
|
+
module ActiveSupport
|
7
|
+
module ParameterFilter
|
8
|
+
class CompiledFilter
|
9
|
+
|
10
|
+
def value_for_key(key, value, parents = [], original_params = nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
11
|
+
parents.push(key) if deep_regexps
|
12
|
+
if regexps.any? { |r| r.match?(key.to_s) }
|
13
|
+
value = @mask.respond_to?(:call) ? @mask.call(key, value, parents, original_params) : @mask
|
14
|
+
elsif deep_regexps && (joined = parents.join('.')) && deep_regexps.any? { |r| r.match?(joined) } # rubocop:disable Lint/DuplicateBranch
|
15
|
+
value = @mask.respond_to?(:call) ? @mask.call(key, value, parents, original_params) : @mask
|
16
|
+
elsif value.is_a?(Hash)
|
17
|
+
value = call(value, parents, original_params)
|
18
|
+
elsif value.is_a?(Array)
|
19
|
+
# If we don't pop the current parent it will be duplicated as we
|
20
|
+
# process each array value.
|
21
|
+
parents.pop if deep_regexps
|
22
|
+
value = value.map { |v| value_for_key(key, v, parents, original_params) }
|
23
|
+
# Restore the parent stack after processing the array.
|
24
|
+
parents.push(key) if deep_regexps
|
25
|
+
elsif blocks.any?
|
26
|
+
key = key.dup if key.duplicable?
|
27
|
+
value = value.dup if value.duplicable?
|
28
|
+
blocks.each { |b| b.arity == 2 ? b.call(key, value) : b.call(key, value, original_params) }
|
29
|
+
end
|
30
|
+
parents.pop if deep_regexps
|
31
|
+
value
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SurrogateKeyLogging
|
4
|
+
class KeyManager
|
5
|
+
|
6
|
+
def call(key, value, _parents = [], _original_params = nil)
|
7
|
+
surrogate = "SK#{Digest::SHA512.hexdigest(value.to_s)}"
|
8
|
+
puts "surrogate for key: `#{key}`, value: `#{value}`, surrogate: `#{surrogate}`"
|
9
|
+
surrogate
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SurrogateKeyLogging
|
4
|
+
class Middleware
|
5
|
+
attr_reader :app
|
6
|
+
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
puts 'surrogate new'
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
SurrogateKeyLogging.reset
|
14
|
+
@app.call(env)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails'
|
4
|
+
|
5
|
+
module SurrogateKeyLogging
|
6
|
+
class Railtie < Rails::Railtie
|
7
|
+
railtie_name :surrogate_key_logging
|
8
|
+
|
9
|
+
initializer 'surrogate_key_logging.filter_parameters' do
|
10
|
+
puts 'surrogate_key_logging.filter_parameters'
|
11
|
+
end
|
12
|
+
|
13
|
+
initializer 'surrogate_key_logging.logs' do
|
14
|
+
puts 'surrogate_key_logging.logs'
|
15
|
+
::ActiveRecord::LogSubscriber.detach_from(:active_record)
|
16
|
+
::SurrogateKeyLogging::ActiveRecord::LogSubscriber.attach_to(:active_record)
|
17
|
+
::ActiveSupport::LogSubscriber.detach_from(:action_controller)
|
18
|
+
::SurrogateKeyLogging::ActionController::LogSubscriber.attach_to(:action_controller)
|
19
|
+
::ActionDispatch::Request.include SurrogateKeyLogging::ActionDispatch::Request
|
20
|
+
end
|
21
|
+
|
22
|
+
initializer 'surrogate_key_logging.middleware' do
|
23
|
+
puts 'surrogate_key_logging.middleware'
|
24
|
+
Rails.application.config.middleware.insert_before 0, Middleware
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
|
5
|
+
# Container Module
|
6
|
+
module SurrogateKeyLogging
|
7
|
+
extend ActiveSupport::Autoload
|
8
|
+
|
9
|
+
autoload :Railtie
|
10
|
+
autoload :Version
|
11
|
+
autoload :ActionController
|
12
|
+
autoload :ActionDispatch
|
13
|
+
autoload :ActiveSupport
|
14
|
+
autoload :ActiveRecord
|
15
|
+
autoload :KeyManager
|
16
|
+
autoload :Middleware
|
17
|
+
|
18
|
+
class << self
|
19
|
+
|
20
|
+
def surrogate_attributes(*attrs)
|
21
|
+
@surrogate_attributes ||= []
|
22
|
+
attrs.each do |attr|
|
23
|
+
@surrogate_attributes << attr.to_s
|
24
|
+
end
|
25
|
+
@surrogate_attributes
|
26
|
+
end
|
27
|
+
|
28
|
+
def reset
|
29
|
+
@key_manager = @parameter_filter = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def key_manager
|
33
|
+
@key_manager ||= KeyManager.new
|
34
|
+
end
|
35
|
+
|
36
|
+
def parameter_filter
|
37
|
+
@parameter_filter ||= ::ActiveSupport::ParameterFilter.new(surrogate_attributes, mask: key_manager)
|
38
|
+
end
|
39
|
+
|
40
|
+
def filter_parameters(params)
|
41
|
+
parameter_filter.filter params
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_param_to_filter(attr, parent = nil)
|
45
|
+
if parent.nil?
|
46
|
+
surrogate_attributes attr.to_s
|
47
|
+
else
|
48
|
+
surrogate_attributes(
|
49
|
+
"#{parent}.#{attr}",
|
50
|
+
"#{parent}[#{attr}]",
|
51
|
+
"[#{parent}][#{attr}]"
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
require 'surrogate_key_logging/railtie'
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), 'lib')
|
4
|
+
require 'surrogate_key_logging/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'rails_surrogate_key_logging'
|
8
|
+
s.version = SurrogateKeyLogging::VERSION
|
9
|
+
s.authors = ['Taylor Yelverton']
|
10
|
+
s.email = 'rubygems@yelvert.io'
|
11
|
+
s.homepage = 'https://github.com/ComplyMD/rails_surrogate_key_logging'
|
12
|
+
s.summary = ''
|
13
|
+
s.license = 'MIT'
|
14
|
+
s.description = ''
|
15
|
+
s.metadata = {
|
16
|
+
'bug_tracker_uri' => 'https://github.com/ComplyMD/rails_surrogate_key_logging/issues',
|
17
|
+
'changelog_uri' => 'https://github.com/ComplyMD/rails_surrogate_key_logging/commits/master',
|
18
|
+
'documentation_uri' => 'https://github.com/ComplyMD/rails_surrogate_key_logging/wiki',
|
19
|
+
'homepage_uri' => 'https://github.com/ComplyMD/rails_surrogate_key_logging',
|
20
|
+
'source_code_uri' => 'https://github.com/ComplyMD/rails_surrogate_key_logging',
|
21
|
+
'rubygems_mfa_required' => 'true',
|
22
|
+
}
|
23
|
+
|
24
|
+
s.files = Dir['lib/**/*', 'README.md', 'MIT-LICENSE', 'rails_surrogate_key_logging.gemspec']
|
25
|
+
|
26
|
+
s.require_paths = %w[ lib ]
|
27
|
+
|
28
|
+
s.required_ruby_version = '>= 2.7.0'
|
29
|
+
|
30
|
+
s.add_dependency('actionpack', '>= 6.0.0')
|
31
|
+
s.add_dependency('activerecord', '>= 6.0.0')
|
32
|
+
s.add_dependency('activesupport', '>= 6.0.0')
|
33
|
+
s.add_dependency('railties', '>= 6.0.0')
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rails_surrogate_key_logging
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Taylor Yelverton
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-01-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: actionpack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 6.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 6.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activerecord
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 6.0.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 6.0.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activesupport
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 6.0.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 6.0.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: railties
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 6.0.0
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 6.0.0
|
69
|
+
description: ''
|
70
|
+
email: rubygems@yelvert.io
|
71
|
+
executables: []
|
72
|
+
extensions: []
|
73
|
+
extra_rdoc_files: []
|
74
|
+
files:
|
75
|
+
- README.md
|
76
|
+
- lib/rails_surrogate_key_logging.rb
|
77
|
+
- lib/surrogate_key_logging.rb
|
78
|
+
- lib/surrogate_key_logging/action_controller.rb
|
79
|
+
- lib/surrogate_key_logging/action_controller/log_subscriber.rb
|
80
|
+
- lib/surrogate_key_logging/action_dispatch.rb
|
81
|
+
- lib/surrogate_key_logging/action_dispatch/request.rb
|
82
|
+
- lib/surrogate_key_logging/active_record.rb
|
83
|
+
- lib/surrogate_key_logging/active_record/attributes.rb
|
84
|
+
- lib/surrogate_key_logging/active_record/log_subscriber.rb
|
85
|
+
- lib/surrogate_key_logging/active_support.rb
|
86
|
+
- lib/surrogate_key_logging/key_manager.rb
|
87
|
+
- lib/surrogate_key_logging/middleware.rb
|
88
|
+
- lib/surrogate_key_logging/railtie.rb
|
89
|
+
- lib/surrogate_key_logging/version.rb
|
90
|
+
- rails_surrogate_key_logging.gemspec
|
91
|
+
homepage: https://github.com/ComplyMD/rails_surrogate_key_logging
|
92
|
+
licenses:
|
93
|
+
- MIT
|
94
|
+
metadata:
|
95
|
+
bug_tracker_uri: https://github.com/ComplyMD/rails_surrogate_key_logging/issues
|
96
|
+
changelog_uri: https://github.com/ComplyMD/rails_surrogate_key_logging/commits/master
|
97
|
+
documentation_uri: https://github.com/ComplyMD/rails_surrogate_key_logging/wiki
|
98
|
+
homepage_uri: https://github.com/ComplyMD/rails_surrogate_key_logging
|
99
|
+
source_code_uri: https://github.com/ComplyMD/rails_surrogate_key_logging
|
100
|
+
rubygems_mfa_required: 'true'
|
101
|
+
post_install_message:
|
102
|
+
rdoc_options: []
|
103
|
+
require_paths:
|
104
|
+
- lib
|
105
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 2.7.0
|
110
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '0'
|
115
|
+
requirements: []
|
116
|
+
rubygems_version: 3.1.4
|
117
|
+
signing_key:
|
118
|
+
specification_version: 4
|
119
|
+
summary: ''
|
120
|
+
test_files: []
|