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.
- checksums.yaml +7 -0
- data/.editorconfig +8 -0
- data/.gitignore +7 -0
- data/.rubocop.yml +25 -0
- data/.ruby-version +1 -0
- data/.travis.yml +25 -0
- data/CHANGELOG.md +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +5 -0
- data/Guardfile +16 -0
- data/LICENSE +7 -0
- data/README.md +5 -0
- data/Rakefile +17 -0
- data/bin/console +18 -0
- data/bin/simmer +11 -0
- data/lib/simmer.rb +50 -0
- data/lib/simmer/configuration.rb +90 -0
- data/lib/simmer/core_ext/hash.rb +21 -0
- data/lib/simmer/externals.rb +12 -0
- data/lib/simmer/externals/aws_file_system.rb +81 -0
- data/lib/simmer/externals/mysql_database.rb +114 -0
- data/lib/simmer/externals/mysql_database/sql_fixture.rb +43 -0
- data/lib/simmer/externals/spoon_client.rb +77 -0
- data/lib/simmer/externals/spoon_client/mock.rb +39 -0
- data/lib/simmer/externals/spoon_client/result.rb +48 -0
- data/lib/simmer/judge.rb +38 -0
- data/lib/simmer/judge/result.rb +34 -0
- data/lib/simmer/runner.rb +125 -0
- data/lib/simmer/runner/result.rb +52 -0
- data/lib/simmer/session.rb +79 -0
- data/lib/simmer/session/reporter.rb +100 -0
- data/lib/simmer/session/result.rb +45 -0
- data/lib/simmer/specification.rb +34 -0
- data/lib/simmer/specification/act.rb +48 -0
- data/lib/simmer/specification/act/params.rb +55 -0
- data/lib/simmer/specification/assert.rb +27 -0
- data/lib/simmer/specification/assert/assertions.rb +27 -0
- data/lib/simmer/specification/assert/assertions/bad_output_assertion.rb +34 -0
- data/lib/simmer/specification/assert/assertions/bad_table_assertion.rb +38 -0
- data/lib/simmer/specification/assert/assertions/output.rb +37 -0
- data/lib/simmer/specification/assert/assertions/table.rb +47 -0
- data/lib/simmer/specification/stage.rb +28 -0
- data/lib/simmer/specification/stage/input_file.rb +33 -0
- data/lib/simmer/suite.rb +73 -0
- data/lib/simmer/util.rb +13 -0
- data/lib/simmer/util/evaluator.rb +32 -0
- data/lib/simmer/util/fixture.rb +32 -0
- data/lib/simmer/util/fixture_set.rb +41 -0
- data/lib/simmer/util/record.rb +54 -0
- data/lib/simmer/util/record_set.rb +51 -0
- data/lib/simmer/util/resolver.rb +28 -0
- data/lib/simmer/util/yaml_reader.rb +61 -0
- data/lib/simmer/version.rb +12 -0
- data/simmer.gemspec +38 -0
- data/spec/simmer/core_ext/hash_spec.rb +16 -0
- data/spec/spec_helper.rb +22 -0
- 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
|
data/lib/simmer/judge.rb
ADDED
@@ -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
|