alexa-rails 0.1.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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +165 -0
  4. data/Rakefile +36 -0
  5. data/app/assets/config/alexa_manifest.js +2 -0
  6. data/app/assets/javascripts/alexa/application.js +13 -0
  7. data/app/assets/stylesheets/alexa/application.css +15 -0
  8. data/app/controllers/alexa/application_controller.rb +6 -0
  9. data/app/controllers/alexa/intent_handlers_controller.rb +49 -0
  10. data/app/helpers/alexa/application_helper.rb +4 -0
  11. data/app/helpers/alexa/context_helper.rb +23 -0
  12. data/app/helpers/alexa/render_helper.rb +24 -0
  13. data/app/jobs/alexa/application_job.rb +4 -0
  14. data/app/mailers/alexa/application_mailer.rb +6 -0
  15. data/app/models/alexa/application_record.rb +5 -0
  16. data/app/models/alexa/usage.rb +11 -0
  17. data/app/models/alexa/user.rb +17 -0
  18. data/app/views/alexa/_response.json.erb +18 -0
  19. data/app/views/alexa/output/_card.json.erb +13 -0
  20. data/app/views/alexa/output/_ssml.json.erb +7 -0
  21. data/app/views/alexa/output/cards/_ask_for_permissions_consent.json.erb +6 -0
  22. data/app/views/alexa/output/cards/_simple.json.erb +5 -0
  23. data/app/views/alexa/permission_consents/device_address.text.erb +7 -0
  24. data/app/views/layouts/alexa/application.html.erb +14 -0
  25. data/config/routes.rb +3 -0
  26. data/lib/alexa/context.rb +38 -0
  27. data/lib/alexa/device.rb +61 -0
  28. data/lib/alexa/engine.rb +5 -0
  29. data/lib/alexa/intent_handlers/base.rb +158 -0
  30. data/lib/alexa/request.rb +97 -0
  31. data/lib/alexa/response.rb +85 -0
  32. data/lib/alexa/responses/bye.rb +26 -0
  33. data/lib/alexa/responses/delegate.rb +18 -0
  34. data/lib/alexa/responses/permission_consents/device_address.rb +28 -0
  35. data/lib/alexa/session.rb +35 -0
  36. data/lib/alexa/slot.rb +50 -0
  37. data/lib/alexa/version.rb +3 -0
  38. data/lib/alexa-rails.rb +19 -0
  39. data/lib/generators/alexa/migrations_generator.rb +22 -0
  40. data/lib/generators/alexa/templates/create_alexa_usages.rb +13 -0
  41. data/lib/generators/alexa/templates/create_alexa_users.rb +13 -0
  42. data/lib/tasks/alexa_tasks.rake +4 -0
  43. metadata +115 -0
