slacker 0.0.1

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