inferno_core 0.3.2 → 0.3.5

Sign up to get free protection for your applications and to get access to all the features.
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]