simmer 2.0.0.pre.alpha.1 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2020-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module Simmer
11
+ class Configuration
12
+ # Defines lifecycle hooks which can be run before and after the entire
13
+ # suite or just a single test. Very similar to Rspec
14
+ # (https://relishapp.com/rspec/rspec-core/v/3-9/docs/hooks/before-and-after-hooks).
15
+ class CallbackDsl
16
+ def initialize
17
+ @before_suite = []
18
+ @after_suite = []
19
+ @before_each = []
20
+ @after_each = []
21
+
22
+ freeze
23
+ end
24
+
25
+ # Used to create a before callback. This accepts and optional level
26
+ # parameter which can either be :suite or :each. ":each" is implied if no
27
+ # level is provided.
28
+ def before(level = LEVEL_EACH, &block)
29
+ verify_level!(level)
30
+
31
+ level == LEVEL_SUITE ? before_suite.push(block) : before_each.push(block)
32
+ end
33
+
34
+ # Used to create an after callback. This accepts and optional level
35
+ # parameter which can either be :suite or :each. ":each" is implied if no
36
+ # level is provided.
37
+ def after(level = LEVEL_EACH, &block)
38
+ verify_level!(level)
39
+
40
+ level == LEVEL_SUITE ? after_suite.push(block) : after_each.push(block)
41
+ end
42
+
43
+ # :nodoc:
44
+ def run_single_test_with_callbacks
45
+ before_each.each(&:call)
46
+
47
+ result = yield
48
+
49
+ after_each.each { |block| block.call(result) }
50
+
51
+ result
52
+ end
53
+
54
+ # :nodoc:
55
+ def run_suite_with_callbacks
56
+ before_suite.each(&:call)
57
+
58
+ result = yield
59
+
60
+ after_suite.each { |block| block.call(result) }
61
+
62
+ result
63
+ end
64
+
65
+ private
66
+
67
+ def verify_level!(level)
68
+ raise ArgumentError, "unknown test level: #{level}" unless CALLBACK_LEVELS.include?(level)
69
+ end
70
+
71
+ attr_reader :after_each, :before_each, :after_suite, :before_suite
72
+
73
+ LEVEL_EACH = :each
74
+ LEVEL_SUITE = :suite
75
+ CALLBACK_LEVELS = Set.new([LEVEL_EACH, LEVEL_SUITE])
76
+ private_constant :LEVEL_EACH, :LEVEL_SUITE, :CALLBACK_LEVELS
77
+ end
78
+ end
79
+ end
@@ -13,6 +13,8 @@ module Simmer
13
13
  module Database
14
14
  # Hydrate a collection of Fixture instances from configuration.
15
15
  class FixtureSet
16
+ class FixtureMissingError < StandardError; end
17
+
16
18
  def initialize(config = {})
17
19
  @fixtures_by_name = config_to_fixures_by_name(config)
18
20
 
@@ -22,7 +24,7 @@ module Simmer
22
24
  def get!(name)
23
25
  key = name.to_s
24
26
 
25
- raise ArgumentError, "fixture not found: #{name}" unless fixtures_by_name.key?(key)
27
+ raise FixtureMissingError, "fixture missing: #{name}" unless fixtures_by_name.key?(key)
26
28
 
27
29
  fixtures_by_name[key]
28
30
  end
@@ -27,7 +27,7 @@ module Simmer
27
27
  end
28
28
 
29
29
  def records(table, columns = [])
30
- query = "SELECT #{sql_select_params(columns)} FROM #{table}"
30
+ query = "SELECT #{sql_select_params(columns)} FROM #{qualify(table)}"
31
31
 
32
32
  client.query(query).to_a
33
33
  end
@@ -53,7 +53,11 @@ module Simmer
53
53
  attr_reader :client, :fixture_set, :table_names
54
54
 
55
55
  def sql_select_params(columns)
