simmer 1.0.0.pre.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +8 -0
  3. data/.gitignore +6 -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 +79 -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 +86 -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 +60 -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 +285 -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.round(2)
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
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