@@ -0,0 +1,158 @@
1
+ module Alexa
2
+ module IntentHandlers
3
+ class Base
4
+ class << self
5
+ def inherited(subclass)
6
+ subclass.instance_variable_set("@_required_slot_names", _required_slot_names.clone)
7
+ end
8
+
9
+ # Lets you set +required_slot_names+ per subclass
10
+ #
11
+ # class IntentHandlers::NewIntent < IntentHandlers::Base
12
+ # required_slot_names :Function, :CareerLevel
13
+ # end
14
+ #
15
+ # handler = IntentHandlers::NewIntent.new
16
+ # handler.required_slot_names # => [:Function, :CareerLevel]
17
+
18
+ def _required_slot_names
19
+ @_required_slot_names ||= []
20
+ end
21
+
22
+ def required_slot_names(*names)
23
+ @_required_slot_names = names.map(&:to_s)
24
+ end
25
+ end
26
+
27
+ attr_accessor :context
28
+
29
+ def initialize(alexa_context)
30
+ @context = alexa_context
31
+ end
32
+
33
+ def request
34
+ context.request
35
+ end
36
+
37
+ def handle
38
+ raise "Override .handle"
39
+ end
40
+
41
+ def session
42
+ request.session
43
+ end
44
+
45
+ def response
46
+ @_response ||= Alexa::Response.new(intent: self)
47
+ end
48
+
49
+ def say_welcome?
50
+ @say_welcome == true
51
+ end
52
+
53
+ def slots
54
+ request.slots
55
+ end
56
+
57
+ def intent_usage_count
58
+ @_usage_count ||= context.user.usage_count_for(intent_name: request.intent_name)
59
+ end
60
+
61
+ def show_device_address_permission_consent_card?
62
+ @_show_device_address_permission_consent_card == true
63
+ end
64
+
65
+
66
+ protected
67
+
68
+ def has_all_slots?
69
+ empty_slots.empty?
70
+ end
71
+
72
+ def has_required_slots?
73
+ non_empty_slot_names = non_empty_slots.map { |_, s| s.name }
74
+ (required_slot_names - non_empty_slot_names).empty?
75
+ end
76
+
77
+ def empty_slots
78
+ @_empty_slots ||= slots.select { |name, slot| slot.value.nil? }
79
+ end
80
+
81
+ def non_empty_slots
82
+ @_non_empty_slots ||= slots.select { |name, slot| !slot.value.nil? }
83
+ end
84
+
85
+ def unmatched_slots
86
+ @_unmatched_slots ||= slots.select { |_, slot| slot.bad_match? }
87
+ end
88
+
89
+ def required_slot_names
90
+ self.class._required_slot_names
91
+ end
92
+
93
+ def intent_confirmed?
94
+ # TODO: Move this to Alexa::Request
95
+ request.params["request"]["intent"]["confirmationStatus"] == "CONFIRMED"
96
+ end
97
+
98
+ def intent_denied?
99
+ # TODO: Move this to Alexa::Request
100
+ request.params["request"]["intent"]["confirmationStatus"] == "DENIED"
101
+ end
102
+
103
+ def dialog_complete?
104
+ dialog_state == "COMPLETED"
105
+ end
106
+
107
+ def has_unmatched_slots?
108
+ unmatched_slots.any?
109
+ end
110
+
111
+ def dialog_state
112
+ request.dialog_state
113
+ end
114
+
115
+ def sorry_response
116
+ {
117
+ "version": "1.0",
118
+ sessionAttributes: session,
119
+ "response": {
120
+ "outputSpeech": {
121
+ "type": "PlainText",
122
+ "ssml": "Sorry! Looks like I couldn't process your request, may be try something else?"
123
+ },
124
+ "shouldEndSession": false
125
+ }
126
+ }
127
+ end
128
+
129
+ def show_device_address_permission_consent_card!
130
+ @_show_device_address_permission_consent_card = true
131
+ end
132
+
133
+ def device_address_permission_consent_response
134
+ @_device_address_permission_consent_response ||= Alexa::Responses::PermissionConsents::DeviceAddress.new(intent: self)
135
+ end
136
+
137
+ def delegate_response
138
+ @_delegate_response ||= Alexa::Responses::Delegate.new
139
+ end
140
+
141
+ def bye_response
142
+ @_bye_response ||= Alexa::Responses::Bye.new(intent: self)
143
+ end
144
+
145
+ def say_welcome!
146
+ @say_welcome = true
147
+ end
148
+
149
+ def video_supported?
150
+ context.device.video_supported?
151
+ end
152
+
153
+ def audio_supported?
154
+ context.device.audio_supported?
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,97 @@
1
+ module Alexa
2
+ class Request
3
+ attr_accessor :body, :params
4
+
5
+ def initialize(request)
6
+ @req = request
7
+ @body = request.body
8
+ @params = if request.body.size > 0
9
+ request.body.rewind
10
+ JSON.parse(request.body.read).with_indifferent_access
11
+ else
12
+ {}
13
+ end
14
+ end
15
+
16
+ def application_id
17
+ session.application_id
18
+ end
19
+
20
+ def user_id
21
+ session.user_id
22
+ end
23
+
24
+ def type
25
+ params["request"]["type"]
26
+ end
27
+
28
+ def intent_request?
29
+ type == "IntentRequest"
30
+ end
31
+
32
+ def launch_request?
33
+ type == "LaunchRequest"
34
+ end
35
+
36
+ def session_ended_request?
37
+ type == "SessionEndedRequest"
38
+ end
39
+
40
+ def help_request?
41
+ intent_request? && intent_name == "AMAZON.HelpIntent"
42
+ end
43
+
44
+ def cancel_request?
45
+ intent_request? && intent_name == "AMAZON.CancelIntent"
46
+ end
47
+
48
+ def session
49
+ @_session ||= Alexa::Session.new(params["session"].dup)
50
+ end
51
+
52
+ def slots
53
+ @_slots ||= begin
54
+ if intent_request?
55
+ return [] if help_request?
56
+ return [] if cancel_request?
57
+
58
+ params["request"]["intent"]["slots"]
59
+ .inject(HashWithIndifferentAccess.new) do |hash, slot|
60
+ name = slot[0]
61
+ data = slot[1]
62
+ hash[name] = Alexa::Slot.new(data)
63
+ hash
64
+ end
65
+ else
66
+ []
67
+ end
68
+ end
69
+ end
70
+
71
+ def dialog_state
72
+ params["request"]["dialogState"]
73
+ end
74
+
75
+ def valid?
76
+ Rails.configuration.x.alexa.skill_ids.include?(application_id)
77
+ end
78
+
79
+ def intent_name
80
+ return nil if !intent_request?
81
+ params["request"]["intent"]["name"]
82
+ end
83
+
84
+ def locale
85
+ request_locale = params["request"]["locale"]
86
+ case params["request"]["locale"]
87
+ when "en-US", "en-GB", "en-IN", 'en-CA'
88
+ # this is to match pjpp locale codes
89
+ params["request"]["locale"].gsub("-", "_").downcase
90
+ when "de-DE"
91
+ "de"
92
+ else
93
+ "en_gb"
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,85 @@
1
+ module Alexa
2
+ class Response
3
+ attr_accessor :intent, :directives
4
+
5
+ def initialize(intent:, directives: [])
6
+ @intent = intent
7
+ @directives = directives
8
+ @slots_to_not_render_elicitation = []
9
+ end
10
+
11
+ # Marks a slot for elicitation.
12
+ #
13
+ # Options:
14
+ # - skip_render: Lets you skip the rendering of the elicited slot's view.
15
+ # Helpful when you have the elication text already in the
16
+ # response and don't wanna override it.
17
+ def elicit_slot!(slot_to_elicit, skip_render: false)
18
+ directives << {
19
+ type: "Dialog.ElicitSlot",
20
+ slotToElicit: slot_to_elicit
21
+ }
22
+
23
+ if skip_render
24
+ @slots_to_not_render_elicitation << slot_to_elicit
25
+ end
26
+ end
27
+
28
+ def partial_path(format: :ssml, filename: nil)
29
+ if elicit_directives.any?
30
+ slot_to_elicit = elicit_directives.first[:slotToElicit]
31
+ end
32
+
33
+ template_path = "alexa/#{intent.context.locale}/intent_handlers/"\
34
+ "#{intent.class.name.demodulize.underscore}"
35
+
36
+ if filename.present?
37
+ if format == :ssml
38
+ "#{template_path}/#{filename}.ssml.erb"
39
+ else
40
+ "#{template_path}/#{filename}.text.erb"
41
+ end
42
+ else
43
+ if intent.show_device_address_permission_consent_card? && format == :text
44
+ return Alexa::Responses::PermissionConsents::DeviceAddress.new(
45
+ intent: intent
46
+ ).partial_path(format: :text)
47
+ end
48
+
49
+ if slot_to_elicit.present? && !@slots_to_not_render_elicitation.include?(slot_to_elicit)
50
+ if format == :ssml
51
+ "#{template_path}/elicitations/#{slot_to_elicit.underscore}.ssml.erb"
52
+ else
53
+ "#{template_path}/elicitations/#{slot_to_elicit.underscore}.text.erb"
54
+ end
55
+ else
56
+ if format == :ssml
57
+ "#{template_path}/speech.ssml.erb"
58
+ else
59
+ "#{template_path}/display.text.erb"
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def elicit_directives
66
+ return [] if directives.empty?
67
+ directives.select { |directive| directive[:type] == "Dialog.ElicitSlot" }
68
+ end
69
+
70
+ def keep_listening!
71
+ @keep_listening = true
72
+ self
73
+ end
74
+
75
+ def keep_listening?
76
+ @keep_listening == true
77
+ end
78
+
79
+ def end_session?
80
+ return false if keep_listening?
81
+ return false if elicit_directives.any?
82
+ return true
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,26 @@
1
+ module Alexa
2
+ module Responses
3
+ class Bye < Alexa::Response
4
+ attr_accessor :intent, :directives
5
+
6
+ def initialize(intent:, directives: [])
7
+ @intent = intent
8
+ end
9
+
10
+ def partial_path(format: :ssml)
11
+ template_path = "alexa/#{intent.context.locale}/intent_handlers/"\
12
+ "#{intent.class.name.demodulize.underscore}"
13
+
14
+ if format == :ssml
15
+ "#{template_path}/bye.ssml.erb"
16
+ else
17
+ "#{template_path}/bye.text.erb"
18
+ end
19
+ end
20
+
21
+ def end_session?
22
+ true
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,18 @@
1
+ module Alexa
2
+ module Responses
3
+ class Delegate
4
+ def as_json(options={})
5
+ {
6
+ "version": "1.0",
7
+ "response": {
8
+ "directives": [
9
+ {
10
+ "type": "Dialog.Delegate"
11
+ }
12
+ ]
13
+ }
14
+ }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,28 @@
1
+ module Alexa
2
+ module Responses
3
+ module PermissionConsents
4
+ class DeviceAddress < Alexa::Response
5
+ attr_accessor :intent, :directives
6
+
7
+ def initialize(intent:, directives: [])
8
+ @intent = intent
9
+ end
10
+
11
+ def partial_path(format: :ssml)
12
+ if format == :ssml
13
+ "alexa/#{intent.context.locale}/intent_handlers/"\
14
+ "#{intent.class.name.demodulize.underscore}"\
15
+ "/permission_consents/"\
16
+ "device_address.ssml.erb"
17
+ else
18
+ "alexa/permission_consents/device_address.text.erb"
19
+ end
20
+ end
21
+
22
+ def end_session?
23
+ true
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,35 @@
1
+ module Alexa
2
+ class Session < HashWithIndifferentAccess
3
+ def initialize(session={})
4
+ @variables = session
5
+ super(session["attributes"])
6
+ end
7
+
8
+ def elicitation_count_for(slot_name)
9
+ self["elicitations"] ||= {}
10
+ if self["elicitations"][slot_name].present?
11
+ return self["elicitations"][slot_name]["count"]
12
+ end
13
+ return 0
14
+ end
15
+
16
+ def increment_elicitation_count!(slot_name)
17
+ count = elicitation_count_for(slot_name)
18
+ self["elicitations"].merge!(
19
+ "#{slot_name}": { count: count + 1 }
20
+ )
21
+ end
22
+
23
+ def new?
24
+ @variables["new"] == true
25
+ end
26
+
27
+ def application_id
28
+ @variables["application"]["applicationId"]
29
+ end
30
+
31
+ def user_id
32
+ @variables["user"]["userId"]
33
+ end
34
+ end
35
+ end
data/lib/alexa/slot.rb ADDED
@@ -0,0 +1,50 @@
1
+ module Alexa
2
+ class Slot
3
+ attr_accessor :name, :value
4
+
5
+ def initialize(attributes={})
6
+ @name = attributes['name']
7
+ @value = attributes['value']
8
+ @resolutions = attributes['resolutions']
9
+ end
10
+
11
+ def matched_value
12
+ if matched?
13
+ resolution["values"].first["value"]["name"]
14
+ end
15
+ end
16
+
17
+ def matched_id
18
+ if matched?
19
+ resolution["values"].first["value"]["id"]
20
+ end
21
+ end
22
+
23
+ def matched?
24
+ return false if !has_resolutions?
25
+ resolution["status"]["code"] == "ER_SUCCESS_MATCH"
26
+ end
27
+
28
+ def bad_match?
29
+ return false if !has_resolutions?
30
+ resolution["status"]["code"] == "ER_SUCCESS_NO_MATCH"
31
+ end
32
+
33
+ def has_resolutions?
34
+ @resolutions.present?
35
+ end
36
+
37
+ def resolution
38
+ return nil if !has_resolutions?
39
+ @resolutions["resolutionsPerAuthority"].first
40
+ end
41
+
42
+ def as_json(options={})
43
+ h = { name: name }
44
+ # add other attributes only if they are present
45
+ h.merge!(value: value) if value.present?
46
+ h.merge!(resolutions: @resolutions) if @resolutions.present?
47
+ h
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,3 @@
1
+ module Alexa
2
+ VERSION = '0.1.1'
3
+ end
@@ -0,0 +1,19 @@
1
+ require "alexa/engine"
2
+ require 'alexa/context'
3
+ require 'alexa/device'
4
+ require 'alexa/request'
5
+ require 'alexa/response'
6
+ require 'alexa/session'
7
+ require 'alexa/slot'
8
+ require 'alexa/version'
9
+ require 'alexa/responses/bye'
10
+ require 'alexa/responses/delegate'
11
+ require 'alexa/responses/permission_consents/device_address'
12
+ require 'alexa/intent_handlers/base'
13
+ require_relative '../app/models/alexa/user'
14
+ require_relative '../app/models/alexa/usage'
15
+
16
+
17
+ module Alexa
18
+ # Your code goes here...
19
+ end
@@ -0,0 +1,22 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+
4
+ module Alexa
5
+ module Generators
6
+ class MigrationsGenerator < ::Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+ source_root File.expand_path('../templates', __FILE__)
9
+ desc "Add the migrations for Alexa"
10
+
11
+ def self.next_migration_number(path)
12
+ next_migration_number = current_migration_number(path) + 1
13
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
14
+ end
15
+
16
+ def copy_migrations
17
+ migration_template "create_alexa_users.rb", "db/migrate/create_alexa_users.rb"
18
+ migration_template "create_alexa_usages.rb", "db/migrate/create_alexa_usages.rb"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ class CreateAlexaUsages < ActiveRecord::Migration[4.2]
2
+ def up
3
+ create_table :alexa_usages do |t|
4
+ t.references :alexa_user, foreign_key: true
5
+ t.string :intent_name
6
+ t.integer :count, default: 0
7
+ end
8
+ end
9
+
10
+ def down
11
+ drop_table :alexa_usages
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ class CreateAlexaUsers < ActiveRecord::Migration[4.2]
2
+ def up
3
+ create_table :alexa_users do |t|
4
+ t.string :amazon_id
5
+ end
6
+ add_index :alexa_users, :amazon_id
7
+ end
8
+
9
+ def down
10
+ remove_index :alexa_users, :amazon_id
11
+ drop_table :alexa_users
12
+ end
13
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :alexa do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: alexa-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Sri Vishnu Totakura
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-03-19 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.1.4
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 5.1.4
27
+ - !ruby/object:Gem::Dependency
28
+ name: pg
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
+ description: The gem adds additional capabilities to your rails app to serve as a
42
+ backend for your Alexa skill by providing routing, controllers, views and structure
43
+ to easily develop and maintain skills' backend
44
+ email:
45
+ - srivishnu@totakura.in
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - MIT-LICENSE
51
+ - README.md
52
+ - Rakefile
53
+ - app/assets/config/alexa_manifest.js
54
+ - app/assets/javascripts/alexa/application.js
55
+ - app/assets/stylesheets/alexa/application.css
56
+ - app/controllers/alexa/application_controller.rb
57
+ - app/controllers/alexa/intent_handlers_controller.rb
58
+ - app/helpers/alexa/application_helper.rb
59
+ - app/helpers/alexa/context_helper.rb
60
+ - app/helpers/alexa/render_helper.rb
61
+ - app/jobs/alexa/application_job.rb
62
+ - app/mailers/alexa/application_mailer.rb
63
+ - app/models/alexa/application_record.rb
64
+ - app/models/alexa/usage.rb
65
+ - app/models/alexa/user.rb
66
+ - app/views/alexa/_response.json.erb
67
+ - app/views/alexa/output/_card.json.erb
68
+ - app/views/alexa/output/_ssml.json.erb
69
+ - app/views/alexa/output/cards/_ask_for_permissions_consent.json.erb
70
+ - app/views/alexa/output/cards/_simple.json.erb
71
+ - app/views/alexa/permission_consents/device_address.text.erb
72
+ - app/views/layouts/alexa/application.html.erb
73
+ - config/routes.rb
74
+ - lib/alexa-rails.rb
75
+ - lib/alexa/context.rb
76
+ - lib/alexa/device.rb
77
+ - lib/alexa/engine.rb
78
+ - lib/alexa/intent_handlers/base.rb
79
+ - lib/alexa/request.rb
80
+ - lib/alexa/response.rb
81
+ - lib/alexa/responses/bye.rb
82
+ - lib/alexa/responses/delegate.rb
83
+ - lib/alexa/responses/permission_consents/device_address.rb
84
+ - lib/alexa/session.rb
85
+ - lib/alexa/slot.rb
86
+ - lib/alexa/version.rb
87
+ - lib/generators/alexa/migrations_generator.rb
88
+ - lib/generators/alexa/templates/create_alexa_usages.rb
89
+ - lib/generators/alexa/templates/create_alexa_users.rb
90
+ - lib/tasks/alexa_tasks.rake
91
+ homepage: https://github.com/tsrivishnu/alexa-rails
92
+ licenses:
93
+ - MIT
94
+ metadata: {}
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubyforge_project:
111
+ rubygems_version: 2.5.2.1
112
+ signing_key:
113
+ specification_version: 4
114
+ summary: Serve Alexa skills with your rails app as backend
115
+ test_files: []