simmer 1.0.0.pre.alpha.2

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 (57) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +8 -0
  3. data/.gitignore +7 -0
  4. data/.rubocop.yml +25 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +25 -0
  7. data/CHANGELOG.md +1 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +5 -0
  10. data/Guardfile +16 -0
  11. data/LICENSE +7 -0
  12. data/README.md +5 -0
  13. data/Rakefile +17 -0
  14. data/bin/console +18 -0
  15. data/bin/simmer +11 -0
  16. data/lib/simmer.rb +50 -0
  17. data/lib/simmer/configuration.rb +90 -0
  18. data/lib/simmer/core_ext/hash.rb +21 -0
  19. data/lib/simmer/externals.rb +12 -0
  20. data/lib/simmer/externals/aws_file_system.rb +81 -0
  21. data/lib/simmer/externals/mysql_database.rb +114 -0
  22. data/lib/simmer/externals/mysql_database/sql_fixture.rb +43 -0
  23. data/lib/simmer/externals/spoon_client.rb +77 -0
  24. data/lib/simmer/externals/spoon_client/mock.rb +39 -0
  25. data/lib/simmer/externals/spoon_client/result.rb +48 -0
  26. data/lib/simmer/judge.rb +38 -0
  27. data/lib/simmer/judge/result.rb +34 -0
  28. data/lib/simmer/runner.rb +125 -0
  29. data/lib/simmer/runner/result.rb +52 -0
  30. data/lib/simmer/session.rb +79 -0
  31. data/lib/simmer/session/reporter.rb +100 -0
  32. data/lib/simmer/session/result.rb +45 -0
  33. data/lib/simmer/specification.rb +34 -0
  34. data/lib/simmer/specification/act.rb +48 -0
  35. data/lib/simmer/specification/act/params.rb +55 -0
  36. data/lib/simmer/specification/assert.rb +27 -0
  37. data/lib/simmer/specification/assert/assertions.rb +27 -0
  38. data/lib/simmer/specification/assert/assertions/bad_output_assertion.rb +34 -0
  39. data/lib/simmer/specification/assert/assertions/bad_table_assertion.rb +38 -0
  40. data/lib/simmer/specification/assert/assertions/output.rb +37 -0
  41. data/lib/simmer/specification/assert/assertions/table.rb +47 -0
  42. data/lib/simmer/specification/stage.rb +28 -0
  43. data/lib/simmer/specification/stage/input_file.rb +33 -0
  44. data/lib/simmer/suite.rb +73 -0
  45. data/lib/simmer/util.rb +13 -0
  46. data/lib/simmer/util/evaluator.rb +32 -0
  47. data/lib/simmer/util/fixture.rb +32 -0
  48. data/lib/simmer/util/fixture_set.rb +41 -0
  49. data/lib/simmer/util/record.rb +54 -0
  50. data/lib/simmer/util/record_set.rb +51 -0
  51. data/lib/simmer/util/resolver.rb +28 -0
  52. data/lib/simmer/util/yaml_reader.rb +61 -0
  53. data/lib/simmer/version.rb +12 -0
  54. data/simmer.gemspec +38 -0
  55. data/spec/simmer/core_ext/hash_spec.rb +16 -0
  56. data/spec/spec_helper.rb +22 -0
  57. metadata +284 -0
