inferno_core 0.3.2 → 0.3.5

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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/lib/inferno/apps/web/controllers/test_sessions/create.rb +7 -3
  3. data/lib/inferno/apps/web/index.html.erb +6 -1
  4. data/lib/inferno/apps/web/router.rb +9 -2
  5. data/lib/inferno/apps/web/serializers/suite_option.rb +13 -0
  6. data/lib/inferno/apps/web/serializers/test_group.rb +6 -2
  7. data/lib/inferno/apps/web/serializers/test_session.rb +5 -3
  8. data/lib/inferno/apps/web/serializers/test_suite.rb +4 -1
  9. data/lib/inferno/config/boot/db.rb +1 -1
  10. data/lib/inferno/config/boot.rb +1 -1
  11. data/lib/inferno/db/migrations/007_add_suite_options.rb +5 -0
  12. data/lib/inferno/db/schema.rb +1 -0
  13. data/lib/inferno/dsl/configurable.rb +1 -1
  14. data/lib/inferno/dsl/fhir_client.rb +22 -10
  15. data/lib/inferno/dsl/fhir_validation.rb +35 -12
  16. data/lib/inferno/dsl/http_client.rb +47 -35
  17. data/lib/inferno/dsl/input_output_handling.rb +22 -2
  18. data/lib/inferno/dsl/runnable.rb +36 -10
  19. data/lib/inferno/dsl/suite_option.rb +40 -0
  20. data/lib/inferno/dsl/tcp_exception_handler.rb +11 -0
  21. data/lib/inferno/entities/test.rb +7 -3
  22. data/lib/inferno/entities/test_group.rb +4 -4
  23. data/lib/inferno/entities/test_session.rb +40 -1
  24. data/lib/inferno/entities/test_suite.rb +12 -14
  25. data/lib/inferno/public/bundle.js +15 -15
  26. data/lib/inferno/repositories/test_sessions.rb +24 -0
  27. data/lib/inferno/test_runner.rb +9 -3
  28. data/lib/inferno/version.rb +1 -1
  29. data/spec/support/factory_bot.rb +6 -0
  30. metadata +23 -7
  31. data/lib/inferno/public/bg-header-1920x170.png +0 -0
  32. data/lib/inferno/public/healthit.gov.logo.png +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d44d2d4f0afb51b463a128ce7725183159791fe21464c2fa28b10b3b919c37ab
4
- data.tar.gz: b62acf63e2bb524bcb22048e4e4d5b433a727c6a7a8201ae8161237fc4dbcfb3
3
+ metadata.gz: 40fa8983d93a7fc191a7d03bdb78c0bd80e138cfe5245d377c15f28360fb6b03
4
+ data.tar.gz: 4aa592411c216c13bc55ebcef30ce2019522cc3575edc184fcb10593eb28239a
5
5
  SHA512:
6
- metadata.gz: 7658c22a6d5595f39a36589a853992420b31717c6002ce0262eabcfcabe4def7c97de8f8fa5a52ad785a7e4c6b25fb471282584bdbfde266ed6388ee8dfc7b8b
7
- data.tar.gz: 9972288e75af701edc3f70457835384f6f51ad5e64d14bfaf4b5ec0855786ca51455e79067de007f44964bbecfc9b84b1a862b6468a72464026152c41acb5536
6
+ metadata.gz: bb0f7e9aafced3a1181cd40d8638f4ac07fefc0239ab771b85ce22aaec3785b7328ca82969cd83b1617051201fa149272ba80a53a05789129e6ee35302cf3a1b
7
+ data.tar.gz: 5f14e0db2c0a2d3238198600a736790ea549ea11c91bb84578dda2e3de2270c31cb314b44972ec2fbc6635aa77b93f0ce4df2926d45c072745b19a1d6371b34f
@@ -3,9 +3,13 @@ module Inferno
3
3
  module Controllers
4
4
  module TestSessions
5
5
  class Create < Controller
6
- PARAMS = [:test_suite_id].freeze
6
+ PARAMS = [:test_suite_id, :suite_options].freeze
7
+
8
+ def call(raw_params)
9
+ query_params = raw_params.to_h
10
+ body_params = JSON.parse(request.body.string).symbolize_keys
11
+ params = query_params.merge(body_params)
7
12
 
8
- def call(params)
9
13
  session = repo.create(create_params(params))
10
14
 
11
15
  repo.apply_preset(session.id, params[:preset_id]) if params[:preset_id].present?
@@ -21,7 +25,7 @@ module Inferno
21
25
  end
