simmer 1.0.0.pre.alpha.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|