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.
Files changed (46) hide show
  1. data/Gemfile +4 -0
  2. data/Gemfile.lock +26 -0
  3. data/LICENSE +3 -0
  4. data/README.markdown +16 -0
  5. data/Rakefile +11 -0
  6. data/bin/slacker +28 -0
  7. data/lib/slacker.rb +138 -0
  8. data/lib/slacker/application.rb +201 -0
  9. data/lib/slacker/command_line_formatter.rb +59 -0
  10. data/lib/slacker/configuration.rb +59 -0
  11. data/lib/slacker/formatter.rb +19 -0
  12. data/lib/slacker/query_result_matcher.rb +169 -0
  13. data/lib/slacker/rspec_ext.rb +69 -0
  14. data/lib/slacker/rspec_monkey.rb +7 -0
  15. data/lib/slacker/sql.rb +39 -0
  16. data/lib/slacker/string_helper.rb +17 -0
  17. data/lib/slacker/version.rb +3 -0
  18. data/slacker.gemspec +23 -0
  19. data/spec/application_spec.rb +14 -0
  20. data/spec/query_result_matcher_spec.rb +267 -0
  21. data/spec/rspec_ext_spec.rb +127 -0
  22. data/spec/slacker_spec.rb +60 -0
  23. data/spec/spec_helper.rb +9 -0
  24. data/spec/test_files/matcher/completely_blank.csv +0 -0
  25. data/spec/test_files/matcher/no_rows.csv +1 -0
  26. data/spec/test_files/matcher/test_1.csv +3 -0
  27. data/spec/test_files/test_slacker_project/data/test_1.csv +3 -0
  28. data/spec/test_files/test_slacker_project/sql/example_1/helper_1.sql +1 -0
  29. data/spec/test_files/test_slacker_project/sql/example_1/helper_2.sql +1 -0
  30. data/spec/test_files/test_slacker_project/sql/example_1/helper_2.sql.erb +1 -0
  31. data/spec/test_files/test_slacker_project/sql/helpers/HELPER_4.SQL +1 -0
  32. data/spec/test_files/test_slacker_project/sql/helpers/helper_1.sql +1 -0
  33. data/spec/test_files/test_slacker_project/sql/helpers/helper_2.sql.erb +1 -0
  34. data/spec/test_files/test_slacker_project/sql/helpers/helper_3.sql +1 -0
  35. data/spec/test_files/test_slacker_project/sql/helpers/helper_3.sql.erb +1 -0
  36. data/spec/test_files/test_slacker_project/sql/helpers/text_file_1.txt +1 -0
  37. data/spec/test_files/test_slacker_project/sql/multi_nested.sql.erb +1 -0
  38. data/spec/test_files/test_slacker_project/sql/nest/example_1/helper_1.sql.erb +1 -0
  39. data/spec/test_files/test_slacker_project/sql/nest/nested_1.sql.erb +2 -0
  40. data/spec/test_files/test_slacker_project/sql/nest/nested_2.sql.erb +1 -0
  41. data/spec/test_files/test_slacker_project/sql/nested.sql.erb +1 -0
  42. data/spec/test_files/test_slacker_project/sql/nested_with_params.sql.erb +1 -0
  43. data/spec/test_files/test_slacker_project/sql/no_params.sql.erb +3 -0
  44. data/spec/test_files/test_slacker_project/sql/params.sql.erb +2 -0
  45. data/spec/test_files/test_slacker_project/sql/test_1.sql +1 -0
  46. metadata +114 -0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in slacker.gemspec
4
+ gemspec
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
@@ -0,0 +1,3 @@
1
+ == slacker
2
+
3
+ Put appropriate LICENSE for your project here.
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
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/clean'
4
+ require 'rubygems/package_task'
5
+ require 'bundler/gem_tasks'
6
+ require 'rspec/core/rake_task'
7
+
8
+ RSpec::Core::RakeTask.new do |t|
9
+ t.fail_on_error = false
10
+ t.pattern = 'spec/**/*.rb'
11
+ end
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