56
- Array(columns).any? ? Array(columns).map { |c| client.escape(c) }.join(',') : '*'
56
+ if Array(columns).any?
57
+ Array(columns).map { |c| qualify(client.escape(c)).to_s }.join(',')
58
+ else
59
+ '*'
60
+ end
57
61
  end
58
62
 
59
63
  def seed_sql_statements(fixtures)
@@ -62,7 +66,7 @@ module Simmer
62
66
 
63
67
  def clean_sql_statements
64
68
  table_names.map do |table_name|
65
- "TRUNCATE #{table_name}"
69
+ "TRUNCATE #{qualify(table_name)}"
66
70
  end
67
71
  end
68
72
 
@@ -110,6 +114,10 @@ module Simmer
110
114
 
111
115
  raise ArgumentError, "database (#{name}) must end in #{DATABASE_SUFFIX}"
112
116
  end
117
+
118
+ def qualify(identifier)
119
+ "`#{identifier}`"
120
+ end
113
121
  end
114
122
  end
115
123
  end
@@ -24,13 +24,13 @@ module Simmer
24
24
  freeze
25
25
  end
26
26
 
27
- def run(specification, config)
27
+ def run(specification, config, &output_capturer)
28
28
  execution_result = nil
29
29
  time_in_seconds = nil
30
30
 
31
31
  begin
32
32
  time_in_seconds = Benchmark.measure do
33
- execution_result = execute!(specification, config)
33
+ execution_result = execute!(specification, config, &output_capturer)
34
34
  end.real
35
35
  rescue Pdi::Spoon::PanError, Pdi::Spoon::KitchenError => e
