Module: Inferno::DSL::Runnable

Includes:
Utils::MarkdownFormatter
Included in:
Entities::TestGroup, Entities::TestSuite
Defined in:
lib/inferno/dsl/runnable.rb

Overview

This module contains the DSL for defining child entities in the test definition framework.

Constant Summary collapse

VARIABLES_NOT_TO_COPY =

Class instance variables are used to hold the metadata for Runnable classes. When inheriting from a Runnable class, these class instance variables need to be copied. Some instance variables should not be copied, and will need to be repopulated from scratch on the new class. Any child Runnable classes will themselves need to be subclassed so that their parent can be updated.

[
  :@id, # New runnable will have a different id
  :@database_id, # New runnable will have a different database_id
  :@parent, # New runnable unlikely to have the same parent
  :@all_children, # New subclasses have to be made for each child
  :@test_count, # Needs to be recalculated
  :@config, # Needs to be set by calling .config, which does extra work
  :@available_inputs, # Needs to be recalculated
  :@children_available_inputs # Needs to be recalculated
].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Utils::MarkdownFormatter

#format_markdown

Instance Attribute Details

#parentObject

Returns the value of attribute parent.



12
13
14
# File 'lib/inferno/dsl/runnable.rb', line 12

def parent
  @parent
end

#suite_option_requirementsObject (readonly)

Returns the value of attribute suite_option_requirements.



13
14
15
# File 'lib/inferno/dsl/runnable.rb', line 13

def suite_option_requirements
  @suite_option_requirements
end

Class Method Details

.extended(extending_class) ⇒ Object

When a class (e.g. TestSuite/TestGroup) uses this module, set it up so that subclassing it works correctly.

  • add the subclass to the relevant repository when it is created

  • copy the class instance variables from the superclass

  • add a hook to the subclass so that its subclasses do the same



23
24
25
26
27
28
29
30
31
# File 'lib/inferno/dsl/runnable.rb', line 23

def self.extended(extending_class)
  super
  extending_class.extend Configurable
  extending_class.extend InputOutputHandling

  extending_class.define_singleton_method(:inherited) do |subclass|
    copy_instance_variables(subclass)
  end
end

Instance Method Details

#add_self_to_repositoryObject



81
82
83
# File 'lib/inferno/dsl/runnable.rb', line 81

def add_self_to_repository
  repository.insert(self)
end

#all_childrenObject



330
331
332
# File 'lib/inferno/dsl/runnable.rb', line 330

def all_children
  @all_children ||= []
end

#all_descendantsObject



335
336
337
# File 'lib/inferno/dsl/runnable.rb', line 335

def all_descendants
  children.flat_map { |child| [child] + child.all_descendants }
end

#all_verified_requirements(suite_options = []) ⇒ Object



559
560
561
562
563
# File 'lib/inferno/dsl/runnable.rb', line 559

def all_verified_requirements(suite_options = [])
  verifies_requirements + children(suite_options).flat_map do |child|
    child.all_verified_requirements(suite_options)
  end.uniq
end

#block(&block) ⇒ Proc Also known as: run

Set/Get the block that is executed when a runnable is run

Parameters:

  • block (Proc)

Returns:

  • (Proc)

    the block that is executed when a runnable is run



321
322
323
324
325
# File 'lib/inferno/dsl/runnable.rb', line 321

def block(&block)
  return @block unless block_given?

  @block = block
end

#child_metadata(metadata = nil) ⇒ Object



136
137
138
139
# File 'lib/inferno/dsl/runnable.rb', line 136

def ( = nil)
  @child_metadata =  if 
  @child_metadata
end

#children(selected_suite_options = []) ⇒ Object



544
545
546
547
548
549
550
551
552
553
554
555
556
# File 'lib/inferno/dsl/runnable.rb', line 544

def children(selected_suite_options = [])
  return all_children if selected_suite_options.blank?

  all_children.select do |child|
    requirements = child.suite_option_requirements

    if requirements.blank?
      true
    else
      requirements.all? { |requirement| selected_suite_options.include? requirement }
    end
  end
end

#configure_child_class(klass, hash_args) ⇒ Object



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/inferno/dsl/runnable.rb', line 155