22
26
 
23
27
  def create_params(params)
24
- params.to_h.slice(*PARAMS)
28
+ params.slice(*PARAMS)
25
29
  end
26
30
  end
27
31
  end
@@ -29,6 +29,11 @@
29
29
  Learn how to configure a non-root public URL by running `npm run build`.
30
30
  -->
31
31
  <style>
32
+ @media print {
33
+ .no-print {
34
+ display: none;
35
+ }
36
+ }
32
37
  .wrapper {
33
38
  height: 100%;
34
39
  display: flex;
@@ -49,7 +54,7 @@
49
54
  <noscript>You need to enable JavaScript to run this app.</noscript>
50
55
  <div class='wrapper'>
51
56
  <% if File.exist? (File.join(Dir.pwd, 'config', 'banner.html.erb')) %>
52
- <div class='banner'><%= ERB.new(File.read(File.join(Dir.pwd, 'config', 'banner.html.erb'))).result %></div>
57
+ <div class='banner no-print'><%= ERB.new(File.read(File.join(Dir.pwd, 'config', 'banner.html.erb'))).result %></div>
53
58
  <% end %>
54
59
  <div class='app' id="root"></div>
55
60
  </div>
@@ -36,8 +36,10 @@ module Inferno
36
36
  get '/version', to: ->(_env) { [200, {}, [{ 'version' => Inferno::VERSION.to_s }.to_json]] }, as: :api_version
37
37
  end
38
38
 
39
- get '/', to: ->(_env) { [200, {}, [client_page]] }
40
- get '/test_sessions/:id', to: ->(_env) { [200, {}, [client_page]] }
39
+ # Should not need Content-Type header but GitHub Codespaces will not work without them.
40
+ # This could be investigated and likely removed if addressed properly elsewhere.
41
+ get '/', to: ->(_env) { [200, { 'Content-Type' => 'text/html' }, [client_page]] }
42
+ get '/test_sessions/:id', to: ->(_env) { [200, { 'Content-Type' => 'text/html' }, [client_page]] }
41
43
 
42
44
  Inferno.routes.each do |route|
43
45
  cleaned_id = route[:suite].id.gsub(/[^a-zA-Z\d\-._~]/, '_')
@@ -49,6 +51,11 @@ module Inferno
49
51
  send(route[:method], path, to: route[:handler])
50
52
  end
51
53
  end
54
+
55
+ Inferno::Repositories::TestSuites.all.map { |suite| "/#{suite.id}" }.each do |suite_path|
56
+ Application['logger'].info("Registering suite route: #{suite_path}")
57
+ get suite_path, to: ->(_env) { [200, {}, [client_page]] }
58
+ end
52
59
  end
53
60
  end
54
61
  end
@@ -0,0 +1,13 @@
1
+ module Inferno
2
+ module Web
3
+ module Serializers
4
+ class SuiteOption < Serializer
5
+ identifier :id
6
+ field :title, if: :field_present?
7
+ field :description, if: :field_present?
8
+ field :list_options, if: :field_present?
9
+ field :value, if: :field_present?
10
+ end
11
+ end
12
+ end
13
+ end
@@ -15,8 +15,12 @@ module Inferno
15
15
  field :user_runnable?, name: :user_runnable
16
16
  field :optional?, name: :optional
17
17
 
18
- association :groups, name: :test_groups, blueprint: TestGroup
19
- association :tests, blueprint: Test
18
+ field :test_groups do |group, options|
19
+ TestGroup.render_as_hash(group.groups(options[:suite_options]))
20
+ end
21
+ field :tests do |group, options|
22
+ Test.render_as_hash(group.tests(options[:suite_options]))
23
+ end
20
24
  field :available_inputs, name: :inputs, extractor: HashValueExtractor, blueprint: Input
21
25
  field :output_definitions, name: :outputs, extractor: HashValueExtractor
22
26
  end
@@ -1,3 +1,4 @@
1
+ require_relative 'suite_option'
1
2
  require_relative 'test_suite'
2
3
 
3
4
  module Inferno
@@ -7,10 +8,11 @@ module Inferno
7
8
  identifier :id
8
9
 
9
10
  field :test_suite_id
11
+ association :suite_options, blueprint: SuiteOption
10
12
 
11
- association :test_suite, blueprint: TestSuite, view: :full
12
- # association :test_runs, blueprint: TestRun
13
- # association :results, blueprint: Result
13
+ field :test_suite do |session, _options|
14
+ TestSuite.render_as_hash(session.test_suite, view: :full, suite_options: session.suite_options)
15
+ end
14
16
  end
15
17
  end
16
18
  end
@@ -11,12 +11,15 @@ module Inferno
11
11
  field :input_instructions
12
12
  field :test_count
13
13
  field :version
14
+ association :suite_options, blueprint: SuiteOption
14
15
  association :presets, view: :summary, blueprint: Preset
15
16
  end
16
17
 
17
18
  view :full do
18
19
  include_view :summary
19
- association :groups, name: :test_groups, blueprint: TestGroup
20
+ field :test_groups do |suite, options|
21
+ TestGroup.render_as_hash(suite.groups(options[:suite_options]))
22
+ end
20
23
  field :configuration_messages
21
24
  field :available_inputs, name: :inputs, extractor: HashValueExtractor, blueprint: Input
22
25
  end
@@ -11,7 +11,7 @@ Inferno::Application.boot(:db) do
11
11
 
12
12
  config_path = File.expand_path('database.yml', File.join(Dir.pwd, 'config'))
13
13
  config_contents = ERB.new(File.read(config_path)).result
14
- config = YAML.safe_load(config_contents)[ENV['APP_ENV']]
14
+ config = YAML.safe_load(config_contents)[ENV.fetch('APP_ENV', nil)]
15
15
  .merge(logger: Inferno::Application['logger'])
16
16
  connection_attempts_remaining = ENV.fetch('MAX_DB_CONNECTION_ATTEMPTS', '10').to_i
17
17
  connection_retry_delay = ENV.fetch('DB_CONNECTION_RETRY_DELAY', '5').to_i
@@ -4,4 +4,4 @@ ENV['APP_ENV'] ||= 'development'
4
4
 
5
5
  root_path = Dir.pwd
6
6
 
7
- Dotenv.load(File.join(root_path, '.env'), File.join(root_path, ".env.#{ENV['APP_ENV']}"))
7
+ Dotenv.load(File.join(root_path, '.env'), File.join(root_path, ".env.#{ENV.fetch('APP_ENV', nil)}"))
@@ -0,0 +1,5 @@
1
+ Sequel.migration do
2
+ change do
3
+ add_column :test_sessions, :suite_options, String, text: true, size: 255
4
+ end
5
+ end
@@ -9,6 +9,7 @@ Sequel.migration do
9
9
  String :test_suite_id, :size=>255, :null=>false
10
10
  DateTime :created_at, :null=>false
11
11
  DateTime :updated_at, :null=>false
12
+ String :suite_options, :text=>true
12
13
 
13
14
  primary_key [:id]
14
15
  end
@@ -16,7 +16,7 @@ module Inferno
16
16
 
17
17
  @config.apply(new_configuration)
18
18
 
19
- children.each { |child| child.config(new_configuration) }
19
+ all_children.each { |child| child.config(new_configuration) }
20
20
 
21
21
  @config
22
22
  end
@@ -1,4 +1,5 @@
1
1
  require_relative 'request_storage'
2
+ require_relative 'tcp_exception_handler'
2
3
 
3
4
  module Inferno
4
5
  module DSL
@@ -40,6 +41,7 @@ module Inferno
40
41
  klass.extend ClassMethods
41
42
  klass.extend Forwardable
42
43
  klass.include RequestStorage
44
+ klass.include TCPExceptionHandler
43
45
 
44
46
  klass.def_delegators 'self.class', :profile_url, :validator_url
45
47
  end
@@ -73,11 +75,13 @@ module Inferno
73
75
  # @return [Inferno::Entities::Request]
74
76
  def fhir_operation(path, body: nil, client: :default, name: nil, headers: {})
75
77
  store_request_and_refresh_token(fhir_client(client), name) do
76
- operation_headers = fhir_client(client).fhir_headers
77
- operation_headers.merge!('Content-Type' => 'application/fhir+json') if body.present?
78
- operation_headers.merge!(headers) if headers.present?
78
+ tcp_exception_handler do
79
+ operation_headers = fhir_client(client).fhir_headers
80
+ operation_headers.merge!('Content-Type' => 'application/fhir+json') if body.present?
81
+ operation_headers.merge!(headers) if headers.present?
79
82
 
80
- fhir_client(client).send(:post, path, body, operation_headers)
83
+ fhir_client(client).send(:post, path, body, operation_headers)
84
+ end
81
85
  end
82
86
  end
83
87
 
@@ -89,8 +93,10 @@ module Inferno
89
93
  # @return [Inferno::Entities::Request]
90
94
  def fhir_get_capability_statement(client: :default, name: nil)
91
95
  store_request_and_refresh_token(fhir_client(client), name) do
92
- fhir_client(client).conformance_statement
93
- fhir_client(client).reply
96
+ tcp_exception_handler do
97
+ fhir_client(client).conformance_statement
98
+ fhir_client(client).reply
99
+ end
94
100
  end
95
101
  end
96
102
 
@@ -104,7 +110,9 @@ module Inferno
104
110
  # @return [Inferno::Entities::Request]
105
111
  def fhir_read(resource_type, id, client: :default, name: nil)
106
112
  store_request_and_refresh_token(fhir_client(client), name) do
107
- fhir_client(client).read(fhir_class_from_resource_type(resource_type), id)
113
+ tcp_exception_handler do
114
+ fhir_client(client).read(fhir_class_from_resource_type(resource_type), id)
115
+ end
108
116
  end
109
117
  end
110
118
 
@@ -126,8 +134,10 @@ module Inferno
126
134
  end
127
135
 
128
136
  store_request_and_refresh_token(fhir_client(client), name) do
129
- fhir_client(client)
130
- .search(fhir_class_from_resource_type(resource_type), { search: search })
137
+ tcp_exception_handler do
138
+ fhir_client(client)
139
+ .search(fhir_class_from_resource_type(resource_type), { search: search })
140
+ end
131
141
  end
132
142
  end
133
143
 
@@ -141,7 +151,9 @@ module Inferno
141
151
  # @return [Inferno::Entities::Request]
142
152
  def fhir_delete(resource_type, id, client: :default, name: nil)
143
153
  store_request('outgoing', name) do
144
- fhir_client(client).destroy(fhir_class_from_resource_type(resource_type), id)
154
+ tcp_exception_handler do
155
+ fhir_client(client).destroy(fhir_class_from_resource_type(resource_type), id)
156
+ end
145
157
  end
146
158
  end
147
159
 
@@ -1,5 +1,4 @@
1
1
  require_relative '../ext/fhir_models'
2
-
3
2
  module Inferno
4
3
  module DSL
5
4
  # This module contains the methods needed to configure a validator to
@@ -39,13 +38,16 @@ module Inferno
39
38
  # Find a particular validator. Looks through a runnable's parents up to
40
39
  # the suite to find a validator with a particular name
41
40
  def find_validator(validator_name)
42
- self.class.find_validator(validator_name)
41
+ self.class.find_validator(validator_name, suite_options)
43
42
  end
44
43
 
45
44
  class Validator
45
+ attr_reader :requirements
46
+
46
47
  # @private
47
- def initialize(&block)
48
+ def initialize(requirements = nil, &block)
48
49
  instance_eval(&block)
50
+ @requirements = requirements
49
51
  end
50
52
 
51
53
  # @private
@@ -115,7 +117,7 @@ module Inferno
115
117
 
116
118
  outcome = FHIR::OperationOutcome.new(JSON.parse(validate(resource, profile_url)))
117
119
 
118
- message_hashes = outcome.issue&.map { |issue| message_hash_from_issue(issue) } || []
120
+ message_hashes = outcome.issue&.map { |issue| message_hash_from_issue(issue, resource) } || []
119
121
 
120
122
  message_hashes.concat(additional_validation_messages(resource, profile_url))
121
123
 
@@ -132,10 +134,10 @@ module Inferno
132
134
  end
133
135
 
134
136
  # @private
135
- def message_hash_from_issue(issue)
137
+ def message_hash_from_issue(issue, resource)
136
138
  {
137
139
  type: issue_severity(issue),
138
- message: issue_message(issue)
140
+ message: issue_message(issue, resource)
139
141
  }
140
142
  end
141
143
 
@@ -152,14 +154,16 @@ module Inferno
152
154
  end
153
155
 
154
156
  # @private
155
- def issue_message(issue)
157
+ def issue_message(issue, resource)
156
158
  location = if issue.respond_to?(:expression)
157
159
  issue.expression&.join(', ')
158
160
  else
159
161
  issue.location&.join(', ')
160
162
  end
161
163
 
162
- "#{location}: #{issue&.details&.text}"
164
+ location_prefix = resource.id ? "#{resource.resourceType}/#{resource.id}" : resource.resourceType
165
+
166
+ "#{location_prefix}: #{location}: #{issue&.details&.text}"
163
167
  end
164
168
 
165
169
  # Post a resource to the validation service for validating.
@@ -198,14 +202,33 @@ module Inferno
198
202
  #
199
203
  # @param name [Symbol] the name of the validator, only needed if you are
200
204
  # using multiple validators
201
- def validator(name = :default, &block)
202
- fhir_validators[name] = Inferno::DSL::FHIRValidation::Validator.new(&block)
205
+ # @param required_suite_options [Hash] suite options that must be
206
+ # selected in order to use this validator
207
+ def validator(name = :default, required_suite_options: nil, &block)
208
+ current_validators = fhir_validators[name] || []
209
+
210
+ new_validator = Inferno::DSL::FHIRValidation::Validator.new(required_suite_options, &block)
211
+
212
+ current_validators.reject! { |validator| validator.requirements == required_suite_options }
213
+ current_validators << new_validator
214
+
215
+ fhir_validators[name] = current_validators
203
216
  end
204
217
 
205
218
  # Find a particular validator. Looks through a runnable's parents up to
206
219
  # the suite to find a validator with a particular name
207
- def find_validator(validator_name)
208
- validator = fhir_validators[validator_name] || parent&.find_validator(validator_name)
220
+ def find_validator(validator_name, selected_suite_options = nil)
221
+ validators = fhir_validators[validator_name] ||
222
+ Array.wrap(parent&.find_validator(validator_name, selected_suite_options))
223
+
224
+ validator =
225
+ if selected_suite_options.present?
226
+ validators.find do |possible_validator|
227
+ possible_validator.requirements.nil? || selected_suite_options >= possible_validator.requirements
228
+ end
229
+ else
230
+ validators.first
231
+ end
209
232
 
210
233
  raise Exceptions::ValidatorNotFoundException, validator_name if validator.nil?
211
234
 
@@ -1,4 +1,5 @@
1
1
  require_relative 'request_storage'
2
+ require_relative 'tcp_exception_handler'
2
3
 
3
4
  module Inferno
4
5
  module DSL
@@ -31,6 +32,7 @@ module Inferno
31
32
  def self.included(klass)
32
33
  klass.extend ClassMethods
33
34
  klass.include RequestStorage
35
+ klass.include TCPExceptionHandler
34
36
  end
35
37
 
36
38
  # Return a previously defined HTTP client
@@ -44,7 +46,9 @@ module Inferno
44
46
  definition = self.class.http_client_definitions[client]
45
47
  return nil if definition.nil?
46
48
 
47
- http_clients[client] = HTTPClientBuilder.new.build(self, definition)
49
+ tcp_exception_handler do
50
+ http_clients[client] = HTTPClientBuilder.new.build(self, definition)
51
+ end
48
52
  end
49
53
 
50
54
  # @private
@@ -65,14 +69,16 @@ module Inferno
65
69
  # @return [Inferno::Entities::Request]
66
70
  def get(url = '', client: :default, name: nil, **options)
67
71
  store_request('outgoing', name) do
68
- client = http_client(client)
69
-
70
- if client
71
- client.get(url, nil, options[:headers])
72
- elsif url.match?(%r{\Ahttps?://})
73
- Faraday.get(url, nil, options[:headers])
74
- else
75
- raise StandardError, 'Must use an absolute url or define an HTTP client with a base url'
72
+ tcp_exception_handler do
73
+ client = http_client(client)
74
+
75
+ if client
76
+ client.get(url, nil, options[:headers])
77
+ elsif url.match?(%r{\Ahttps?://})
78
+ Faraday.get(url, nil, options[:headers])
79
+ else
80
+ raise StandardError, 'Must use an absolute url or define an HTTP client with a base url'
81
+ end
76
82
  end
77
83
  end
78
84
  end
@@ -91,14 +97,16 @@ module Inferno
91
97
  # @return [Inferno::Entities::Request]
92
98
  def post(url = '', body: nil, client: :default, name: nil, **options)
93
99
  store_request('outgoing', name) do
94
- client = http_client(client)
95
-
96
- if client
97
- client.post(url, body, options[:headers])
98
- elsif url.match?(%r{\Ahttps?://})
99
- Faraday.post(url, body, options[:headers])
100
- else
101
- raise StandardError, 'Must use an absolute url or define an HTTP client with a base url'
100
+ tcp_exception_handler do
101
+ client = http_client(client)
102
+
103
+ if client
104
+ client.post(url, body, options[:headers])
105
+ elsif url.match?(%r{\Ahttps?://})
106
+ Faraday.post(url, body, options[:headers])
107
+ else
108
+ raise StandardError, 'Must use an absolute url or define an HTTP client with a base url'
109
+ end
102
110
  end
103
111
  end
104
112
  end
@@ -114,14 +122,16 @@ module Inferno
114
122
  # @return [Inferno::Entities::Request]
115
123
  def delete(url = '', client: :default, name: :nil, **options)
116
124
  store_request('outgoing', name) do
117
- client = http_client(client)
118
-
119
- if client
120
- client.delete(url, nil, options[:headers])
121
- elsif url.match?(%r{\Ahttps?://})
122
- Faraday.delete(url, nil, options[:headers])
123
- else
124
- raise StandardError, 'Must use an absolute url or define an HTTP client with a base url'
125
+ tcp_exception_handler do
126
+ client = http_client(client)
127
+
128
+ if client
129
+ client.delete(url, nil, options[:headers])
130
+ elsif url.match?(%r{\Ahttps?://})
131
+ Faraday.delete(url, nil, options[:headers])
132
+ else
133
+ raise StandardError, 'Must use an absolute url or define an HTTP client with a base url'
134
+ end
125
135
  end
126
136
  end
127
137
  end
@@ -151,17 +161,19 @@ module Inferno
151
161
  end
152
162
 
153
163
  store_request('outgoing', name) do
154
- client = http_client(client)
155
-
156
- if client
157
- response = client.get(url, nil, options[:headers]) { |req| req.options.on_data = collector }
158
- elsif url.match?(%r{\Ahttps?://})
159
- response = Faraday.get(url, nil, options[:headers]) { |req| req.options.on_data = collector }
160
- else
161
- raise StandardError, 'Must use an absolute url or define an HTTP client with a base url'
164
+ tcp_exception_handler do
165
+ client = http_client(client)
166
+
167
+ if client
168
+ response = client.get(url, nil, options[:headers]) { |req| req.options.on_data = collector }
169
+ elsif url.match?(%r{\Ahttps?://})
170
+ response = Faraday.get(url, nil, options[:headers]) { |req| req.options.on_data = collector }
171
+ else
172
+ raise StandardError, 'Must use an absolute url or define an HTTP client with a base url'
173
+ end
174
+ response.env.body = streamed.join
175
+ response
162
176
  end
163
- response.env.body = streamed.join
164
- response
165
177
  end
166
178
  end
167
179
 
@@ -24,10 +24,20 @@ module Inferno
24
24
  [identifier, *other_identifiers].compact.each do |input_identifier|
25
25
  inputs << input_identifier
26
26
  config.add_input(input_identifier)
27
+ children
28
+ .reject { |child| child.inputs.include? input_identifier }
29
+ .each do |child|
30
+ child.input(input_identifier)
31
+ end
27
32
  end
28
33
  else
29
34
  inputs << identifier
30
35
  config.add_input(identifier, input_params)
36
+ children
37
+ .reject { |child| child.inputs.include? identifier }
38
+ .each do |child|
39
+ child.input(identifier, **input_params)
40
+ end
31
41
  end
32
42
  end
33
43
 
@@ -47,10 +57,20 @@ module Inferno
47
57
  [identifier, *other_identifiers].compact.each do |output_identifier|
48
58
  outputs << output_identifier
49
59
  config.add_output(output_identifier)
60
+ children
61
+ .reject { |child| child.outputs.include? output_identifier }
62
+ .each do |child|
63
+ child.output(output_identifier)
64
+ end
50
65
  end
51
66
  else
52
67
  outputs << identifier
53
68
  config.add_output(identifier, output_definition)
69
+ children
70
+ .reject { |child| child.outputs.include? identifier }
71
+ .each do |child|
72
+ child.output(identifier, **output_definition)
73
+ end
54
74
  end
55
75
  end
56
76
 
@@ -125,7 +145,7 @@ module Inferno
125
145
  def all_outputs
126
146
  outputs
127
147
  .map { |output_identifier| config.output_name(output_identifier) }
128
- .concat(children.flat_map(&:all_outputs))
148
+ .concat(all_children.flat_map(&:all_outputs))
129
149
  .uniq
130
150
  end
131
151
 
@@ -137,7 +157,7 @@ module Inferno
137
157
  @children_available_inputs ||=
138
158
  begin
139
159
  child_outputs = []
140
- children.each_with_object({}) do |child, definitions|
160
+ all_children.each_with_object({}) do |child, definitions|
141
161
  new_definitions = child.available_inputs.map(&:dup)
142
162
  new_definitions.each do |input, new_definition|
143
163
  existing_definition = definitions[input]