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
@@ -0,0 +1,59 @@
1
+ module Slacker
2
+ class Configuration
3
+ attr_accessor :base_dir, :error_stream, :output_stream, :formatter, :db_server, :db_name, :db_user, :db_password, :console_enabled
4
+
5
+ def initialize
6
+ @console_enabled = true
7
+ @base_dir = Dir.pwd
8
+ @error_stream = nil
9
+ @output_stream = nil
10
+ @rspec_args = nil
11
+ @formatter = nil
12
+ @db_server = nil
13
+ @db_name = nil
14
+ @db_user = nil
15
+ @db_password = nil
16
+ end
17
+
18
+ def expand_path(path)
19
+ File.expand_path("#{@base_dir}/#{path}")
20
+ end
21
+
22
+ def console_enabled
23
+ @console_enabled
24
+ end
25
+
26
+ def console_enabled=(value)
27
+ @console_enabled = value
28
+ if @console_enabled
29
+ @error_stream = $stderr
30
+ @output_stream = $stdout
31
+ @rspec_args = ARGV
32
+ else
33
+ @error_stream = nil
34
+ @output_stream = nil
35
+ @rspec_args = []
36
+ end
37
+ end
38
+
39
+ def rspec_args
40
+ if @rspec_args.nil? || @rspec_args.empty?
41
+ Dir.glob(expand_path("spec/**/*.rb"))
42
+ else
43
+ @rspec_args
44
+ end
45
+ end
46
+
47
+ def rspec_args=(value)
48
+ @rspec_args = value
49
+ end
50
+
51
+ def dsn_string
52
+ "Driver={SQL Server};Server=#{@db_server};Database=#{@db_name};Uid=#{@db_user};Pwd=#{@db_password}"
53
+ end
54
+
55
+ def db_config
56
+ {:adapter => 'sqlserver', :mode => 'odbc', :dsn => dsn_string}
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,19 @@
1
+ require 'slacker'
2
+
3
+ module Slacker
4
+ module Formatter
5
+ def example_failure_text(example)
6
+ text = ''
7
+ exception = example.execution_result[:exception]
8
+ text << "Failure/Error: #{read_failed_line(exception, example).strip}\n"
9
+ text << "#{long_padding}#{exception.class.name << ":"}\n" unless exception.class.name =~ /RSpec/
10
+ exception.message.split("\n").each { |line| text << "#{line}\n" }
11
+
12
+ format_backtrace(example.execution_result[:exception].backtrace, example).each do |backtrace_info|
13
+ text << "# #{backtrace_info}\n"
14
+ end
15
+
16
+ text
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,169 @@
1
+ require 'csv'
2
+
3
+ module Slacker
4
+ class QueryResultMatcher
5
+ def initialize(golden_master)
6
+ @golden_master = golden_master
7
+ @failure_message = ''
8
+ end
9
+
10
+ def matches?(subject)
11
+ does_match = false
12
+
13
+ subject = normalize_query_result_subject(subject)
14
+
15
+ catch :no_match do
16
+ test_type_match(subject)
17
+ test_value_match(subject)
18
+ does_match = true
19
+ end
20
+
21
+ does_match
22
+ end
23
+
24
+ def failure_message_for_should
25
+ @failure_message
26
+ end
27
+
28
+ private
29
+
30
+ #test if the subject type is consistent with the golden master
31
+ def test_type_match(subject)
32
+ if !is_well_formed_query_result?(subject)
33
+ throw_no_match "Can perform query matches only against a well formed query result subject"
34
+ end
35
+
36
+ if (@golden_master.kind_of? Array) && !is_well_formed_query_result?(@golden_master)
37
+ throw_no_match "Cannot match against a non-well formed golden master array"
38
+ end
39
+ end
40
+
41
+ def test_value_match(subject)
42
+ case @golden_master
43
+ when CSV::Table
44
+ test_csv_table_match(subject)
45
+ when Array
46
+ test_array_match(subject)
47
+ else
48
+ test_single_value_match(subject)
49
+ end
50
+ end
51
+
52
+ # Compare the golden master CSV table with the subject query result
53
+ def test_csv_table_match(subject)
54
+ # Compare the fields
55
+ if !subject.empty?
56
+ subject_fields = subject[0].keys
57
+ master_fields = @golden_master.headers
58
+
59
+ if subject_fields.count != master_fields.count
60
+ throw_no_match "Expected #{master_fields.count} field(s), got #{subject_fields.count}"
61
+ end
62
+
63
+ master_fields.each_with_index do |column, index|
64
+ if column != subject_fields[index]
65
+ throw_no_match "Expected field \"#{column}\", got field \"#{subject_fields[index]}\""
66
+ end
67
+ end
68
+ end
69
+
70
+ # Compare the number of records
71
+ subject_record_count = subject.count
72
+ master_record_count = @golden_master.inject(0){|count| count += 1}
73
+ if subject_record_count != master_record_count
74
+ throw_no_match "Expected #{master_record_count} record(s), got #{subject_record_count}"
75
+ end
76
+
77
+ # Compare the values of the golden master with the subject
78
+ current_row = 0
79
+ @golden_master.each do |row|
80
+ row.each do |field, master_string|
81
+ subject_value = subject[current_row][field]
82
+ if !match_values?(master_string, subject_value)
83
+ throw_no_match "Field \"#{field}\", Record #{current_row + 1}: Expected value #{master_string.nil? ? '<NULL>' : "\"#{master_string}\""}, got #{subject_value.nil? ? '<NULL>' : "\"#{subject_value}\""}"
84
+ end
85
+ end
86
+ current_row += 1
87
+ end
88
+ end
89
+
90
+ def match_types?(master_val, subject_val)
91
+ subject_val.kind_of? master_val.class
92
+ end
93
+
94
+ def match_values?(master_val, subject_val)
95
+ if master_val.nil?
96
+ master_val == subject_val
97
+ elsif subject_val.kind_of?(Time) && master_val.kind_of?(String)
98
+ Time.parse(master_val) == subject_val
99
+ else
100
+ subject_val.to_s == master_val.to_s
101
+ end
102
+ end
103
+
104
+ def test_array_match(subject)
105
+ # Compare the fields
106
+ if !(subject.empty? || @golden_master.empty?)
107
+ subject_fields = subject[0].keys
108
+ master_fields = @golden_master[0].keys
109
+
110
+ if subject_fields.count != master_fields.count
111
+ throw_no_match "Expected #{master_fields.count} field(s), got #{subject_fields.count}"
112
+ end
113
+
114
+ master_fields.each_with_index do |column, index|
115
+ if column != subject_fields[index]
116
+ throw_no_match "Expected field \"#{column}\", got field \"#{subject_fields[index]}\""
117
+ end
118
+ end
119
+ end
120
+
121
+ # Compare the number of records
122
+ subject_record_count = subject.count
123
+ master_record_count = @golden_master.count
124
+ if subject_record_count != master_record_count
125
+ throw_no_match "Expected #{master_record_count} record(s), got #{subject_record_count}"
126
+ end
127
+
128
+ # Compare the values of the golden master with the subject
129
+ current_row = 0
130
+ @golden_master.each do |row|
131
+ row.each do |field, master_value|
132
+ subject_value = subject[current_row][field]
133
+ if !match_values?(master_value, subject_value)
134
+ throw_no_match "Field \"#{field}\", Record #{current_row + 1}: Expected value #{master_value.nil? ? '<NULL>' : "\"#{master_value}\""}, got #{subject_value.nil? ? '<NULL>' : "\"#{subject_value}\""}"
135
+ end
136
+ end
137
+ current_row += 1
138
+ end
139
+ end
140
+
141
+ def is_well_formed_query_result?(arr)
142
+ return false unless arr.kind_of? Array
143
+ header =[]
144
+ arr.find{|row| !row.kind_of?(Hash) || (header = header.empty? ? row.keys : header) != row.keys || row.keys.empty?}.nil?
145
+ end
146
+
147
+ def test_single_value_match(subject)
148
+ subject_value = subject[0].values[0]
149
+ subject_field = subject[0].keys[0]
150
+ if !match_types?(@golden_master, subject_value)
151
+ throw_no_match "Field \"#{subject_field}\", Record 1: Expected type \"#{@golden_master.class}\", got \"#{subject_value.class}\""
152
+ end
153
+
154
+ if !match_values?(@golden_master, subject_value)
155
+ throw_no_match "Field \"#{subject_field}\", Record 1: Expected value #{@golden_master.nil? ? '<NULL>' : "\"#{@golden_master}\""}, got #{subject_value.nil? ? '<NULL>' : "\"#{subject_value}\""}"
156
+ end
157
+ end
158
+
159
+ # In case of a multi-resultset subject, extract the first result set
160
+ def normalize_query_result_subject(subject)
161
+ subject.kind_of?(Array) && !subject.empty? && is_well_formed_query_result?(subject[0]) ? subject[0] : subject
162
+ end
163
+
164
+ def throw_no_match(message)
165
+ @failure_message = message
166
+ throw :no_match
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,69 @@
1
+ require 'slacker'
2
+ require 'slacker/query_result_matcher'
3
+ require 'csv'
4
+
5
+ module Slacker
6
+ module RSpecExt
7
+ def query(query_string, options = {}, log_name = nil)
8
+ log_name ||= Slacker.construct_log_name('query', query_string, options)
9
+ sql = Slacker.sql_from_query_string(query_string, options)
10
+ @results = Slacker.query_script(example, sql, log_name)
11
+ if block_given?
12
+ yield
13
+ end
14
+ @results
15
+ end
16
+
17
+ def result(index = 1, options = {})
18
+ # Flatten the result in case we're getting a multi-result-set response
19
+ res = case !@results.empty? && @results[0].kind_of?(Array)
20
+ when true
21
+ raise "The query result contains only #{@results.count} result set(s)" unless @results.count >= index
22
+ @results[index - 1]
23
+ else
24
+ raise "The query result contains a single result set" unless index == 1
25
+ @results
26
+ end
27
+
28
+ #Optionally extract the record and or the field
29
+ if options[:record] != nil
30
+ raise "The result set contains only #{res.count} record(s)" unless res.count >= options[:record]
31
+ res = res[options[:record] - 1]
32
+ if options[:field] != nil
33
+ raise "The result set does not contain field \"#{options[:field]}\"" unless res[options[:field]] != nil
34
+ res = res[options[:field]]
35
+ end
36
+ end
37
+
38
+ res
39
+ end
40
+
41
+ # Get a matcher which will compare the query results to a golden master
42
+ def match(golden_master)
43
+ QueryResultMatcher.new(Slacker.filter_golden_master(golden_master))
44
+ end
45
+
46
+ def load_csv(csv_file_or_object, table_name, log_name = nil)
47
+ log_name ||= "load_csv '#{csv_file_or_object.kind_of?(CSV::Table) ? 'CSV Object' : csv_file_or_object }', 'table_name'"
48
+ csv_object = case csv_file_or_object
49
+ when String then Slacker.get_csv(csv_file_or_object)
50
+ when CSV::Table then csv_file_or_object
51
+ when Array then Slacker.hash_array_to_csv(csv_file_or_object)
52
+ end
53
+
54
+ Slacker.load_csv(example, csv_object, table_name, log_name)
55
+ end
56
+
57
+ def csv(csv_file)
58
+ Slacker.get_csv(csv_file)
59
+ end
60
+
61
+ def sql
62
+ Slacker.sql(self)
63
+ end
64
+
65
+ def yes?(val)
66
+ val != nil && val.downcase == 'yes'
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,7 @@
1
+ # Monkeypatch a method to reset RSpec to reset it
2
+ module RSpec
3
+ def self.slacker_reset
4
+ @world = nil
5
+ @configuration = nil
6
+ end
7
+ end
@@ -0,0 +1,39 @@
1
+ require 'slacker'
2
+ require 'slacker/rspec_ext'
3
+
4
+ module Slacker
5
+ class Sql < BasicObject
6
+ attr_accessor :base_folder, :rspec_ext
7
+
8
+ def initialize(base_folder, rspec_ext)
9
+ @base_folder = base_folder
10
+ @rspec_ext = rspec_ext
11
+ end
12
+
13
+ def method_missing(method_name, *params, &block)
14
+ ::Kernel.raise "Slacker::Sql.rspec_ext not initialized" if rspec_ext.nil?
15
+ ::Kernel.raise "Missing folder #{base_folder}" if !::File.directory?(base_folder)
16
+
17
+ method_name = method_name.to_s
18
+
19
+ if ::File.directory?(::File.join(base_folder, method_name))
20
+ ::Slacker::Sql.new(::File.join(base_folder, method_name), rspec_ext)
21
+ else
22
+ sql_file = ::Slacker.sql_file_from_method_name(base_folder, method_name)
23
+ case sql_file
24
+ when nil
25
+ ::Kernel.raise "No SQL file found corresponding to method '#{method_name}' in folder #{base_folder}"
26
+ else
27
+ rspec_ext.query sql_file, *params, &block
28
+ end
29
+ end
30
+ end
31
+
32
+ def respond_to?(method_name)
33
+ method_name = method_name.to_s
34
+ ::Kernel.raise "Slacker::Sql.rspec_ext not initialized" if rspec_ext.nil?
35
+ ::File.directory?(::File.join(base_folder, method_name)) ||
36
+ !(::Slacker.sql_file_from_method_name(base_folder, method_name).nil?)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,17 @@
1
+ module Slacker
2
+ module StringHelper
3
+ class << self
4
+ def camelize(lower_case_and_underscored_word)
5
+ lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
6
+ end
7
+
8
+ def constantize(camel_cased_word)
9
+ unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ camel_cased_word
10
+ raise NameError, "#{camel_cased_word.inspect} is not a valid constant name!"
11
+ end
12
+
13
+ Object.module_eval("::#{$1}", __FILE__, __LINE__)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module Slacker
2
+ VERSION = "0.0.1"
3
+ end
data/slacker.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "slacker/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "slacker"
7
+ s.version = Slacker::VERSION
8
+ s.authors = ["Vassil Kovatchev"]
9
+ s.email = ["vassil.kovatchev@gmail.com"]
10
+ s.homepage = "https://github.com/vassilvk/slacker"
11
+ s.summary = %q{Behavior Driven Development for SQL Server}
12
+ s.description = %q{RSpec-based framework for developing automated tests for SQL Server programmable objects such as stored procedures and scalar/table functions}
13
+
14
+ s.rubyforge_project = "slacker"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency 'ruby-odbc', '=0.99994'
22
+ s.add_dependency 'rspec', '=2.5.0'
23
+ end
@@ -0,0 +1,14 @@
1
+ require 'slacker'
2
+
3
+ describe Slacker::Application do
4
+ it 'responds to run' do
5
+ Slacker.application.should respond_to(:run)
6
+ end
7
+
8
+ it 'responds to target_folder_structure with the correct list of folders' do
9
+ app = Slacker.application
10
+ app.should respond_to(:target_folder_structure)
11
+ folder_struct = app.target_folder_structure
12
+ folder_struct.should == ['data', 'debug', 'debug/passed_examples', 'debug/failed_examples', 'sql', 'spec', 'lib', 'lib/helpers']
13
+ end
14
+ end
@@ -0,0 +1,267 @@
1
+ require 'slacker'
2
+ require 'spec_helper'
3
+ require 'time'
4
+
5
+ describe Slacker::QueryResultMatcher do
6
+ def deep_copy(obj)
7
+ Marshal.load(Marshal.dump(obj))
8
+ end
9
+
10
+ before(:each) do
11
+ @subject = [{'Field 1' => 12, 'Field_2' => nil, 'b' => ''},
12
+ {'Field 1' => 'test string', 'Field_2' => Time.parse('1/1/2011'), 'b' => 8.9}]
13
+ end
14
+
15
+ shared_examples_for 'table-based matcher' do
16
+ it 'correctly rejects a non-matching query result based on wrong columns' do
17
+ one_column_too_few = deep_copy(@subject).each{|row| row.delete('b')}
18
+ one_column_too_many = deep_copy(@subject).each{|row| row['new column'] = 'val 1'}
19
+ wrong_column_name = deep_copy(@subject).each{|row| row.delete('Field_2'); row['Field_x'] = 'val x'}
20
+
21
+ @matcher.matches?(one_column_too_few).should be_false
22
+ @matcher.failure_message_for_should.should == 'Expected 3 field(s), got 2'
23
+
24
+ @matcher.matches?(one_column_too_many).should be_false
25
+ @matcher.failure_message_for_should.should == 'Expected 3 field(s), got 4'
26
+
27
+ @matcher.matches?(wrong_column_name).should be_false
28
+ @matcher.failure_message_for_should.should == 'Expected field "Field_2", got field "b"'
29
+ end
30
+
31
+ it 'correctly rejects a non-matching query result based on number of records' do
32
+ one_row_too_few = deep_copy(@subject)
33
+ one_row_too_few.shift
34
+ one_row_too_many = deep_copy(@subject) << @subject[0]
35
+ empty_query_result = []
36
+
37
+ @matcher.matches?(one_row_too_few).should be_false
38
+ @matcher.failure_message_for_should.should == 'Expected 2 record(s), got 1'
39
+
40
+ @matcher.matches?(one_row_too_many).should be_false
41
+ @matcher.failure_message_for_should.should == 'Expected 2 record(s), got 3'
42
+
43
+ @matcher.matches?(empty_query_result).should be_false
44
+ @matcher.failure_message_for_should.should == 'Expected 2 record(s), got 0'
45
+ end
46
+
47
+ it 'correctly rejects a non-matching query result based on a value' do
48
+ wrong_int = deep_copy(@subject)
49
+ wrong_int[0]['Field 1'] = 14
50
+
51
+ wrong_int_type = deep_copy(@subject)
52
+ wrong_int_type[1]['Field 1'] = 14
53
+
54
+ wrong_nil = deep_copy(@subject)
55
+ wrong_nil[0]['Field 1'] = nil
56
+
57
+ wrong_non_nil = deep_copy(@subject)
58
+ wrong_non_nil[0]['Field_2'] = 'whatever'
59
+
60
+ misplaced_fields = deep_copy(@subject)
61
+ misplaced_fields[0].delete('Field 1')
62
+ misplaced_fields[1].delete('Field 1')
63
+ misplaced_fields[0]['Field 1'] = 12
64
+ misplaced_fields[1]['Field 1'] = 'test string'
65
+
66
+ @matcher.matches?(wrong_int).should be_false
67
+ @matcher.failure_message_for_should.should == 'Field "Field 1", Record 1: Expected value "12", got "14"'
68
+
69
+
70
+ @matcher.matches?(wrong_int_type).should be_false
71
+ @matcher.failure_message_for_should.should == 'Field "Field 1", Record 2: Expected value "test string", got "14"'
72
+
73
+ @matcher.matches?(wrong_nil).should be_false
74
+ @matcher.failure_message_for_should.should == 'Field "Field 1", Record 1: Expected value "12", got <NULL>'
75
+
76
+ @matcher.matches?(wrong_non_nil).should be_false
77
+ @matcher.failure_message_for_should.should == 'Field "Field_2", Record 1: Expected value <NULL>, got "whatever"'
78
+
79
+ @matcher.matches?(misplaced_fields).should be_false
80
+ @matcher.failure_message_for_should.should == 'Expected field "Field 1", got field "Field_2"'
81
+ end
82
+
83
+ it 'correctly compares with a query result' do
84
+ @matcher.matches?(@subject).should be_true(@matcher.failure_message_for_should)
85
+ end
86
+
87
+ it 'only accepts well formed query result subjects' do
88
+ expected_message = /Can perform query matches only against a well formed query result subject/
89
+ @matcher.matches?(1).should be_false
90
+ @matcher.failure_message_for_should.should =~ expected_message
91
+
92
+ @matcher.matches?('test string').should be_false
93
+ @matcher.failure_message_for_should.should =~ expected_message
94
+
95
+ @matcher.matches?(1.1).should be_false
96
+ @matcher.failure_message_for_should.should =~ expected_message
97
+
98
+ @matcher.matches?(Time.now).should be_false
99
+ @matcher.failure_message_for_should.should =~ expected_message
100
+
101
+ @matcher.matches?(nil).should be_false
102
+ @matcher.failure_message_for_should.should =~ expected_message
103
+
104
+ @matcher.matches?([{:f1 => 'x', :f2 => 'y'}, {:f1 => 'x'}]).should be_false
105
+ @matcher.failure_message_for_should.should =~ expected_message
106
+
107
+ @matcher.matches?([{:f1 => 'x', :f2 => 'y'}, {:f1 => 'x', :f3 => 'y'}]).should be_false
108
+ @matcher.failure_message_for_should.should =~ expected_message
109
+
110
+ @matcher.matches?([{:f1 => 'x', :f2 => 'y'}, {:f1 => 'x', :f2 => 'y', :f3 => 'z'}]).should be_false
111
+ @matcher.failure_message_for_should.should =~ expected_message
112
+
113
+ @matcher.matches?([{:f1 => 'x', :f2 => 'y'}, 12]).should be_false
114
+ @matcher.failure_message_for_should.should =~ expected_message
115
+
116
+ @matcher.matches?([12, {:f1 => 'x', :f2 => 'y'}]).should be_false
117
+ @matcher.failure_message_for_should.should =~ expected_message
118
+
119
+ # An array with a single empty row is not a well-formed query result
120
+ @matcher.matches?([{}]).should be_false
121
+ @matcher.failure_message_for_should.should =~ expected_message
122
+ end
123
+
124
+ end
125
+
126
+ describe 'CSV-based golden master' do
127
+ before(:each) do
128
+ @matcher = Slacker::QueryResultMatcher.new(SpecHelper.load_csv('matcher/test_1.csv'))
129
+ end
130
+
131
+ it_behaves_like 'table-based matcher'
132
+
133
+ it 'correctly compares an empty subject with an empty CSV file' do
134
+ matcher = Slacker::QueryResultMatcher.new(SpecHelper.load_csv('matcher/no_rows.csv'))
135
+ matcher.matches?([]).should be_true(matcher.failure_message_for_should)
136
+
137
+ matcher = Slacker::QueryResultMatcher.new(SpecHelper.load_csv('matcher/completely_blank.csv'))
138
+ matcher.matches?([]).should be_true(matcher.failure_message_for_should)
139
+ end
140
+ end
141
+
142
+ describe 'Array-based golden master' do
143
+ before(:each) do
144
+ @matcher = Slacker::QueryResultMatcher.new(deep_copy(@subject))
145
+ end
146
+
147
+ it_behaves_like 'table-based matcher'
148
+
149
+ it 'only accepts well formed query result golden master' do
150
+ expected_message = /Cannot match against a non-well formed golden master array/
151
+
152
+ matcher = Slacker::QueryResultMatcher.new([{:f1 => 'x', :f2 => 'y'}, {:f1 => 'x'}])
153
+ matcher.matches?([]).should be_false
154
+ matcher.failure_message_for_should.should =~ expected_message
155
+
156
+ matcher = Slacker::QueryResultMatcher.new([{:f1 => 'x', :f2 => 'y'}, {:f1 => 'x', :f3 => 'y'}])
157
+ matcher.matches?([]).should be_false
158
+ matcher.failure_message_for_should.should =~ expected_message
159
+
160
+ matcher = Slacker::QueryResultMatcher.new([{:f1 => 'x', :f2 => 'y'}, {:f1 => 'x', :f2 => 'y', :f3 => 'z'}])
161
+ matcher.matches?([]).should be_false
162
+ matcher.failure_message_for_should.should =~ expected_message
163
+
164
+ matcher = Slacker::QueryResultMatcher.new([{:f1 => 'x', :f2 => 'y'}, 12])
165
+ matcher.matches?([]).should be_false
166
+ matcher.failure_message_for_should.should =~ expected_message
167
+
168
+ matcher = Slacker::QueryResultMatcher.new([12, {:f1 => 'x', :f2 => 'y'}])
169
+ matcher.matches?([]).should be_false
170
+ matcher.failure_message_for_should.should =~ expected_message
171
+ end
172
+
173
+ it 'correctly compares an empty subject with an empty golden master' do
174
+ matcher = Slacker::QueryResultMatcher.new([])
175
+ matcher.matches?([]).should be_true(matcher.failure_message_for_should)
176
+ end
177
+ end
178
+
179
+ shared_examples_for 'single-value-based matcher' do
180
+ it 'should correctly match the first value in the result set' do
181
+ matcher = Slacker::QueryResultMatcher.new(@correct_golden_master)
182
+ matcher.matches?(@subject).should be_true(matcher.failure_message_for_should)
183
+ end
184
+
185
+ it 'should correctly reject a non-matching first item by value' do
186
+ if !@wrong_value_golden_master.nil? #Skip nil from value check when the master is nil
187
+ matcher = Slacker::QueryResultMatcher.new(@wrong_value_golden_master)
188
+ matcher.matches?(@subject).should be_false
189
+ matcher.failure_message_for_should.should =~ /Expected value "#{@wrong_value_golden_master}", got "#{@subject[0].values[0]}"/
190
+ end
191
+ end
192
+
193
+ it 'should correctly reject a non-matching first item by type' do
194
+ matcher = Slacker::QueryResultMatcher.new(@wrong_type_golden_master)
195
+ matcher.matches?(@subject).should be_false
196
+ matcher.failure_message_for_should.should =~ /Expected type "#{@wrong_type_golden_master.class}", got \"#{@subject[0].values[0].class}\"/
197
+ end
198
+ end
199
+
200
+ describe 'Integer-based golden master' do
201
+ before(:each) do
202
+ @subject = [{'Field 1' => 14, 'Field_2' => 'whatever'}]
203
+ @correct_golden_master = 14
204
+ @wrong_value_golden_master = 15
205
+ @wrong_type_golden_master = 'whatever'
206
+ end
207
+
208
+ it_behaves_like 'single-value-based matcher'
209
+ end
210
+
211
+ describe 'String-based golden master' do
212
+ before(:each) do
213
+ @subject = [{'Field 1' => 'test string', 'Field_2' => 12}]
214
+ @correct_golden_master = 'test string'
215
+ @wrong_value_golden_master = 'whatever'
216
+ @wrong_type_golden_master = 15
217
+ end
218
+
219
+ it_behaves_like 'single-value-based matcher'
220
+ end
221
+
222
+ describe 'Floating point-based golden master' do
223
+ before(:each) do
224
+ @subject = [{'Field 1' => 18.2, 'Field_2' => 12}]
225
+ @correct_golden_master = 18.2
226
+ @wrong_value_golden_master = 18.7
227
+ @wrong_type_golden_master = 15
228
+ end
229
+
230
+ it_behaves_like 'single-value-based matcher'
231
+ end
232
+
233
+ describe 'Date-based golden master' do
234
+ before(:each) do
235
+ @subject = [{'Field 1' => Time.parse('1/1/2011'), 'Field_2' => 12}]
236
+ @correct_golden_master = Time.parse('1/1/2011')
237
+ @wrong_value_golden_master = Time.parse('2/1/2011')
238
+ @wrong_type_golden_master = 'whatever'
239
+ end
240
+
241
+ it_behaves_like 'single-value-based matcher'
242
+ end
243
+
244
+ describe 'Nil-based golden master' do
245
+ before(:each) do
246
+ @subject = [{'Field 1' => nil, 'Field_2' => 12}]
247
+ @correct_golden_master = nil
248
+ @wrong_value_golden_master = nil # There is no wrong value for nil-based master
249
+ @wrong_type_golden_master = 'whatever'
250
+ end
251
+
252
+ it_behaves_like 'single-value-based matcher'
253
+ end
254
+
255
+ it 'correctly matches a multi-result subject' do
256
+ subject = [@subject, [{:Field_x => 12}]]
257
+ matcher = Slacker::QueryResultMatcher.new(deep_copy(@subject))
258
+ matcher.matches?(subject).should be_true(matcher.failure_message_for_should)
259
+ end
260
+
261
+ it 'correctly rejects a wrong multi-result subject' do
262
+ subject = [[{:Field_x => 12}], @subject]
263
+ matcher = Slacker::QueryResultMatcher.new(deep_copy(@subject))
264
+ matcher.matches?(subject).should be_false
265
+ matcher.failure_message_for_should.should == 'Expected 3 field(s), got 1'
266
+ end
267
+ end