def configure_child_class(klass, hash_args) # rubocop:disable Metrics/CyclomaticComplexity
  inputs.each do |name|
    next if klass.inputs.any? { |klass_input_name| klass_input_name == name }

    klass.input name
  end

  outputs.each do |output_name|
    next if klass.outputs.include? output_name

    klass.output output_name
  end

  klass.config(config)

  klass.all_children.select!(&:required?) if hash_args.delete(:exclude_optional)

  hash_args.each do |key, value|
    if value.is_a? Array
      klass.send(key, *value)
    else
      klass.send(key, value)
    end
  end

  klass.all_children.each do |child_class|
    klass.configure_child_class(child_class, {})
    child_class.add_self_to_repository
  end
end

#copy_instance_variables(subclass) ⇒ Object



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/inferno/dsl/runnable.rb', line 64

def copy_instance_variables(subclass)
  instance_variables
    .reject { |variable| VARIABLES_NOT_TO_COPY.include? variable }
    .each { |variable| subclass.instance_variable_set(variable, deep_dup(instance_variable_get(variable))) }

  subclass.config(config)

  new_children = all_children.map do |child|
    Class.new(child).tap do |subclass_child|
      subclass_child.parent = subclass
    end
  end

  subclass.instance_variable_set(:@all_children, new_children)
end

#create_child_class(hash_args) ⇒ Object



142
143
144
145
146
147
148
149
150
151
152
# File 'lib/inferno/dsl/runnable.rb', line 142

def create_child_class(hash_args)
  superclass_id = hash_args.delete :from

  return Class.new([:class]) if superclass_id.blank?

  superclass = [:repo].find(superclass_id)

  raise Exceptions::ParentNotLoadedException.new([:class], superclass_id) unless superclass

  Class.new(superclass)
end

#database_idObject



210
211
212
# File 'lib/inferno/dsl/runnable.rb', line 210

def database_id
  @database_id ||= id
end

#deep_dup(value) ⇒ Object

Recursively duplicate arrays/hashes to prevent them from being shared across different runnables



53
54
55
56
57
58
59
60
61
# File 'lib/inferno/dsl/runnable.rb', line 53

def deep_dup(value)
  if value.is_a? Array
    value.map { |element| deep_dup(element) }
  elsif value.is_a? Hash
    value.transform_values { |element| deep_dup(element) }
  else
    value.dup
  end
end

#default_idObject



313
314
315
# File 'lib/inferno/dsl/runnable.rb', line 313

def default_id
  to_s
end

#define_child(*args) ⇒ Object

This method defines a child entity. Classes using this module should alias the method name they wish to use to define child entities to this method.



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/inferno/dsl/runnable.rb', line 101

def define_child(*args, &)
  hash_args = process_args(args)

  klass = create_child_class(hash_args)

  klass.parent = self

  all_children << klass

  configure_child_class(klass, hash_args)

  handle_child_definition_block(klass, &)

  klass.add_self_to_repository

  klass
end

#description(new_description = nil) ⇒ String

Set/Get a runnable’s description

Parameters:

  • new_description (String) (defaults to: nil)

Returns:

  • (String)

    the description



238
239
240
241
242
# File 'lib/inferno/dsl/runnable.rb', line 238

def description(new_description = nil)
  return @description if new_description.nil?

  @description = format_markdown(new_description)
end

#handle_child_definition_block(klass) ⇒ Object



187
188
189
# File 'lib/inferno/dsl/runnable.rb', line 187

def handle_child_definition_block(klass, &)
  klass.class_eval(&) if block_given?
end

#id(new_id = nil) ⇒ String, Symbol

Set/Get a runnable’s id

Parameters:

  • new_id (String, Symbol) (defaults to: nil)

Returns:

  • (String, Symbol)

    the id



195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/inferno/dsl/runnable.rb', line 195

def id(new_id = nil)
  return @id if new_id.nil? && @id.present?

  prefix = parent ? "#{parent.id}-" : ''
  @base_id = new_id || @base_id || default_id
  @id = "#{prefix}#{@base_id}"

  if @id.length > 255
    hash = Digest::SHA1.hexdigest(@id)[0...10]
    @database_id = "#{@id[0...244]}-#{hash}"
  end

  @id
