priora 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b201552054fb05af631621353382d07913a23fcd
4
+ data.tar.gz: 50da40ed3a3c0f41984332f1d2b90422288e52ea
5
+ SHA512:
6
+ metadata.gz: 804d38a846743aa25d93e25bb41058882778980b8b8442d27fb851186531f2fe328c341121d30b76dc86b381d4b90dd0654543921d1c475543ec1899568ec3a9
7
+ data.tar.gz: 4e61cfd500b941a3cc7988145c9f8cf6bee9dfb467309b4bc205da3475af89dbea0e6f643b3b14ffdc504131ea0addd10b069198f256cfed8e4a47ca81741c37
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+ .idea/*
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.2
5
+ before_install: gem install bundler -v 1.16.1
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in priora.gemspec
6
+ gemspec
@@ -0,0 +1,35 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ priora (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.2.5)
10
+ rake (10.4.2)
11
+ rspec (3.6.0)
12
+ rspec-core (~> 3.6.0)
13
+ rspec-expectations (~> 3.6.0)
14
+ rspec-mocks (~> 3.6.0)
15
+ rspec-core (3.6.0)
16
+ rspec-support (~> 3.6.0)
17
+ rspec-expectations (3.6.0)
18
+ diff-lcs (>= 1.2.0, < 2.0)
19
+ rspec-support (~> 3.6.0)
20
+ rspec-mocks (3.6.0)
21
+ diff-lcs (>= 1.2.0, < 2.0)
22
+ rspec-support (~> 3.6.0)
23
+ rspec-support (3.6.0)
24
+
25
+ PLATFORMS
26
+ ruby
27
+
28
+ DEPENDENCIES
29
+ bundler (~> 1.16)
30
+ priora!
31
+ rake (~> 10.0)
32
+ rspec (~> 3.0)
33
+
34
+ BUNDLED WITH
35
+ 1.16.1
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Eliav Lavi
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
13
+ all 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
21
+ THE SOFTWARE.
@@ -0,0 +1,173 @@
1
+ # Priora: An Object Prioritization Utility for Ruby
2
+
3
+ Priora supplies an easy and intuitive way to prioritize a collection of objects in Ruby.
4
+ It serves as a useful utility for working with a collection of several instances of some data class.
5
+ Often, we would like to get that collection arranged according to some prioritization logic.
6
+ Instead of writing custom sorting blocks or implementing the spaceship operator (`<=>`) in your class,
7
+ Priora offers a declarative style in order to obtain ready-for-consumption collections.
8
+
9
+ For example, let's assume we have a simple `Post` class, holding data regarding the author name,
10
+ how many likes did it receive and whether this post is sponsored:
11
+
12
+ ```ruby
13
+ class Post
14
+ attr_reader :author, :like_count, :is_sponsored
15
+
16
+ def initialize(author:, like_count:, is_sponsored:)
17
+ @author = author
18
+ @like_count = like_count
19
+ @is_sponsored = is_sponsored
20
+ end
21
+ end
22
+ ```
23
+
24
+ Then, in a given scenario, we have three instances at hand (these examples will be used throughout this README):
25
+
26
+ ```ruby
27
+ low_like_count_sponsored = Post.new(author: 'Jay C.', like_count: 10, is_sponsored: true)
28
+ high_like_count_unsponsored = Post.new(author: 'Aaron R.', like_count: 90, is_sponsored: false)
29
+ high_like_count_sponsored = Post.new(author: 'Don Y.', like_count: 90, is_sponsored: true)
30
+ ```
31
+
32
+ Using Priora, we can easily get the collection prioritized according to our needs:
33
+
34
+ ```ruby
35
+ unprioritized_array = [high_like_count_unsponsored, low_like_count_sponsored, high_like_count_sponsored]
36
+ prioritized_array = [high_like_count_sponsored, high_like_count_unsponsored, low_like_count_sponsored]
37
+ Priora.prioritize(unprioritized_array, by: [:like_count, :is_sponsored]) == prioritized_array
38
+ => true
39
+ ```
40
+
41
+ In case we can commit to the prioritization between `Post` objects - i.e. we do not need the flexibility of
42
+ changing the priorities each time - we can include the `Priora` module in our class and
43
+ declare the priorities using the `prioritize_by` class macro and gain shorter invocation.
44
+ Our class would then read like this:
45
+
46
+ ```ruby
47
+ class Post
48
+ include Priora
49
+ prioritize_by :like_count, :is_sponsored
50
+
51
+ attr_reader :author, :like_count, :is_sponsored
52
+
53
+ def initialize(author:, like_count:, is_sponsored:)
54
+ @author = author
55
+ @like_count = like_count
56
+ @is_sponsored = is_sponsored
57
+ end
58
+ end
59
+ ```
60
+
61
+ And getting the prioritized array would read like this:
62
+
63
+ ```ruby
64
+ Priora.prioritize(unprioritized_array) == prioritized_array
65
+ => true
66
+ ```
67
+
68
+ Using the `prioritize_by` class macro increases the readability of your code for the cost of flexibility.
69
+ By adopting this usage, priorities are declared in-class and Priora can fetch it implicitly.
70
+ For some cases this might be the right choice while for others the explicit style is more suitable.
71
+
72
+ ### Advantages Over Using Custom `sort` Or Implementing `<=>`
73
+ One might come up with the following snippet as an equivalent solution:
74
+
75
+ ```ruby
76
+ unprioritized_array.sort { |a, b| [a.like_count, a.is_sponsored ? 1 : 0 ] <=> [b.like_count, b.is_sponsored ? 1 : 0] }.reverse == prioritized_array
77
+ => true
78
+ ```
79
+
80
+ Which is, of course, correct. However, I find several issues with this code:
81
+ * It is more verbose and prone to errors.
82
+ * It declares the prioritization logic twice.
83
+ * It handles the conversion of a boolean value (`true` / `false`) into a sortable value (`1` / `0`) inline,
84
+ thus mixing levels of abstractions and confusing the potential reader.
85
+
86
+ Another possible alternative is implementing the spaceship operator (`<=>`) for `Post` instances,
87
+ and then simply employ reverse sorting.
88
+ I regard this approach as somewhat more elegant, but its main problem is that it assumes our sorting logic is always the same
89
+ for a given class, which is not always true.
90
+ Priora solves this problem by supporting the explicit `by` parameter.
91
+
92
+ I created Priora after having encountered a few scenarios in which I needed to get some collections prioritized in some
93
+ specific manner, and having to supply these explicit blocks again and again was quite annoying.
94
+ I figured out a modest library solving this problem could be nice to have.
95
+
96
+ ### Reverse Sorting, Extended: An Agenda
97
+
98
+ Priora is based on the presumption that when we talk about a prioritized collection,
99
+ we often refer to the outcome of sorting it and then reversing the result.
100
+ This is because we naturally think about sorting in an ascending fashion, from small to large,
101
+ while when we talk about "top priorities" we usually think of the largest items first.
102
+
103
+ #### Directional Priorities
104
+
105
+ Obviously, this is not always true and some prioritization processes should give precedence to smaller items first;
106
+ Priora supports this scenario as well.
107
+ You may change the prioritization direction for a specific priority:
108
+
109
+ ```ruby
110
+ Priora.prioritize(unprioritized_array, by: [[like_count: :asc], :is_sponsored])
111
+ => [low_like_count_sponsored, high_like_count_sponsored, high_like_count_unsponsored]
112
+ ```
113
+
114
+ We can see that the `Post` with the low `like_count` comes up first,
115
+ however the two high `like_count` posts are prioritized by `is_sponsored`, so the sponsored `Post` comes up first.
116
+
117
+ If you have several priorities for which you wish to specify direction, you need to do so for each separately:
118
+ ```ruby
119
+ Priora.prioritize(unprioritized_array, by: [[like_count: :asc], [is_sponsored: :asc]])
120
+ => [low_like_count_sponsored, high_like_count_unsponsored, high_like_count_sponsored]
121
+ ```
122
+
123
+ ### Implicit Conversions
124
+
125
+ As you might have noted, Priora also takes care of converting non-sortable values,
126
+ such as `true`, `false` or `nil`, into sortable values.
127
+ By default, it assumes that `true` is larger than `false` and that `nil` evaluates to `0`.
128
+
129
+ You may override these implicit conversions with your own lambdas,
130
+ as well as supply your own custom lambdas for other classes (and perhaps override their sorting logic!).
131
+
132
+ For example, if we wished to sort attributes of class `String` by their length,
133
+ we could configure `Priora` accordingly beforehand:
134
+
135
+ ```ruby
136
+ Priora.configuration.add_conversion_lambda(String, lambda { |value| value.length })
137
+ ```
138
+
139
+ Conversion lambdas are also removable, should that need arise:
140
+
141
+ ```ruby
142
+ Priora.configuration.remove_conversion_lambda(String)
143
+ ```
144
+
145
+ ## Installation
146
+
147
+ Add this line to your application's Gemfile:
148
+
149
+ ```ruby
150
+ gem 'priora'
151
+ ```
152
+
153
+ And then execute:
154
+
155
+ $ bundle
156
+
157
+ Or install it yourself as:
158
+
159
+ $ gem install priora
160
+
161
+ ## Development
162
+
163
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
164
+
165
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
166
+
167
+ ## Contributing
168
+
169
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/priora.
170
+
171
+ ## License
172
+
173
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "priora"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,38 @@
1
+ require_relative 'priora/version'
2
+ require_relative 'priora/configuration'
3
+ require_relative 'priora/class_methods'
4
+ require_relative 'priora/collection'
5
+ require_relative 'priora/priority_builder'
6
+ require_relative 'priora/errors'
7
+
8
+ module Priora
9
+ class << self
10
+ def configuration
11
+ @configuration ||= Configuration.new
12
+ end
13
+
14
+ def included(base_class)
15
+ base_class.extend ClassMethods
16
+ end
17
+
18
+ def prioritize(collection, by: nil)
19
+ raw_priorities = by
20
+ priorities = priorities_from_call(collection, raw_priorities)
21
+ Collection.new(collection).prioritize_by(priorities)
22
+ end
23
+
24
+ private
25
+
26
+ def priorities_from_call(collection, raw_priorities)
27
+ if raw_priorities
28
+ raw_priorities.map { |raw_priority| Priora::PriorityBuilder.build(raw_priority) }
29
+ else
30
+ begin
31
+ collection.map { |item| item.class.priorities }.uniq.first
32
+ rescue StandardError => e
33
+ raise UnsuppliedPrioritiesError
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,9 @@
1
+ module Priora
2
+ module ClassMethods
3
+ attr_reader :priorities
4
+
5
+ def prioritize_by(*raw_priorities)
6
+ @priorities = raw_priorities.map { |raw_priority| Priora::PriorityBuilder.build(raw_priority) }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,28 @@
1
+ module Priora
2
+ class Collection
3
+ def initialize(collection)
4
+ @collection = collection
5
+ @comparison_ready_objects = {}
6
+ end
7
+
8
+ def prioritize_by(priorities)
9
+ sorted_collection_by(priorities).reverse
10
+ end
11
+
12
+ private
13
+
14
+ def sorted_collection_by(priorities)
15
+ @collection.sort do |object_a, object_b|
16
+ comparison_ready(object_a, priorities) <=> comparison_ready(object_b, priorities)
17
+ end
18
+ end
19
+
20
+ def comparison_ready(object, priorities)
21
+ @comparison_ready_objects[[object, priorities]] ||= begin
22
+ priorities.map do |priority|
23
+ priority.comparable_value_from(object)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ module Priora
2
+ class Configuration
3
+ def initialize
4
+ @conversion_lambdas = {
5
+ TrueClass: lambda { |_| 1 },
6
+ FalseClass: lambda { |_| 0 },
7
+ NilClass: lambda { |_| 0 }
8
+ }
9
+ end
10
+
11
+ def conversion_lambda_for(klass)
12
+ @conversion_lambdas[klass.to_s.to_sym]
13
+ end
14
+
15
+ def add_conversion_lambda(klass, lambda)
16
+ raise InvalidConversionLambda if lambda.arity > 1
17
+ @conversion_lambdas[klass.to_s.to_sym] = lambda
18
+ end
19
+
20
+ def remove_conversion_lambda(klass)
21
+ @conversion_lambdas.delete_if { |k, _| k == klass.to_s.to_sym }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ module Priora
2
+ class UnsuppliedPrioritiesError < StandardError
3
+ def initialize
4
+ super('prioritization attributes must be declared in class or supplied manually!')
5
+ end
6
+ end
7
+
8
+ class InvalidPrioritySyntaxError < StandardError
9
+ def initialize(msg = 'priorities must be a symbol or an array/hash for directional priorities!')
10
+ super
11
+ end
12
+ end
13
+
14
+ class InvalidConversionLambda < StandardError
15
+ def initialize
16
+ super('conversion lambdas may take only 0 or 1 arguments!')
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,35 @@
1
+ module Priora
2
+ class Priority
3
+ DIRECTION_DESC = :desc
4
+ DIRECTION_ASC = :asc
5
+
6
+ DIRECTIONAL_METHODS = {
7
+ DIRECTION_DESC => :+,
8
+ DIRECTION_ASC => :-
9
+ }
10
+
11
+ attr_reader :attribute, :direction
12
+
13
+ def initialize(attribute:, direction: DIRECTION_DESC)
14
+ @attribute = attribute
15
+ @direction = direction
16
+ end
17
+
18
+ def comparable_value_from(object)
19
+ raw_value = object.send(@attribute)
20
+ numeric_value = numeric_value_from(raw_value)
21
+ directional_value_from(numeric_value)
22
+ end
23
+
24
+ private
25
+
26
+ def numeric_value_from(raw_value)
27
+ conversion_lambda = Priora.configuration.conversion_lambda_for(raw_value.class)
28
+ conversion_lambda ? conversion_lambda.call(raw_value) : raw_value
29
+ end
30
+
31
+ def directional_value_from(numeric_value)
32
+ 0.send(DIRECTIONAL_METHODS[@direction], numeric_value)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,36 @@
1
+ require_relative 'priority'
2
+
3
+ module Priora
4
+ class PriorityBuilder
5
+ class << self
6
+ def build(raw_priority)
7
+ case raw_priority
8
+ when Symbol
9
+ Priority.new(attribute: raw_priority)
10
+ when Array
11
+ priority_hash = raw_priority.reduce(:merge)
12
+ build_from_hash(priority_hash)
13
+ when Hash
14
+ build_from_hash(raw_priority)
15
+ else
16
+ raise InvalidPrioritySyntaxError
17
+ end
18
+ rescue => e
19
+ raise InvalidPrioritySyntaxError
20
+ end
21
+
22
+ private
23
+
24
+ def build_from_hash(priority_hash)
25
+ unless priority_hash.one?
26
+ raise InvalidPrioritySyntaxError, 'directional priority declaration takes only a single priority at a time!'
27
+ end
28
+
29
+ attribute = priority_hash.keys.first
30
+ direction = priority_hash[attribute]
31
+
32
+ Priority.new(attribute: attribute, direction: direction)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,3 @@
1
+ module Priora
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1,26 @@
1
+
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'priora/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'priora'
8
+ spec.version = Priora::VERSION
9
+ spec.authors = ['Eliav Lavi']
10
+ spec.email = ['eliavlavi@gmail.com']
11
+
12
+ spec.summary = %q{An object prioritization helper}
13
+ spec.description = %q{Priora helps in prioritizing a collection of objects according to your needs.}
14
+ spec.homepage = 'http://www.github.com/eliavlavi/priora'
15
+ spec.license = 'MIT'
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = 'exe'
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_development_dependency 'bundler', '~> 1.16'
24
+ spec.add_development_dependency 'rake', '~> 10.0'
25
+ spec.add_development_dependency 'rspec', '~> 3.0'
26
+ end
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: priora
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Eliav Lavi
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-09-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description: Priora helps in prioritizing a collection of objects according to your
56
+ needs.
57
+ email:
58
+ - eliavlavi@gmail.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".gitignore"
64
+ - ".rspec"
65
+ - ".travis.yml"
66
+ - Gemfile
67
+ - Gemfile.lock
68
+ - LICENSE.txt
69
+ - README.md
70
+ - Rakefile
71
+ - bin/console
72
+ - bin/setup
73
+ - lib/priora.rb
74
+ - lib/priora/class_methods.rb
75
+ - lib/priora/collection.rb
76
+ - lib/priora/configuration.rb
77
+ - lib/priora/errors.rb
78
+ - lib/priora/priority.rb
79
+ - lib/priora/priority_builder.rb
80
+ - lib/priora/version.rb
81
+ - priora.gemspec
82
+ homepage: http://www.github.com/eliavlavi/priora
83
+ licenses:
84
+ - MIT
85
+ metadata: {}
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubyforge_project:
102
+ rubygems_version: 2.6.14
103
+ signing_key:
104
+ specification_version: 4
105
+ summary: An object prioritization helper
106
+ test_files: []