index_tree 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 331b4073422929cabfbb5292d94d11bd1aca0e80
4
+ data.tar.gz: 20958ba592b248ed9137ddac5b72f9f2f1b8cc89
5
+ SHA512:
6
+ metadata.gz: 7f0b87af9ac9a7d5be4921f588464bec77655f1abb75ebefa7f38bf63fc9c4cf14897b940941defdf89b56c6f79565e9fa4f3c5fab398c35542f9fae0bc3aa4d
7
+ data.tar.gz: 18ea0a9f0a72749582ba0bd6713e104d7ff577819afc49526070078de54a1353ab40563812f8349c7457d7aa3a071ec07fd8b8af4b1a073c73e0bcf6df2d16b0
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /.idea
4
+ /Gemfile.lock
5
+ /_yardoc/
6
+ /coverage/
7
+ /doc/
8
+ /pkg/
9
+ /spec/reports/
10
+ /tmp/
11
+ *.bundle
12
+ *.so
13
+ *.o
14
+ *.a
15
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in index-tree.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Natural-Intelligence
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Natural Intelligence
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # IndexTree
2
+
3
+ This Gem eager loads trees by indexing the nodes of the tree. The number of queries needed to load a tree is N,
4
+ when N is the number of different models(ActiveRecords) in the tree.
5
+
6
+ Each inner object in the tree have an index node instance that is connecting it to the root.
7
+ When the root of the tree is loaded, only the objects that are in the tree are fetched(Pruning).
8
+ The index nodes are created when the root element is saved.
9
+
10
+ Example:
11
+
12
+ class Equation
13
+ acts_as_indexed_node :root => true do
14
+ has_many :expressions
15
+ end
16
+
17
+ has_one :not_tree_association_a
18
+ end
19
+
20
+
21
+ class Expression
22
+ acts_as_indexed_node do
23
+ has_many :expressions
24
+ end
25
+
26
+ has_one :not_tree_association_b
27
+ end
28
+
29
+ +----------+ +----------+
30
+ |Equation 1| |Equation 2|
31
+ +-+------+-+ +-+------+-+
32
+ | | | |
33
+ +-------+ +-------+ +-------+ +-------+
34
+ | | | |
35
+ v v v v
36
+ +-----------+ +-----------+ +-----------+ +-----------+
37
+ |Expression1| |Expression2| |Expression5| |Expression6|
38
+ +-----------+ +-+-------+-+ +-----------+ +-+-------+-+
39
+ | | | |
40
+ +-------+ +-------+ +-------+ +-------+
41
+ | | | |
42
+ v v v v
43
+ +-----------+ +-----------+ +-----------+ +-----------+
44
+ |Expression3| |Expression4| |Expression7| |Expression8|
45
+ +-----------+ +-----------+ +-----------+ +-----------+
46
+
47
+ The following statement fetches only the objects in the Equation1 tree in two queries:
48
+
49
+ Equation.find(1).preload_tree
50
+
51
+ One query to fetch Equations, and the second query is to fetch Expressions(Doesn't matter how deep is the tree it is still one query)
52
+
53
+
54
+ ## Installation
55
+
56
+ Add this line to your application's Gemfile:
57
+
58
+ ```ruby
59
+ gem 'index_tree'
60
+ ```
61
+
62
+ And then execute:
63
+
64
+ $ bundle
65
+ $ rake db:migrate
66
+
67
+ There is a migration which creates index_tree_index_node table
68
+
69
+ ## Declaration
70
+
71
+ All the models should be loaded in the Rails application, before using the preload_tree.
72
+
73
+ class RootNode < ActiveRecord::Base
74
+ acts_as_indexed_node :root => true do
75
+ has_many :child_nodes, dependent: :destroy
76
+ end
77
+ end
78
+
79
+ ### The following associations are supported:
80
+
81
+ belongs_to
82
+ belongs_to :class, polymorphic: true
83
+ has_one
84
+ has_many
85
+
86
+ ### The following types of inheritance are supported:
87
+
88
+ STI
89
+ Polymorphic associations
90
+
91
+ ### Options:
92
+
93
+ :root Used to declare a root model(default is false)
94
+
95
+ ## Usage
96
+
97
+ RootModel.find(1).preload_tree
98
+ RootModel.all.preload_tree
99
+ RootModel.where(color: 'red').all.preload_tree
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,7 @@
1
+ # Used to for indexing the nodes in tree
2
+ module IndexTree
3
+ class IndexNode < ActiveRecord::Base
4
+ belongs_to :root_element, polymorphic: true
5
+ belongs_to :node_element, polymorphic: true
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ class CreateIndexTreeIndexNodes < ActiveRecord::Migration
2
+ def change
3
+ create_table :index_tree_index_nodes do |t|
4
+ t.references :root_element, polymorphic: true, :null => false
5
+ t.references :node_element, polymorphic: true, :null => false
6
+ t.timestamps
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ class AddIndexToTreeIndexNodes < ActiveRecord::Migration
2
+ def change
3
+ add_index :index_tree_index_nodes, [:root_element_id, :root_element_type], :name => 'index_index_tree_index_nodes_on_root_element'
4
+ end
5
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'index_tree/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "index_tree"
8
+ spec.version = IndexTree::VERSION
9
+ spec.authors = ["Alex Stanovsky"]
10
+ spec.email = %w(info@naturalint.com)
11
+ spec.homepage = 'http://www.naturalint.com'
12
+
13
+ spec.description = %q{Eager loads trees by indexing the nodes of the tree. The number of queries needed to load a tree is N,
14
+ when N is number of different models(ActiveRecords) in the tree}
15
+
16
+ spec.summary = %q{This Gem eager loads trees by indexing the nodes of the tree. The number of queries needed to load a tree is N,
17
+ when N is number of different models(ActiveRecords) in the tree.
18
+ Each inner object in the tree have an index node instance that is connecting it to the root.
19
+ When the root of the tree is loaded, only the objects that are in the tree are fetched(Pruning).
20
+ The index nodes are created when the root element is saved.}
21
+
22
+ spec.files = `git ls-files`.split($/)
23
+ spec.require_paths = %w(lib app)
24
+
25
+ spec.add_dependency "activerecord"
26
+ end
@@ -0,0 +1,20 @@
1
+ module IndexTree
2
+ module ActsAsIndexedNode
3
+ def acts_as_indexed_node(options={}, &block)
4
+ if options[:root]
5
+ include IndexTree::RootElement
6
+ else
7
+ include IndexTree::NodeElement
8
+ end
9
+
10
+ # Find what associations were defined in the given block
11
+ # And set the child nodes
12
+ if block_given?
13
+ current_associations = reflections.keys
14
+ yield
15
+ associations_in_block = reflections.keys - current_associations
16
+ child_nodes *associations_in_block
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ # Rails Engine copies the migration to the Rails app
2
+ module IndexTree
3
+ class Engine < Rails::Engine
4
+ isolate_namespace IndexTree
5
+
6
+ initializer :append_migrations do |app|
7
+ unless app.root.to_s.match root.to_s
8
+ config.paths["db/migrate"].expanded.each do |expanded_path|
9
+ app.config.paths["db/migrate"] << expanded_path
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,33 @@
1
+ # Adds a preload_tree method to the model
2
+ # Usage example:
3
+ #
4
+ # Model.find(1).preload_tree
5
+ #
6
+ module IndexTree
7
+ module FinderMethods
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ def preload_tree
12
+ return IndexTree::TreePreloader.preload_entities(self)
13
+ end
14
+
15
+ end
16
+ end
17
+ end
18
+
19
+
20
+ # Add a preload_tree method to the ActiveRecord_Relation
21
+ # Usage example:
22
+ #
23
+ # Model.where(level: 2).all.preload_tree
24
+ # Model.all.preload_tree
25
+ #
26
+ module ActiveRecord
27
+ class Relation
28
+ def preload_tree
29
+ load unless loaded?
30
+ return IndexTree::TreePreloader.preload_entities(@records)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,59 @@
1
+ require 'active_support/concern'
2
+
3
+ module IndexTree
4
+ module NodeElement
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ # Pointer to the index node
9
+ has_one :index_tree_index_node, :class_name => 'IndexTree::IndexNode', as: :node_element
10
+
11
+ # Creates index node for current tree node, and invokes index node creation for each defined association
12
+ # @param [root_element] Id of the root
13
+ def create_index_node(root_element)
14
+ IndexTree::IndexNode.create!(:root_element => root_element, :node_element => self)
15
+ create_index_nodes_for_children(root_element)
16
+ end
17
+
18
+ private
19
+
20
+ class_attribute :child_nodes
21
+
22
+ # Empty default value
23
+ self.child_nodes = {}
24
+
25
+ # Define what associations are child nodes
26
+ # It will be used to create Index nodes when the root element will be saved
27
+ # @param [*attributes] list of associations
28
+ def self.child_nodes(*attributes)
29
+ self.child_nodes = valid_associations(attributes)
30
+ end
31
+
32
+ # @return [valid_associations] map with existing associations
33
+ def self.valid_associations(attributes)
34
+ valid_associations = {}
35
+
36
+ # Check if the declared input is an existing association
37
+ Array(attributes).each do |association_name|
38
+ valid_associations[association_name] = reflections[association_name] if reflections[association_name]
39
+ end
40
+
41
+ return valid_associations
42
+ end
43
+
44
+ # Invoke index node creation for children node
45
+ # @param [root_element] id of the root element
46
+ def create_index_nodes_for_children(root_element)
47
+
48
+ self.child_nodes.keys.each do |association_name|
49
+ association_value = send(association_name)
50
+
51
+ Array(association_value).each do |child_node|
52
+ child_node.create_index_node(root_element)
53
+ end
54
+ end
55
+ end
56
+
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,8 @@
1
+ # Defines the ActsAssTreeNode Method
2
+ module IndexTree
3
+ class Railtie < Rails::Railtie
4
+ ActiveSupport.on_load(:active_record) do
5
+ extend IndexTree::ActsAsIndexedNode
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,82 @@
1
+ require 'active_support/concern'
2
+
3
+ # Defines the root element of the tree, it invokes the recursive index nodes creation.
4
+ # And Hold the structure of the tree
5
+ module IndexTree
6
+ module RootElement
7
+ extend ActiveSupport::Concern
8
+ include IndexTree::FinderMethods
9
+ include IndexTree::NodeElement
10
+
11
+ included do
12
+ has_many :index_tree_index_nodes, :class_name => 'IndexTree::IndexNode', as: :root_element, dependent: :destroy
13
+
14
+ after_save :rebuild_index_nodes
15
+
16
+ private
17
+ # Will rebuild the index nodes for the decision tree of the Root element
18
+ # It will be used in the eager load for fetching only the data that is related to the current Root Element
19
+ def rebuild_index_nodes
20
+ # Delete all old indices
21
+ IndexTree::IndexNode.delete_all({:root_element => self})
22
+
23
+ # Creates index node for the current tree node, and invokes recursive call for the children of the node
24
+ create_index_nodes_for_children(self)
25
+ end
26
+
27
+ # @return [tree_structure] Each Root Element hold the structure of the tree
28
+ def self.tree_structure
29
+ @@tree_structure ||= create_tree_structure({}, self)
30
+ end
31
+
32
+ # Recursive function that create the structure of the tree
33
+ # Iterates each tree node relation and creates a loading instruction
34
+ # @param [loaded_associations] Previous associations
35
+ # @param [class_to_load] Current class association
36
+ def self.create_tree_structure(loaded_associations, class_to_load)
37
+ class_to_load.child_nodes.each do |association_name, association|
38
+ get_all_child_classes(association).each do |child_class|
39
+
40
+ if (loaded_associations[class_to_load].nil? || loaded_associations[class_to_load][child_class].nil?)
41
+ loaded_associations[class_to_load] ||= {}
42
+
43
+ if association.macro == :belongs_to
44
+ loaded_associations[class_to_load][child_class] = {many: false, opposite: false, association_name: association_name, polymorphic: association.polymorphic?}
45
+ elsif association.macro == :has_one
46
+ loaded_associations[class_to_load][child_class] = {many: false, opposite: true, association_name: association_name, polymorphic: association.polymorphic? }
47
+ elsif association.macro == :has_many
48
+ loaded_associations[class_to_load][child_class] = {many: true, opposite: true, association_name: association_name, polymorphic: association.polymorphic?}
49
+ end
50
+
51
+ create_tree_structure(loaded_associations, child_class)
52
+ end
53
+ end
54
+ end
55
+
56
+ return loaded_associations
57
+ end
58
+
59
+ # Return all the classes for current association
60
+ # @param [association]
61
+ def self.get_all_child_classes(association)
62
+ if (association.polymorphic?)
63
+ return find_all_polymorphic_classes(association.name)
64
+ else
65
+ return Array(association.klass)
66
+ end
67
+ end
68
+
69
+ # Find all the classes from the polymorphic type
70
+ # @param [polymorphic_class] type to find
71
+ def self.find_all_polymorphic_classes(polymorphic_class)
72
+ ret = []
73
+ ObjectSpace.each_object(Class).select { |klass| klass < ActiveRecord::Base }.each do |i|
74
+ unless i.reflect_on_all_associations.select { |j| j.options[:as] == polymorphic_class }.empty?
75
+ ret << i
76
+ end
77
+ end
78
+ ret.flatten
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,68 @@
1
+ module IndexTree
2
+ module TreePreloader
3
+ def self.preload_entities(root_entities)
4
+
5
+ root_entities_array = Array(root_entities)
6
+ root_entity_class = root_entities_array.first.class
7
+
8
+ cache = {root_entity_class =>
9
+ Hash[root_entities_array.map { |t| [t.id, t] }]}
10
+
11
+ tree_structure = root_entity_class.tree_structure
12
+
13
+ tree_structure.each do |source_class, target_classes|
14
+ target_classes.each do |target_class, load_instruction|
15
+ preload_class(cache, root_entity_class, source_class)
16
+ preload_class(cache, root_entity_class, target_class)
17
+
18
+ if load_instruction[:opposite]
19
+ associate_entities_opposite(cache, source_class, load_instruction[:association_name], target_class, load_instruction[:many])
20
+ else
21
+ associate_entities(cache, source_class, load_instruction[:association_name], target_class, load_instruction[:polymorphic])
22
+ end
23
+ end
24
+ end
25
+
26
+ return root_entities
27
+ end
28
+
29
+ def self.preload_class(cache, root_entity_class, class_to_load)
30
+ unless cache.has_key?(class_to_load)
31
+ cache[class_to_load] = Hash[class_to_load.joins(:index_tree_index_node).where(:index_tree_index_nodes => {:root_element_type => cache[root_entity_class].values.first.class,
32
+ :root_element_id => cache[root_entity_class].keys}).all.map { |t| [t.id, t] }]
33
+ end
34
+ end
35
+
36
+ def self.associate_entities(cache, source_class, association_name, target_class, is_polymorphic)
37
+ cache[source_class].values.each do |source_entity|
38
+ if (is_polymorphic)
39
+ parent_type = source_entity.send(association_name.to_s + '_type')
40
+ next unless parent_type == target_class.to_s
41
+ end
42
+
43
+ parent_id = source_entity.send(association_name.to_s + '_id')
44
+ source_entity.association(association_name).target = cache[target_class][parent_id]
45
+ source_entity.association(association_name).loaded!
46
+ end
47
+ end
48
+
49
+ def self.associate_entities_opposite(cache, source_class, association_name, target_class, many)
50
+ cache[target_class].values.each do |target_entity|
51
+ attr_name = source_class.model_name.element + '_id'
52
+ parent_id = target_entity.send(attr_name)
53
+ if (parent_id)
54
+ if (many)
55
+ cache[source_class][parent_id].association(association_name).target << target_entity
56
+ else
57
+ cache[source_class][parent_id].association(association_name).target = target_entity
58
+ end
59
+ end
60
+ end
61
+
62
+ cache[source_class].values.each do |source_entity|
63
+ source_entity.association(association_name).loaded!
64
+ end
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,3 @@
1
+ module IndexTree
2
+ VERSION = "0.0.1"
3
+ end
data/lib/index_tree.rb ADDED
@@ -0,0 +1,18 @@
1
+ require "index_tree/version"
2
+ require "index_tree/engine"
3
+
4
+
5
+ require 'index_tree/finder_methods'
6
+ require 'index_tree/node_element'
7
+ require 'index_tree/root_element'
8
+ require 'models/index_node'
9
+ require 'index_tree/tree_preloader'
10
+
11
+ require 'index_tree/acts_as_indexed_node'
12
+ require 'index_tree/railtie'
13
+
14
+ module IndexTree
15
+ def self.table_name_prefix
16
+ 'index_tree_'
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: index_tree
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alex Stanovsky
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-09-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: |-
28
+ Eager loads trees by indexing the nodes of the tree. The number of queries needed to load a tree is N,
29
+ when N is number of different models(ActiveRecords) in the tree
30
+ email:
31
+ - info@naturalint.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - ".gitignore"
37
+ - Gemfile
38
+ - LICENSE
39
+ - LICENSE.txt
40
+ - README.md
41
+ - Rakefile
42
+ - app/models/index_node.rb
43
+ - db/migrate/20141004075233_create_index_tree_index_nodes.rb
44
+ - db/migrate/20141004075333_add_index_to_tree_index_nodes.rb
45
+ - index_tree.gemspec
46
+ - lib/index_tree.rb
47
+ - lib/index_tree/acts_as_indexed_node.rb
48
+ - lib/index_tree/engine.rb
49
+ - lib/index_tree/finder_methods.rb
50
+ - lib/index_tree/node_element.rb
51
+ - lib/index_tree/railtie.rb
52
+ - lib/index_tree/root_element.rb
53
+ - lib/index_tree/tree_preloader.rb
54
+ - lib/index_tree/version.rb
55
+ homepage: http://www.naturalint.com
56
+ licenses: []
57
+ metadata: {}
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ - app
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubyforge_project:
75
+ rubygems_version: 2.2.2
76
+ signing_key:
77
+ specification_version: 4
78
+ summary: This Gem eager loads trees by indexing the nodes of the tree. The number
79
+ of queries needed to load a tree is N, when N is number of different models(ActiveRecords)
80
+ in the tree. Each inner object in the tree have an index node instance that is connecting
81
+ it to the root. When the root of the tree is loaded, only the objects that are in
82
+ the tree are fetched(Pruning). The index nodes are created when the root element
83
+ is saved.
84
+ test_files: []