end

#input_instructions(new_input_instructions = nil) ⇒ String

Set/Get a runnable’s input instructions

Parameters:

  • new_input_instructions (String) (defaults to: nil)

Returns:

  • (String)

    the input instructions



258
259
260
261
262
# File 'lib/inferno/dsl/runnable.rb', line 258

def input_instructions(new_input_instructions = nil)
  return @input_instructions if new_input_instructions.nil?

  @input_instructions = format_markdown(new_input_instructions)
end

#inspectObject



566
567
568
569
570
571
572
573
574
# File 'lib/inferno/dsl/runnable.rb', line 566

def inspect
  non_dynamic_ancestor = ancestors.find { |ancestor| !ancestor.to_s.start_with? '#' }
  "#<#{non_dynamic_ancestor}".tap do |inspect_string|
    inspect_string.concat(" @id=#{id.inspect},")
    inspect_string.concat(" @short_id=#{short_id.inspect},") if respond_to? :short_id
    inspect_string.concat(" @title=#{title.inspect}")
    inspect_string.concat('>')
  end
end

#optional(optional = true) ⇒ void

This method returns an undefined value.

Mark as optional. Tests are required by default.

Parameters:

  • optional (Boolean) (defaults to: true)


283
284
285
# File 'lib/inferno/dsl/runnable.rb', line 283

def optional(optional = true) # rubocop:disable Style/OptionalBooleanParameter
  @optional = optional
end

#optional?Boolean

The test or group is optional if true

Returns:

  • (Boolean)


301
302
303
# File 'lib/inferno/dsl/runnable.rb', line 301

def optional?
  !!@optional
end

#process_args(args) ⇒ Object



120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/inferno/dsl/runnable.rb', line 120

def process_args(args)
  hash_args =
    if args[0].is_a? Hash
      args[0]
    elsif args[1].is_a? Hash
      args[1]
    else
      {}
    end

  hash_args[:title] = args[0] if args[0].is_a? String

  hash_args
end

#remove(id_to_remove) ⇒ Object

Remove a child test/group

Examples:

test from: :test1
test from: :test2
test from: :test3

remove :test2

Parameters:

  • id_to_remove (Symbol, String)


537
538
539
540
541
# File 'lib/inferno/dsl/runnable.rb', line 537

def remove(id_to_remove)
  removed = children.select { |child| child.id.to_s.end_with? id_to_remove.to_s }
  children.reject! { |child| child.id.to_s.end_with? id_to_remove.to_s }
  removed.each(&:remove_self_from_repository)
end

#remove_self_from_repositoryObject



86
87
88
89
# File 'lib/inferno/dsl/runnable.rb', line 86

def remove_self_from_repository
  repository.remove(self)
  children.each(&:remove_self_from_repository)
end

#reorder(child_id, new_index) ⇒ Object

Move a child test/group to a new position within the children list.

Examples:

reorder(:test3, 1) # Moves `test3` to index 1

Parameters:

  • child_id (Symbol, String)

    The ID of the child to be moved.

  • new_index (Integer)

    The new position for the child.

Raises:



485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
# File 'lib/inferno/dsl/runnable.rb', line 485

