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
@@ -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