slacker 0.0.1
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.
- data/Gemfile +4 -0
- data/Gemfile.lock +26 -0
- data/LICENSE +3 -0
- data/README.markdown +16 -0
- data/Rakefile +11 -0
- data/bin/slacker +28 -0
- data/lib/slacker.rb +138 -0
- data/lib/slacker/application.rb +201 -0
- data/lib/slacker/command_line_formatter.rb +59 -0
- data/lib/slacker/configuration.rb +59 -0
- data/lib/slacker/formatter.rb +19 -0
- data/lib/slacker/query_result_matcher.rb +169 -0
- data/lib/slacker/rspec_ext.rb +69 -0
- data/lib/slacker/rspec_monkey.rb +7 -0
- data/lib/slacker/sql.rb +39 -0
- data/lib/slacker/string_helper.rb +17 -0
- data/lib/slacker/version.rb +3 -0
- data/slacker.gemspec +23 -0
- data/spec/application_spec.rb +14 -0
- data/spec/query_result_matcher_spec.rb +267 -0
- data/spec/rspec_ext_spec.rb +127 -0
- data/spec/slacker_spec.rb +60 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/test_files/matcher/completely_blank.csv +0 -0
- data/spec/test_files/matcher/no_rows.csv +1 -0
- data/spec/test_files/matcher/test_1.csv +3 -0
- data/spec/test_files/test_slacker_project/data/test_1.csv +3 -0
- data/spec/test_files/test_slacker_project/sql/example_1/helper_1.sql +1 -0
- data/spec/test_files/test_slacker_project/sql/example_1/helper_2.sql +1 -0
- data/spec/test_files/test_slacker_project/sql/example_1/helper_2.sql.erb +1 -0
- data/spec/test_files/test_slacker_project/sql/helpers/HELPER_4.SQL +1 -0
- data/spec/test_files/test_slacker_project/sql/helpers/helper_1.sql +1 -0
- data/spec/test_files/test_slacker_project/sql/helpers/helper_2.sql.erb +1 -0
- data/spec/test_files/test_slacker_project/sql/helpers/helper_3.sql +1 -0
- data/spec/test_files/test_slacker_project/sql/helpers/helper_3.sql.erb +1 -0
- data/spec/test_files/test_slacker_project/sql/helpers/text_file_1.txt +1 -0
- data/spec/test_files/test_slacker_project/sql/multi_nested.sql.erb +1 -0
- data/spec/test_files/test_slacker_project/sql/nest/example_1/helper_1.sql.erb +1 -0
- data/spec/test_files/test_slacker_project/sql/nest/nested_1.sql.erb +2 -0
- data/spec/test_files/test_slacker_project/sql/nest/nested_2.sql.erb +1 -0
- data/spec/test_files/test_slacker_project/sql/nested.sql.erb +1 -0
- data/spec/test_files/test_slacker_project/sql/nested_with_params.sql.erb +1 -0
- data/spec/test_files/test_slacker_project/sql/no_params.sql.erb +3 -0
- data/spec/test_files/test_slacker_project/sql/params.sql.erb +2 -0
- data/spec/test_files/test_slacker_project/sql/test_1.sql +1 -0
- metadata +114 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
slacker (0.0.1)
|
5
|
+
rspec (= 2.5.0)
|
6
|
+
ruby-odbc (= 0.99994)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: http://rubygems.org/
|
10
|
+
specs:
|
11
|
+
diff-lcs (1.1.2)
|
12
|
+
rspec (2.5.0)
|
13
|
+
rspec-core (~> 2.5.0)
|
14
|
+
rspec-expectations (~> 2.5.0)
|
15
|
+
rspec-mocks (~> 2.5.0)
|
16
|
+
rspec-core (2.5.1)
|
17
|
+
rspec-expectations (2.5.0)
|
18
|
+
diff-lcs (~> 1.1.2)
|
19
|
+
rspec-mocks (2.5.0)
|
20
|
+
ruby-odbc (0.99994)
|
21
|
+
|
22
|
+
PLATFORMS
|
23
|
+
x86-mingw32
|
24
|
+
|
25
|
+
DEPENDENCIES
|
26
|
+
slacker!
|
data/LICENSE
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# Slacker
|
2
|
+
Behavior Driven Development for SQL Server
|
3
|
+
|
4
|
+
# Description
|
5
|
+
__Slacker__ is a Ruby (RSpec-based) framework for developing automated tests for SQL Server programmable objects such as stored procedures and scalar/table functions.
|
6
|
+
|
7
|
+
# Install
|
8
|
+
gem install slacker
|
9
|
+
|
10
|
+
__Slacker__ automatically installs the following gems:
|
11
|
+
|
12
|
+
* rspec 2.5.0
|
13
|
+
* ruby-odbc 0.99994
|
14
|
+
|
15
|
+
__Slacker__ runs on Windows and Linux.<br/>
|
16
|
+
Before installing __Slacker__ on Windows, you need to install the Windows DevKit (ruby-odbc contains native extensions).
|
data/Rakefile
ADDED
data/bin/slacker
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'slacker'
|
3
|
+
require 'yaml'
|
4
|
+
require 'slacker/command_line_formatter'
|
5
|
+
|
6
|
+
def db_config_from_file(file_path)
|
7
|
+
dbconfig = nil
|
8
|
+
File.open(file_path) do |dbconfig_file|
|
9
|
+
dbconfig = YAML::load(dbconfig_file)
|
10
|
+
end
|
11
|
+
dbconfig
|
12
|
+
end
|
13
|
+
|
14
|
+
# Preset the application to run on the console
|
15
|
+
Slacker.configure do |config|
|
16
|
+
config.console_enabled = true
|
17
|
+
config.formatter = Slacker::CommandLineFormatter.new($stdout)
|
18
|
+
|
19
|
+
# Setup the target connection based on the contents in database.yml
|
20
|
+
db_config = db_config_from_file(config.expand_path('database.yml'))
|
21
|
+
|
22
|
+
config.db_server = db_config["server"]
|
23
|
+
config.db_name = db_config["database"]
|
24
|
+
config.db_user = db_config["user"]
|
25
|
+
config.db_password = db_config["password"]
|
26
|
+
end
|
27
|
+
|
28
|
+
Slacker.application.run
|
data/lib/slacker.rb
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
require "slacker/version"
|
2
|
+
require 'slacker/application'
|
3
|
+
require 'slacker/configuration'
|
4
|
+
require 'slacker/sql'
|
5
|
+
require 'slacker/formatter'
|
6
|
+
require 'csv'
|
7
|
+
require 'erb'
|
8
|
+
|
9
|
+
module Slacker
|
10
|
+
class << self
|
11
|
+
def application
|
12
|
+
@application ||= Slacker::Application.new(configuration)
|
13
|
+
end
|
14
|
+
|
15
|
+
def sql(rspec_ext)
|
16
|
+
Slacker::Sql.new(configuration.expand_path('sql'), rspec_ext)
|
17
|
+
end
|
18
|
+
|
19
|
+
def configuration
|
20
|
+
@configuration ||= Slacker::Configuration.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def configure
|
24
|
+
yield configuration
|
25
|
+
end
|
26
|
+
|
27
|
+
def sql_template_path_stack
|
28
|
+
if @sql_template_path_stack.nil?
|
29
|
+
@sql_template_path_stack = []
|
30
|
+
@sql_template_path_stack.push(configuration.expand_path('sql'))
|
31
|
+
end
|
32
|
+
@sql_template_path_stack
|
33
|
+
end
|
34
|
+
|
35
|
+
# Given a template name produce the path to that template
|
36
|
+
def get_sql_template_path(template_name)
|
37
|
+
template_base_dir = template_name[0].chr == '/' ? sql_template_path_stack.first : sql_template_path_stack.last
|
38
|
+
File.expand_path(template_base_dir + '/' + template_name)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Render a template file and return the result
|
42
|
+
def render(template_name, options = {})
|
43
|
+
template_file_path = get_sql_template_path(template_name)
|
44
|
+
|
45
|
+
if !File.exists?(template_file_path)
|
46
|
+
raise "File #{template_file_path} does not exist"
|
47
|
+
end
|
48
|
+
|
49
|
+
begin
|
50
|
+
sql_template_path_stack.push(File.dirname(template_file_path))
|
51
|
+
result = render_text(IO.read(template_file_path), options)
|
52
|
+
rescue => detail
|
53
|
+
# Report errors in the template
|
54
|
+
if detail.backtrace[0] =~ /^\(erb\)/
|
55
|
+
raise "Template error in #{template_name}:\n#{detail.backtrace[0]} : #{detail.message}\n"
|
56
|
+
else
|
57
|
+
raise detail
|
58
|
+
end
|
59
|
+
ensure
|
60
|
+
sql_template_path_stack.pop
|
61
|
+
end
|
62
|
+
|
63
|
+
result
|
64
|
+
end
|
65
|
+
|
66
|
+
# Render a template test and return the result
|
67
|
+
def render_text(template_text, options)
|
68
|
+
ERB.new(template_text, 0, '%<>').result(binding)
|
69
|
+
end
|
70
|
+
|
71
|
+
def filter_golden_master(golden_master)
|
72
|
+
golden_master = case golden_master
|
73
|
+
when String
|
74
|
+
golden_master =~ /\.csv$/ ? get_csv(golden_master) : golden_master
|
75
|
+
else
|
76
|
+
golden_master
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def sql_from_query_string(query_string, options = {})
|
81
|
+
case query_string
|
82
|
+
when /\.sql$/i,/\.erb$/i
|
83
|
+
#Pass the file through an ERb template engine
|
84
|
+
render(query_string, options)
|
85
|
+
else
|
86
|
+
query_string
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def get_csv(csv_file_path)
|
91
|
+
CSV.read(configuration.expand_path("data/#{csv_file_path}"), {:headers => true, :encoding => 'Windows-1252', :header_converters => :symbol})
|
92
|
+
end
|
93
|
+
|
94
|
+
def hash_array_to_csv(raw_array)
|
95
|
+
csv_array = []
|
96
|
+
raw_array.each do |raw_row|
|
97
|
+
csv_array << CSV::Row.new(raw_row.keys, raw_row.values)
|
98
|
+
end
|
99
|
+
CSV::Table.new(csv_array)
|
100
|
+
end
|
101
|
+
|
102
|
+
def sql_file_from_method_name(base_folder, method_name)
|
103
|
+
file_name = File.join(base_folder, method_name)
|
104
|
+
|
105
|
+
file_name = case
|
106
|
+
when File.exists?("#{file_name}.sql") then "#{file_name}.sql"
|
107
|
+
when File.exists?("#{file_name}.sql.erb") then "#{file_name}.sql.erb"
|
108
|
+
else nil
|
109
|
+
end
|
110
|
+
|
111
|
+
file_name.nil? ? nil : file_name.gsub(/#{Regexp.escape(configuration.expand_path('sql'))}/i, '')
|
112
|
+
end
|
113
|
+
|
114
|
+
def construct_log_name(entry_point, query_string, options)
|
115
|
+
"#{entry_point} '#{query_string}'" + (options.empty? ? '': ", options = #{options.inspect}")
|
116
|
+
end
|
117
|
+
|
118
|
+
# Run a SQL query against an example
|
119
|
+
def query_script(example, sql, log_name=nil)
|
120
|
+
log_name ||= 'Run SQL Script'
|
121
|
+
example.metadata[:sql] += ((example.metadata[:sql] == '' ? '' : "\n\n") + "-- #{log_name}\n#{sql}")
|
122
|
+
application.query_script(sql)
|
123
|
+
end
|
124
|
+
|
125
|
+
def load_csv(example, csv, table_name, log_name = nil)
|
126
|
+
csv_a = csv.to_a
|
127
|
+
sql = nil
|
128
|
+
csv_a.each_with_index do |row, index|
|
129
|
+
if index == 0
|
130
|
+
sql = "INSERT INTO #{table_name}(#{row.map{|header| "[#{header}]"}.join(',')})"
|
131
|
+
else
|
132
|
+
sql += ("\nSELECT #{row.map{|val| val.nil? ? 'NULL': "'#{val}'"}.join(',')}" + (index < (csv_a.count - 1) ? ' UNION ALL' : ''))
|
133
|
+
end
|
134
|
+
end
|
135
|
+
query_script(example, sql, log_name) unless sql.nil?
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'rspec/core'
|
3
|
+
require 'slacker/rspec_monkey'
|
4
|
+
require 'slacker/rspec_ext'
|
5
|
+
require 'slacker/string_helper'
|
6
|
+
require 'odbc'
|
7
|
+
|
8
|
+
module Slacker
|
9
|
+
class Application
|
10
|
+
attr_reader :target_folder_structure
|
11
|
+
|
12
|
+
SQL_OPTIONS = <<EOF
|
13
|
+
set textsize 2147483647;
|
14
|
+
set language us_english;
|
15
|
+
set dateformat mdy;
|
16
|
+
set datefirst 7;
|
17
|
+
set lock_timeout -1;
|
18
|
+
set quoted_identifier on;
|
19
|
+
set arithabort on;
|
20
|
+
set ansi_null_dflt_on on;
|
21
|
+
set ansi_warnings on;
|
22
|
+
set ansi_padding on;
|
23
|
+
set ansi_nulls on;
|
24
|
+
set concat_null_yields_null on;
|
25
|
+
EOF
|
26
|
+
|
27
|
+
def initialize(configuration)
|
28
|
+
@configuration = configuration
|
29
|
+
@target_folder_structure = ['data', 'debug', 'debug/passed_examples', 'debug/failed_examples', 'sql', 'spec', 'lib', 'lib/helpers']
|
30
|
+
@error_message = ''
|
31
|
+
@database = ODBC::Database.new
|
32
|
+
end
|
33
|
+
|
34
|
+
def print_connection_message
|
35
|
+
puts "#{@configuration.db_name} (#{@configuration.db_server})" if @configuration.console_enabled
|
36
|
+
end
|
37
|
+
|
38
|
+
# Customize RSpec and run it
|
39
|
+
def run
|
40
|
+
begin
|
41
|
+
error = catch :error_exit do
|
42
|
+
print_connection_message
|
43
|
+
test_folder_structure
|
44
|
+
cleanup_folders
|
45
|
+
configure
|
46
|
+
run_rspec
|
47
|
+
false #Return false to error
|
48
|
+
end
|
49
|
+
ensure
|
50
|
+
cleanup_after_run
|
51
|
+
end
|
52
|
+
|
53
|
+
if @configuration.console_enabled
|
54
|
+
puts @error_message if error
|
55
|
+
else
|
56
|
+
raise @error_message if error
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def run_rspec
|
61
|
+
RSpec::Core::Runner.disable_autorun!
|
62
|
+
|
63
|
+
RSpec::Core::Runner.run(@configuration.rspec_args,
|
64
|
+
@configuration.error_stream,
|
65
|
+
@configuration.output_stream)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Configure Slacker
|
69
|
+
def configure
|
70
|
+
configure_db
|
71
|
+
configure_rspec
|
72
|
+
configure_misc
|
73
|
+
end
|
74
|
+
|
75
|
+
def cleanup_after_run
|
76
|
+
@database.disconnect if (@database && @database.connected?)
|
77
|
+
end
|
78
|
+
|
79
|
+
def cleanup_folders
|
80
|
+
cleanup_folder('debug/passed_examples')
|
81
|
+
cleanup_folder('debug/failed_examples')
|
82
|
+
end
|
83
|
+
|
84
|
+
def cleanup_folder(folder)
|
85
|
+
folder_path = get_path(folder)
|
86
|
+
Dir.new(folder_path).each{|file_name| File.delete("#{folder_path}/#{file_name}") if File.file?("#{folder_path}/#{file_name}")}
|
87
|
+
end
|
88
|
+
|
89
|
+
# Get a path relative to the current path
|
90
|
+
def get_path(path)
|
91
|
+
@configuration.expand_path(path)
|
92
|
+
end
|
93
|
+
|
94
|
+
def configure_misc
|
95
|
+
# Add the lib folder to the load path
|
96
|
+
$:.push get_path('lib')
|
97
|
+
# Mixin the helper modules
|
98
|
+
mixin_helpers
|
99
|
+
end
|
100
|
+
|
101
|
+
# Mix in the helper modules
|
102
|
+
def mixin_helpers
|
103
|
+
helpers_dir = get_path('lib/helpers')
|
104
|
+
$:.push helpers_dir
|
105
|
+
Dir.new(helpers_dir).each do |file_name|
|
106
|
+
if file_name =~ /\.rb$/
|
107
|
+
require file_name
|
108
|
+
RSpec.configure do |config|
|
109
|
+
config.include(Slacker::StringHelper.constantize(Slacker::StringHelper.camelize(file_name.gsub(/\.rb$/,''))))
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Configure database connection
|
116
|
+
def configure_db
|
117
|
+
drv = ODBC::Driver.new
|
118
|
+
drv.name = 'Driver1'
|
119
|
+
drv.attrs.tap do |a|
|
120
|
+
a['Driver'] = '{SQL Server}'
|
121
|
+
a['Server']= @configuration.db_server
|
122
|
+
a['Database']= @configuration.db_name
|
123
|
+
a['Uid'] = @configuration.db_user
|
124
|
+
a['Pwd'] = @configuration.db_password
|
125
|
+
a['TDS_Version'] = '7.0' #Used by the linux driver
|
126
|
+
end
|
127
|
+
|
128
|
+
@database.drvconnect(drv)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Run a script against the currently configured database
|
132
|
+
def query_script(sql)
|
133
|
+
results = []
|
134
|
+
begin
|
135
|
+
st = @database.run(sql)
|
136
|
+
begin
|
137
|
+
if st.ncols > 0
|
138
|
+
rows = []
|
139
|
+
st.each_hash(false, true){|row| rows << row}
|
140
|
+
results << rows
|
141
|
+
end
|
142
|
+
end while(st.more_results)
|
143
|
+
ensure
|
144
|
+
st.drop unless st.nil?
|
145
|
+
end
|
146
|
+
results.count > 1 ? results : results.first
|
147
|
+
end
|
148
|
+
|
149
|
+
# Customize RSpec
|
150
|
+
def configure_rspec
|
151
|
+
before_proc = lambda do |example|
|
152
|
+
# Initialize the example's SQL
|
153
|
+
example.metadata[:sql] = ''
|
154
|
+
Slacker.query_script(example, 'begin transaction;', 'Initiate the example script')
|
155
|
+
Slacker.query_script(example, SQL_OPTIONS, 'Set default options')
|
156
|
+
end
|
157
|
+
|
158
|
+
after_proc = lambda do |example|
|
159
|
+
Slacker.query_script(example, 'rollback transaction;', 'Rollback the changes made by the example script')
|
160
|
+
end
|
161
|
+
|
162
|
+
# Reset RSpec through a monkey-patched method
|
163
|
+
RSpec.slacker_reset
|
164
|
+
|
165
|
+
RSpec.configure do |config|
|
166
|
+
# Global "before" hooks to begin a transaction
|
167
|
+
config.before(:each) do
|
168
|
+
before_proc.call(example)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Global "after" hooks to rollback a transaction
|
172
|
+
config.after(:each) do
|
173
|
+
after_proc.call(example)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Slacker's RSpec extension module
|
177
|
+
config.include(Slacker::RSpecExt)
|
178
|
+
config.extend(Slacker::RSpecExt)
|
179
|
+
|
180
|
+
config.output_stream = @configuration.output_stream
|
181
|
+
config.error_stream = @configuration.error_stream
|
182
|
+
|
183
|
+
config.formatters << @configuration.formatter unless @configuration.formatter.nil?
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Tests the current folder's structure
|
188
|
+
def test_folder_structure()
|
189
|
+
target_folder_structure.each do |dir|
|
190
|
+
if !File.directory?(get_path(dir))
|
191
|
+
throw_error("Cannot find directory \"#{get_path(dir)}\"")
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def throw_error(msg)
|
197
|
+
@error_message = msg
|
198
|
+
throw :error_exit, true
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'slacker/formatter'
|
2
|
+
require 'rspec/core/formatters/progress_formatter'
|
3
|
+
|
4
|
+
module Slacker
|
5
|
+
class CommandLineFormatter < RSpec::Core::Formatters::ProgressFormatter
|
6
|
+
include Slacker::Formatter
|
7
|
+
|
8
|
+
def initialize(output)
|
9
|
+
super(output)
|
10
|
+
@failed_examples_count = 0
|
11
|
+
@passed_examples_count = 0
|
12
|
+
end
|
13
|
+
|
14
|
+
def example_passed(example)
|
15
|
+
process_example_debug_output(example, false)
|
16
|
+
super(example)
|
17
|
+
end
|
18
|
+
|
19
|
+
def example_failed(example)
|
20
|
+
process_example_debug_output(example, true)
|
21
|
+
super(example)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def process_example_debug_output(example, example_failed)
|
27
|
+
if example_failed
|
28
|
+
@failed_examples_count += 1
|
29
|
+
debug_output(example, Slacker.configuration.expand_path('debug/failed_examples'), @failed_examples_count, example_failed)
|
30
|
+
else
|
31
|
+
@passed_examples_count += 1
|
32
|
+
debug_output(example, Slacker.configuration.expand_path('debug/passed_examples'), @passed_examples_count, example_failed)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def debug_output(example, out_folder, file_number, example_failed)
|
37
|
+
# Write out the SQL
|
38
|
+
File.open("#{out_folder}/example_#{'%03d' % file_number}.sql", 'w') do |out_file|
|
39
|
+
out_file.write(get_formatted_example_sql(example, example_failed))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_formatted_example_sql(example, example_failed)
|
44
|
+
sql = <<EOF
|
45
|
+
-- Example "#{example.metadata[:full_description]}"
|
46
|
+
-- #{example.metadata[:location]}
|
47
|
+
-- Executed at #{example.metadata[:execution_result][:started_at]}
|
48
|
+
|
49
|
+
#{example.metadata[:sql]}
|
50
|
+
|
51
|
+
-- SLACKER RESULTS
|
52
|
+
-- *******************************************
|
53
|
+
#{example_failed ? example_failure_text(example).split("\n").collect{|line| '-- ' + line}.join("\n") : '-- Example Passed OK'}
|
54
|
+
-- *******************************************
|
55
|
+
EOF
|
56
|
+
sql.strip
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|