def reorder(child_id, new_index)
  index = children.find_index { |child| child.id.to_s.end_with? child_id.to_s }
  raise Exceptions::RunnableChildNotFoundException.new(child_id, self) unless index

  unless new_index.between?(0, children.length - 1)
    Inferno::Application[:logger].error <<~ERROR
      Error trying to reorder children for #{self}:
      new_index #{new_index} for #{child_id} is out of range
      (must be between 0 and #{children.length - 1})
    ERROR
    return
  end

  child = children.delete_at(index)
  children.insert(new_index, child)
end

#replace(id_to_replace, replacement_id) {|Inferno::TestGroup, Inferno::Test| ... } ⇒ Object

Replace a child test/group

Examples:

replace :test2, :test4 do
  id :new_test_id
  config(...)
end

Parameters:

  • id_to_replace (Symbol, String)

    The ID of the child to be replaced.

  • replacement_id (Symbol, String)

    The global ID of the group/test that will take the place of the child being replaced.

Yields:

Raises:



514
515
516
517
518
519
520
521
522
523
524
525
526
# File 'lib/inferno/dsl/runnable.rb', line 514

def replace(id_to_replace, replacement_id, &)
  index = children.find_index { |child| child.id.to_s.end_with? id_to_replace.to_s }
  raise Exceptions::RunnableChildNotFoundException.new(id_to_replace, self) unless index

  if children[index] < Inferno::TestGroup
    group(from: replacement_id, &)
  else
    test(from: replacement_id, &)
  end

  remove(id_to_replace)
  children.insert(index, children.pop)
end

#repositoryObject

An instance of the repository for the class using this module



93
94
95
# File 'lib/inferno/dsl/runnable.rb', line 93

def repository
  nil
end

#required(required = true) ⇒ void

This method returns an undefined value.

Mark as required

Tests are required by default. This method is provided to make an existing optional test required.

Parameters:

  • required (Boolean) (defaults to: true)


294
295
296
# File 'lib/inferno/dsl/runnable.rb', line 294

def required(required = true) # rubocop:disable Style/OptionalBooleanParameter
  @optional = !required
end

#required?Boolean

The test or group is required if true

Returns:

  • (Boolean)


308
309
310
# File 'lib/inferno/dsl/runnable.rb', line 308

def required?
  !optional?
end

#required_suite_options(suite_option_requirements) ⇒ void

This method returns an undefined value.

Set/get suite options required for this runnable to be executed.

Examples:

suite_option :ig_version,
            list_options: [
              {
                label: 'IG v1',
                value: 'ig_v1'
              },
              {
                label: 'IG v2',
                value: 'ig_v2'
              }
            ]

group from: :ig_v1_group,
      required_suite_options: { ig_version: 'ig_v1' }

group from: :ig_v2_group do
  required_suite_options ig_version: 'ig_v2'
end

Parameters:

  • suite_option_requirements (Hash)


471
472
473
474
475
476
# File 'lib/inferno/dsl/runnable.rb', line 471

def required_suite_options(suite_option_requirements)
  @suite_option_requirements =
    suite_option_requirements.map do |key, value|
      DSL::SuiteOption.new(id: key, value:)
    end
end

#resume_test_route(method, path, tags: [], result: 'pass') { ... } ⇒ void

This method returns an undefined value.

Create a route which will resume a test run when a request is received

Examples:

resume_test_route :get, '/launch', tags: ['launch'] do
  request.query_parameters['iss']
end

test do
  input :issuer
  receives_request :launch

  run do
    wait(
      identifier: issuer,
      message: "Wating to receive a request with an issuer of #{issuer}"
    )
  end
end

Parameters:

  • method (Symbol)

    the HTTP request type (:get, :post, etc.) for the incoming request

  • path (String)

    the path for this request. The route will be served with a prefix of ‘/custom/TEST_SUITE_ID` to prevent path conflicts. [Any of the path options available in Hanami Router](github.com/hanami/router/tree/f41001d4c3ee9e2d2c7bb142f74b43f8e1d3a265#a-beautiful-dsl) can be used here.

  • tags (Array<String>) (defaults to: [])

    a list of tags to assign to the request

  • result (String) (defaults to: 'pass')

    the result for the waiting test. Must be one of: ‘pass’, ‘fail’, ‘skip’, ‘omit’, ‘cancel’

Yields:

  • This method takes a block which must return the identifier defined when a test was set to wait for the test run that hit this route. The block has access to the ‘request` method which returns a Entities::Request object with the information for the incoming request.

See Also:



382
383
384
385
386
387
388
389
390
# File 'lib/inferno/dsl/runnable.rb', line 382

def resume_test_route(method, path, tags: [], result: 'pass', &block)
  route_class = Class.new(ResumeTestRoute) do |klass|
    klass.singleton_class.instance_variable_set(:@test_run_identifier_block, block)
    klass.singleton_class.instance_variable_set(:@tags, tags)
    klass.singleton_class.instance_variable_set(:@result, result)
  end

  route(method, path, route_class)
end

#route(method, path, handler) ⇒ void

This method returns an undefined value.

Create a route to handle a request

Parameters:



424
425
426
# File 'lib/inferno/dsl/runnable.rb', line 424

def route(method, path, handler)
  Inferno.routes << { method:, path:, handler:, suite: }
end

#short_description(new_short_description = nil) ⇒ String

Set/Get a runnable’s short one-sentence description

Parameters:

  • new_short_description (String) (defaults to: nil)

Returns:

  • (String)

    the one-sentence description



248
249
250
251
252
# File 'lib/inferno/dsl/runnable.rb', line 248

def short_description(new_short_description = nil)
  return @short_description if new_short_description.nil?

  @short_description = format_markdown(new_short_description)
end

#short_title(new_short_title = nil) ⇒ String

Set/Get a runnable’s short title

Parameters:

  • new_short_title (String) (defaults to: nil)

Returns:

  • (String)

    the short title



228
229
230
231
232
# File 'lib/inferno/dsl/runnable.rb', line 228

def short_title(new_short_title = nil)
  return @short_title if new_short_title.nil?

  @short_title = new_short_title
end

#suiteObject



340
341
342
343
344
# File 'lib/inferno/dsl/runnable.rb', line 340

def suite
  return self if ancestors.include? Inferno::Entities::TestSuite

  parent.suite
end

#suite_endpoint(method, path, endpoint_class) ⇒ void

This method returns an undefined value.

Create an endpoint to receive incoming requests during a Test Run.

Examples:

suite_endpoint :post, '/my_suite_endpoint', MySuiteEndpoint

Parameters:

  • method (Symbol)

    the HTTP request type (:get, :post, etc.) for the incoming request

  • path (String)

    the path for this request. The route will be served with a prefix of ‘/custom/TEST_SUITE_ID` to prevent path conflicts. [Any of the path options available in Hanami Router](github.com/hanami/router/tree/f41001d4c3ee9e2d2c7bb142f74b43f8e1d3a265#a-beautiful-dsl) can be used here.

  • a (Class)

    subclass of Inferno::DSL::SuiteEndpoint

See Also:



406
407
408
# File 'lib/inferno/dsl/runnable.rb', line 406

def suite_endpoint(method, path, endpoint_class)
  route(method, path, endpoint_class)
end

#test_count(selected_suite_options = []) ⇒ Object



429
430
431
432
433
434
435
436
437
438
439
# File 'lib/inferno/dsl/runnable.rb', line 429

def test_count(selected_suite_options = [])
  @test_counts ||= {}

  options_json = selected_suite_options.to_json

  return @test_counts[options_json] if @test_counts[options_json]

  @test_counts[options_json] =
    children(selected_suite_options)
      &.reduce(0) { |sum, child| sum + child.test_count(selected_suite_options) } || 0
end

#title(new_title = nil) ⇒ String

Set/Get a runnable’s title

Parameters:

  • new_title (String) (defaults to: nil)

Returns:

  • (String)

    the title



218
219
220
221
222
# File 'lib/inferno/dsl/runnable.rb', line 218

def title(new_title = nil)
  return @title if new_title.nil?

  @title = new_title
end

#user_runnable?Boolean

Returns:

  • (Boolean)


442
443
444
445
446
# File 'lib/inferno/dsl/runnable.rb', line 442

def user_runnable?
  @user_runnable ||= parent.nil? ||
                     !parent.respond_to?(:run_as_group?) ||
                     (parent.user_runnable? && !parent.run_as_group?)
end

#verifies_requirements(*requirement_ids) ⇒ Array<String>

Set/Get the IDs of requirements verified by this runnable Set with [] to clear the list

Parameters:

  • requirements (Array<String>)

Returns:

  • (Array<String>)

    the requirement IDs



269
270
271
272
273
274
275
276
277
# File 'lib/inferno/dsl/runnable.rb', line 269

def verifies_requirements(*requirement_ids)
  if requirement_ids.empty?
    @requirement_ids || []
  elsif requirement_ids == [[]]
    @requirement_ids = []
  else
    @requirement_ids = requirement_ids
  end
end