@@ -0,0 +1,114 @@
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 'mysql_database/sql_fixture'
11
+
12
+ module Simmer
13
+ module Externals
14
+ # Provides a wrapper around mysql2 for Simmer.
15
+ class MysqlDatabase
16
+ DATABASE_SUFFIX = 'test'
17
+
18
+ def initialize(config, fixture_set)
19
+ config = (config || {}).symbolize_keys
20
+
21
+ database = config[:database].to_s
22
+ assert_database_name(database)
23
+
24
+ @client = Mysql2::Client.new(config)
25
+ @fixture_set = fixture_set
26
+ exclude_tables = Array(config[:exclude_tables]).map(&:to_s)
27
+ @table_names = retrieve_table_names - exclude_tables
28
+
29
+ freeze
30
+ end
31
+
32
+ def records(table, columns = [])
33
+ query = "SELECT #{sql_select_params(columns)} FROM #{table}"
34
+
35
+ client.query(query).to_a
36
+ end
37
+
38
+ def seed!(specification)
39
+ sql_statements = seed_sql_statements(specification)
40
+
41
+ shameless_execute(sql_statements)
42
+
43
+ sql_statements.length
44
+ end
45
+
46
+ def clean!
47
+ sql_statements = clean_sql_statements
48
+
49
+ shameless_execute(sql_statements)
50
+
51
+ sql_statements.length
52
+ end
53
+
54
+ private
55
+
56
+ attr_reader :client, :fixture_set, :table_names
57
+
58
+ def sql_select_params(columns)
59
+ Array(columns).any? ? Array(columns).map { |c| client.escape(c) }.join(',') : '*'
60
+ end
61
+
62
+ def seed_sql_statements(specification)
63
+ fixture_names = specification.stage.fixtures
64
+
65
+ fixture_names.map do |fixture_name|
66
+ fixture = fixture_set.get!(fixture_name)
67
+
68
+ SqlFixture.new(client, fixture).to_sql
69
+ end
70
+ end
71
+
72
+ def clean_sql_statements
73
+ table_names.map do |table_name|
74
+ "TRUNCATE #{table_name}"
75
+ end
76
+ end
77
+
78
+ def shameless_execute(sql_statements)
79
+ execute(disable_checks_sql_statement)
80
+ execute(sql_statements)
81
+ execute(enable_checks_sql_statement)
82
+ end
83
+
84
+ def execute(*sql_statements)
85
+ sql_statements.flatten.each do |sql_statement|
86
+ client.query(sql_statement)
87
+ end
88
+
89
+ nil
90
+ end
91
+
92
+ def disable_checks_sql_statement
93
+ 'SET @@foreign_key_checks = 0'
94
+ end
95
+
96
+ def enable_checks_sql_statement
97
+ 'SET @@foreign_key_checks = 1'
98
+ end
99
+
100
+ def retrieve_table_names
101
+ schema = client.escape(client.query_options[:database].to_s)
102
+ sql = "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = '#{schema}'"
103
+
104
+ client.query(sql).to_a.map { |v| v['TABLE_NAME'].to_s }
105
+ end
106
+
107
+ def assert_database_name(name)
108
+ return if name.end_with?(DATABASE_SUFFIX)
109
+
110
+ raise ArgumentError, "database (#{name}) must end in #{DATABASE_SUFFIX}"
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,43 @@
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
+ module Externals
12
+ class MysqlDatabase
13
+ # This class knows how to turn a fixture into sql.
14
+ class SqlFixture
15
+ extend Forwardable
16
+
17
+ def_delegators :fixture,
18
+ :fields,
19
+ :table
20
+
21
+ def initialize(client, fixture)
22
+ raise ArgumentError, 'fixture is required' unless fixture
23
+
24
+ @client = client
25
+ @fixture = fixture
26
+
27
+ freeze
28
+ end
29
+
30
+ def to_sql
31
+ sql_columns = fields.keys.join(',')
32
+ sql_values = fields.values.map { |v| "'#{client.escape(v.to_s)}'" }.join(',')
33
+
34
+ "INSERT INTO #{table} (#{sql_columns}) VALUES (#{sql_values})"
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :client, :fixture
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,77 @@
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 'spoon_client/mock'
11
+ require_relative 'spoon_client/result'
12
+
13
+ module Simmer
14
+ module Externals
15
+ # Wraps up Pdi::Spoon at a higher-level for Simmer to consume.
16
+ class SpoonClient
17
+ MOCK_KEY = :mock
18
+ MOCK_ERR_KEY = :mock_err
19
+
20
+ private_constant :MOCK_KEY, :MOCK_ERR_KEY
21
+
22
+ def initialize(config, files_dir)
23
+ @files_dir = files_dir
24
+
25
+ config = (config || {}).symbolize_keys
26
+
27
+ @spoon =
28
+ if config[MOCK_KEY]
29
+ Mock.new
30
+ elsif config[MOCK_ERR_KEY]
31
+ Mock.new(false)
32
+ else
33
+ Pdi::Spoon.new(dir: config[:dir])
34
+ end
35
+
36
+ freeze
37
+ end
38
+
39
+ def run(specification, config = {})
40
+ execution_result = nil
41
+ time_in_seconds = nil
42
+
43
+ begin
44
+ time_in_seconds = Benchmark.measure do
45
+ execution_result = execute!(specification, config)
46
+ end.real
47
+ rescue Pdi::Spoon::PanError, Pdi::Spoon::KitchenError => e
48
+ return Result.new(
49
+ message: "PDI execution returned an error: #{e.class.name} (#{e.execution.code})",
50
+ execution_result: e.execution,
51
+ time_in_seconds: time_in_seconds
52
+ )
53
+ end
54
+
55
+ Result.new(
56
+ execution_result: execution_result,
57
+ time_in_seconds: time_in_seconds
58
+ )
59
+ end
60
+
61
+ private
62
+
63
+ attr_reader :files_dir, :spoon
64
+
65
+ def execute!(specification, config)
66
+ act = specification.act
67
+
68
+ spoon.run(
69
+ repository: act.repository,
70
+ name: act.name,
71
+ params: act.compiled_params(files_dir, config),
72
+ type: act.type
73
+ )
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,39 @@
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
+ module Externals
12
+ class SpoonClient
13
+ # Provides a simple mock for the underlying Pdi::Spoon class.
14
+ class Mock
15
+ attr_reader :pass
16
+
17
+ def initialize(pass = true)
18
+ @pass = pass
19
+
20
+ freeze
21
+ end
22
+
23
+ def run(*)
24
+ raise Pdi::Spoon::KitchenError, 'mocked' unless pass
25
+
26
+ Pdi::Executor::Result.new(
27
+ args: [],
28
+ status: {
29
+ code: 0,
30
+ err: 'Some error output from PDI',
31
+ out: 'Some output from PDI',
32
+ pid: 123
33
+ }
34
+ )
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,48 @@
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
+ module Externals
12
+ class SpoonClient
13
+ # The return object from a SpoonClient#run call.
14
+ class Result
15
+ attr_reader :message, :execution_result, :time_in_seconds
16
+
17
+ def initialize(message: '', execution_result:, time_in_seconds:)
18
+ @message = message
19
+ @execution_result = execution_result
20
+ @time_in_seconds = (time_in_seconds || 0).round(2)
21
+
22
+ freeze
23
+ end
24
+
25
+ def pass?
26
+ execution_result.code.zero?
27
+ end
28
+
29
+ def fail?
30
+ !pass?
31
+ end
32
+
33
+ def to_h
34
+ {
35
+ 'pass' => pass?,
36
+ 'message' => message,
37
+ 'execution_result' => {
38
+ 'args' => execution_result.args,
39
+ 'code' => execution_result.code,
40
+ 'pid' => execution_result.pid
41
+ },
42
+ 'time_in_seconds' => time_in_seconds
43
+ }
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,38 @@
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/result'
11
+ require_relative 'specification'
12
+
13
+ module Simmer
14
+ # Runs all assertions and reports back the results.
15
+ class Judge
16
+ attr_reader :database
17
+
18
+ def initialize(database)
19
+ raise ArgumentError, 'database is required' unless database
20
+
21
+ @database = database
22
+
23
+ freeze
24
+ end
25
+
26
+ def assert(specification, output)
27
+ assertions = specification.assert.assertions
28
+
29
+ bad_assertions = assertions.each_with_object([]) do |assertion, memo|
30
+ bad_assert = assertion.assert(database, output)
31
+
32
+ memo << bad_assert if bad_assert
33
+ end
34
+
35
+ Result.new(bad_assertions)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,34 @@
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 Judge
12
+ # The return object of a Judge#assert call.
13
+ class Result
14
+ attr_reader :bad_assertions
15
+
16
+ def initialize(bad_assertions)
17
+ @bad_assertions = bad_assertions
18
+
19
+ freeze
20
+ end
21
+
22
+ def pass?
23
+ bad_assertions.empty?
24
+ end
25
+
26
+ def to_h
27
+ {
28
+ 'pass' => pass?,
29
+ 'bad_assertions' => bad_assertions.map(&:to_h)
30
+ }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,125 @@
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
+ # Runs a single specification.
15
+ class Runner
16
+ def initialize(database:, file_system:, out:, spoon_client:)
17
+ @database = database
18
+ @file_system = file_system
19
+ @judge = Judge.new(database)
20
+ @out = out
21
+ @spoon_client = spoon_client
22
+
23
+ freeze
24
+ end
25
+
26
+ def run(specification, config: {}, id: SecureRandom.uuid)
27
+ print("Name: #{specification.name}")
28
+ print("Path: #{specification.path}")
29
+
30
+ clean_db
31
+ seed_db(specification)
32
+ clean_file_system
33
+ seed_file_system(specification)
34
+
35
+ spoon_client_result = execute_spoon(specification, config)
36
+ judge_result = assert(specification, spoon_client_result)
37
+
38
+ Result.new(id, judge_result, specification, spoon_client_result).tap do |result|
39
+ msg = result.pass? ? 'PASS' : 'FAIL'
40
+ print_waiting('Done', 'Final verdict')
41
+ print(msg)
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :database, :file_system, :judge, :out, :spoon_client
48
+
49
+ def clean_db
50
+ print_waiting('Stage', 'Cleaning database')
51
+ count = database.clean!
52
+ print("#{count} table(s) emptied")
53
+
54
+ count
55
+ end
56
+
57
+ def seed_db(specification)
58
+ print_waiting('Stage', 'Seeding database')
59
+ count = database.seed!(specification)
60
+ print("#{count} record(s) inserted")
61
+
62
+ count
63
+ end
64
+
65
+ def clean_file_system
66
+ print_waiting('Stage', 'Cleaning File System')
67
+ count = file_system.clean
68
+ print("#{count} file(s) deleted")
69
+
70
+ count
71
+ end
72
+
73
+ def seed_file_system(specification)
74
+ print_waiting('Stage', 'Seeding File System')
75
+ count = file_system.write(specification)
76
+ print("#{count} file(s) uploaded")
77
+
78
+ count
79
+ end
80
+
81
+ def execute_spoon(specification, config)
82
+ print_waiting('Act', 'Executing Spoon')
83
+ spoon_client_result = spoon_client.run(specification, config)
84
+ msg = spoon_client_result.pass? ? 'Pass' : 'Fail'
85
+ print(msg)
86
+
87
+ spoon_client_result
88
+ end
89
+
90
+ def assert(specification, spoon_client_result)
91
+ print_waiting('Assert', 'Checking results')
92
+
93
+ if spoon_client_result.fail?
94
+ print('Skipped')
95
+ return nil
96
+ end
97
+
98
+ output = spoon_client_result.execution_result.out
99
+ judge_result = judge.assert(specification, output)
100
+ msg = judge_result.pass? ? 'Pass' : 'Fail'
101
+
102
+ print(msg)
103
+
104
+ judge_result
105
+ end
106
+
107
+ def print(msg)
108
+ out.puts(msg)
109
+ end
110
+
111
+ def print_waiting(stage, msg)
112
+ max = 25
113
+ char = '.'
114
+ msg = " > #{pad_right(stage, 6)} - #{pad_right(msg, max, char)}"
115
+
116
+ out.print(msg)
117
+ end
118
+
119
+ def pad_right(msg, len, char = ' ')
120
+ missing = len - msg.length
121
+
122
+ "#{msg}#{char * missing}"
123
+ end
124
+ end
125
+ end