slacker 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|