36
36
  return Result.new(
@@ -50,14 +50,15 @@ module Simmer
50
50
 
51
51
  attr_reader :files_dir
52
52
 
53
- def execute!(specification, config)
53
+ def execute!(specification, config, &output_capturer)
54
54
  act = specification.act
55
55
 
56
56
  spoon.run(
57
57
  repository: act.repository,
58
58
  name: act.name,
59
59
  params: act.compiled_params(files_dir, config),
60
- type: act.type
60
+ type: act.type,
61
+ &output_capturer
61
62
  )
62
63
  end
63
64
  end
@@ -13,7 +13,7 @@ module Simmer
13
13
  class Result
14
14
  attr_reader :bad_assertions
15
15
 
16
- def initialize(bad_assertions)
16
+ def initialize(bad_assertions = [])
17
17
  @bad_assertions = bad_assertions
18
18
 
19
19
  freeze
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2020-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require_relative 'judge'
11
+ require_relative 'runner/result'
12
+
13
+ module Simmer
14
+ # :nodoc:
15
+ # Wraps a <tt>Simmer::Runner</tt> and knows how to re-run tests based
16
+ # on certain failure cases.
17
+ class ReRunner < SimpleDelegator
18
+ attr_reader :timeout_failure_retry_count
19
+
20
+ def initialize(runner, out, timeout_failure_retry_count: 0)
21
+ @timeout_failure_retry_count = timeout_failure_retry_count.to_i
22
+ @out = out
23
+
24
+ super(runner)
25
+ end
26
+
27
+ def run(*args)
28
+ rerun_on_timeout(args, timeout_failure_retry_count)
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :out
34
+
35
+ def rerun_on_timeout(run_args, times)
36
+ result = __getobj__.run(*run_args)
37
+
38
+ if result.timed_out? && times.positive?
39
+ out.console_puts('Retrying due to a timeout...')
40
+ rerun_on_timeout(run_args, times - 1)
41
+ else
42
+ result
43
+ end
44
+ end
45
+ end
46
+ end
@@ -9,6 +9,7 @@
9
9
 
10
10
  require_relative 'judge'
11
11
  require_relative 'runner/result'
12
+ require_relative 'runner/timeout_error'
12
13
 
13
14
  module Simmer
14
15
  # Runs a single specification.
@@ -26,110 +27,109 @@ module Simmer
26
27
  freeze
27
28
  end
28
29
 
29
- def run(specification, config: {}, id: SecureRandom.uuid)
30
- print("Name: #{specification.name}")
31
- print("Path: #{specification.path}")
30
+ def run(specification, config:, id: SecureRandom.uuid)
31
+ out.announce_start(id, specification)
32
32
 
33
- clean_db
34
- seed_db(specification)
35
- clean_file_system
36
- seed_file_system(specification)
33
+ config.run_single_test_with_callbacks do
34
+ clean_and_seed(specification)
37
35
 
38
- spoon_client_result = execute_spoon(specification, config)
39
- judge_result = assert(specification, spoon_client_result)
36
+ spoon_client_result = execute_spoon(specification, config)
37
+ judge_result = assert(specification, spoon_client_result)
40
38
 
41
- Result.new(id, judge_result, specification, spoon_client_result).tap do |result|
42
- msg = pass_message(result)
43
- print_waiting('Done', 'Final verdict')
44
- print(msg)
39
+ Result.new(
40
+ id: id,
41
+ judge_result: judge_result,
42
+ specification: specification,
43
+ spoon_client_result: spoon_client_result
44
+ ).tap { |result| out.final_verdict(result) }
45
+ rescue Database::FixtureSet::FixtureMissingError, Simmer::Runner::TimeoutError => e
46
+ Result.new(id: id, specification: specification, errors: e)
47
+ .tap { |result| out.final_verdict(result) }
45
48
  end
46
49
  end
47
50
 
51
+ def complete
52
+ out.close
53
+ end
54
+
48
55
  private
49
56
 
50
57
  attr_reader :database, :file_system, :fixture_set, :judge, :out
51
58
 
59
+ def clean_and_seed(specification)
60
+ clean_db
61
+ seed_db(specification)
62
+ clean_file_system
63
+ seed_file_system(specification)
64
+ end
65
+
52
66
  def clean_db
53
- print_waiting('Stage', 'Cleaning database')
67
+ out.waiting('Stage', 'Cleaning database')
54
68
  count = database.clean!
55
- print("#{count} table(s) emptied")
69
+ out.console_puts("#{count} table(s) emptied")
56
70
 
57
71
  count
58
72
  end
59
73
 
60
74
  def seed_db(specification)
61
- print_waiting('Stage', 'Seeding database')
75
+ out.waiting('Stage', 'Seeding database')
62
76
 
63
77
  fixtures = specification.stage.fixtures.map { |f| fixture_set.get!(f) }
64
78
  count = database.seed!(fixtures)
65
79
 
66
- print("#{count} record(s) inserted")
80
+ out.console_puts("#{count} record(s) inserted")
67
81
 
68
82
  count
83
+ rescue Database::FixtureSet::FixtureMissingError => e
84
+ out.console_puts('Missing Fixture(s)')
85
+ raise e
69
86
  end
70
87
 
71
88
  def clean_file_system
72
- print_waiting('Stage', 'Cleaning File System')
89
+ out.waiting('Stage', 'Cleaning File System')
73
90
  count = file_system.clean!
74
- print("#{count} file(s) deleted")
91
+ out.console_puts("#{count} file(s) deleted")
75
92
 
76
93
  count
77
94
  end
78
95
 
79
96
  def seed_file_system(specification)
80
- print_waiting('Stage', 'Seeding File System')
97
+ out.waiting('Stage', 'Seeding File System')
81
98
  count = file_system.write!(specification.stage.files)
82
- print("#{count} file(s) uploaded")
99
+ out.console_puts("#{count} file(s) uploaded")
83
100
 
84
101
  count
85
102
  end
86
103
 
87
104
  def execute_spoon(specification, config)
88
- print_waiting('Act', 'Executing Spoon')
89
- spoon_client_result = spoon_client.run(specification, config)
90
- msg = pass_message(spoon_client_result)
91
- print(msg)
105
+ out.waiting('Act', 'Executing Spoon')
106
+
107
+ spoon_client_result = spoon_client.run(specification, config.config) do |output|
108
+ out.capture_spoon_output(output)
109
+ end
110
+
111
+ out.finish_spec
112
+ out.spoon_execution_detail_message(spoon_client_result)
92
113
 
93
114
  spoon_client_result
115
+ rescue Timeout::Error => e
116
+ out.console_puts('Timed out')
117
+ raise Simmer::Runner::TimeoutError, e
94
118
  end
95
119
 
96
120
  def assert(specification, spoon_client_result)
97
- print_waiting('Assert', 'Checking results')
121
+ out.waiting('Assert', 'Checking results')
98
122
 
99
123
  if spoon_client_result.fail?
100
- print('Skipped')
124
+ out.console_puts('Skipped')
101
125
  return nil
102
126
  end
103
127
 
104
128
  output = spoon_client_result.execution_result.out
105
129
  judge_result = judge.assert(specification, output)
106
- msg = pass_message(judge_result)
107
-
108
- print(msg)
130
+ out.result(judge_result)
109
131
 
110
132
  judge_result
111
133
  end
112
-
113
- def print(msg)
114
- out.puts(msg)
115
- end
116
-
117
- def print_waiting(stage, msg)
118
- max = 25
119
- char = '.'
120
- msg = " > #{pad_right(stage, 6)} - #{pad_right(msg, max, char)}"
121
-
122
- out.print(msg)
123
- end
124
-
125
- def pad_right(msg, len, char = ' ')
126
- missing = len - msg.length
127
-
128
- "#{msg}#{char * missing}"
129
- end
130
-
131
- def pass_message(obj)
132
- obj.pass? ? 'Pass' : 'Fail'
133
- end
134
134
  end
135
135
  end
@@ -13,29 +13,47 @@ module Simmer
13
13
  class Result
14
14
  extend Forwardable
15
15
 
16
- attr_reader :id, :judge_result, :specification, :spoon_client_result
17
-
18
- def_delegators :spoon_client_result, :time_in_seconds
16
+ attr_reader :errors, :id, :judge_result, :specification, :spoon_client_result
19
17
 
20
18
  def_delegators :specification, :name
21
19
 
22
- def initialize(id, judge_result, specification, spoon_client_result)
20
+ def initialize(
21
+ id:,
22
+ specification:,
23
+ judge_result: nil,
24
+ spoon_client_result: nil,
25
+ errors: []
26
+ )
23
27
  @id = id.to_s
24
28
  @judge_result = judge_result
25
29
  @specification = specification
26
30
  @spoon_client_result = spoon_client_result
31
+ @errors = Array(errors)
27
32
 
28
33
  freeze
29
34
  end
30
35
 
36
+ def time_in_seconds
37
+ spoon_client_result&.time_in_seconds || 0
38
+ end
39
+
31
40
  def pass?
32
- judge_result&.pass? && spoon_client_result&.pass?
41
+ [
42
+ judge_result&.pass?,
43
+ spoon_client_result&.pass?,
44
+ errors.empty?,
45
+ ].all?
33
46
  end
47
+ alias passing? pass?
34
48
 
35
49
  def fail?
36
50
  !pass?
37
51
  end
38
52
 
53
+ def timed_out?
54
+ errors.any? { |e| e.is_a?(Simmer::Runner::TimeoutError) }
55
+ end
56
+
39
57
  def to_h
40
58
  {
41
59
  'name' => specification.name,
@@ -44,7 +62,8 @@ module Simmer
44
62
  'time_in_seconds' => time_in_seconds,
45
63
  'pass' => pass?,
46
64
  'spoon_client_result' => spoon_client_result.to_h,
47
- 'judge_result' => judge_result.to_h
65
+ 'judge_result' => judge_result.to_h,
66
+ 'errors' => errors.map(&:message),
48
67
  }
49
68
  end
50
69
  end