whitestone 1.0.0

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.
@@ -0,0 +1,14 @@
1
+
2
+ # A convenient way to include the Whitestone methods like T, Eq, etc. in the top
3
+ # level.
4
+ #
5
+ # Long way:
6
+ # require 'whitestone'
7
+ # include Whitestone
8
+ #
9
+ # Short way:
10
+ # require 'whitestone/include'
11
+ #
12
+
13
+ require 'whitestone'
14
+ include Whitestone
@@ -0,0 +1,335 @@
1
+
2
+ require 'stringio'
3
+ require 'col'
4
+
5
+ module Whitestone
6
+
7
+ # --------------------------------------------------------------section---- #
8
+ # #
9
+ # Whitestone::Output #
10
+ # #
11
+ # Contains all code that writes to the console #
12
+ # #
13
+ # ------------------------------------------------------------------------- #
14
+
15
+ class Output
16
+
17
+ def initialize
18
+ # @buf is the buffer into which we write details of errors and failures so
19
+ # that they can be emitted to the console all together.
20
+ @buf = StringIO.new
21
+ # @@files is a means of printing lines of code in failure and error
22
+ # details.
23
+ @@files ||= Hash.new { |h,k| h[k] = File.readlines(k) rescue nil }
24
+ @filter_backtrace_yn = true
25
+ end
26
+
27
+ def set_full_backtrace
28
+ @filter_backtrace_yn = false
29
+ end
30
+
31
+ def Output.relative_path(path)
32
+ @current_dir ||= Dir.pwd
33
+ path.sub(@current_dir, '.')
34
+ end
35
+
36
+
37
+
38
+ ##
39
+ # Print the name and result of each test, using indentation.
40
+ # This must be done after execution is finished in order to get the tree
41
+ # structure right.
42
+ def display_test_by_test_result(top_level)
43
+ pipe = "|"
44
+ space = " "
45
+ #empty_line = space + pipe + (space * 76) + pipe
46
+ header = Col[" +----- Report " + "-" * (77-14) + "+"].cb
47
+ empty_line = Col[space, pipe, space * 76, pipe].fmt(:_, :cb, :_, :cb)
48
+ line = lambda { |desc,c1,s1,result,c2,s2|
49
+ padding = space * ( 77 - (1 + desc.size + result.size ) )
50
+ Col.inline( space, :_, pipe, :cb, desc, [c1,s1], \
51
+ result, [c2,s2], padding, :_, pipe, :cb)
52
+ }
53
+ footer = Col[" +#{'-'*76}+"].cb
54
+
55
+ puts
56
+ puts header
57
+
58
+ tree_walk(top_level.tests) do |test, level|
59
+ description = (space + space + " " * level + test.description).ljust(67)
60
+ description = description[0...67]
61
+ colour1, style1 =
62
+ case test.result
63
+ when :pass then [:_, :_]
64
+ when :fail then [:red, :bold]
65
+ when :error then [:magenta, :bold]
66
+ when :blank then [:_, :_]
67
+ end
68
+ result, colour2, style2 =
69
+ case test.result
70
+ when :pass then ['PASS', :green, :bold]
71
+ when :fail then ['FAIL', :red, :bold]
72
+ when :error then ['ERROR', :magenta, :bold]
73
+ when :blank then ['-', :green, :bold]
74
+ end
75
+ result = " " + result
76
+ if level == 0
77
+ puts empty_line
78
+ colour1 = (test.passed? or test.blank?) ? :yellow : colour2
79
+ style1 = :bold
80
+ end
81
+ puts line[description, colour1, style1, result, colour2, style2]
82
+ end
83
+
84
+ puts empty_line
85
+ puts footer
86
+ end
87
+
88
+ # Yield each test and its children (along with the current level 0,1,2,...)
89
+ # in depth-first order.
90
+ def tree_walk(tests, level=0, &block)
91
+ tests.each do |test|
92
+ block.call(test, level)
93
+ unless test.children.empty?
94
+ tree_walk( test.children, level+1, &block )
95
+ end
96
+ end
97
+ end
98
+ private :tree_walk
99
+
100
+
101
+
102
+ def display_details_of_failures_and_errors
103
+ unless @buf.string.strip.empty?
104
+ puts
105
+ puts @buf.string
106
+ end
107
+ end
108
+
109
+
110
+
111
+ # Prepares and displays a colourful summary message saying how many tests
112
+ # have passed, failed and errored.
113
+ def display_results_npass_nfail_nerror_etc(stats)
114
+ npass = stats[:pass] || 0
115
+ nfail = stats[:fail] || 0
116
+ nerror = stats[:error] || 0
117
+ overall = (nfail + nerror > 0) ? :FAIL : :PASS
118
+ time = stats[:time]
119
+ assertions = stats[:assertions]
120
+
121
+ overall_str = overall.to_s.ljust(9)
122
+ npass_str = sprintf "#pass: %-6d", npass
123
+ nfail_str = sprintf "#fail: %-6d", nfail
124
+ nerror_str = sprintf "#error: %-6d", nerror
125
+ assertions_str = sprintf "assertions: %-6d", assertions
126
+ time_str = sprintf "time: %3.3f", time
127
+
128
+ overall_col = (overall == :PASS) ? :green : :red
129
+ npass_col = :green
130
+ nfail_col = (nfail > 0) ? :red : :green
131
+ nerror_col = (nerror > 0) ? :magenta : :green
132
+ assertions_col = :white
133
+ time_col = :white
134
+
135
+ coloured_info = Col.inline(
136
+ overall_str, [overall_col, :bold],
137
+ npass_str, [npass_col, :bold],
138
+ nfail_str, [nfail_col, :bold],
139
+ nerror_str, [nerror_col, :bold],
140
+ assertions_str, [assertions_col, :bold],
141
+ time_str, [time_col, :bold]
142
+ )
143
+
144
+ equals = Col["=" * 80].fmt [overall_col, :bold]
145
+ nl = "\n"
146
+
147
+ output = String.new.tap { |str|
148
+ str << equals << nl
149
+ str << " " << coloured_info << nl
150
+ str << equals << nl
151
+ }
152
+
153
+ puts
154
+ puts output
155
+ end
156
+
157
+
158
+
159
+ def report_failure(description, message, backtrace)
160
+ message ||= "No message! #{__FILE__}:#{__LINE__}"
161
+ bp = BacktraceProcessor.new(backtrace, @filter_backtrace_yn)
162
+
163
+ # Determine the file and line number of the failed assertion, and extract
164
+ # the code surrounding that line.
165
+ file, line = bp.determine_file_and_lineno()
166
+ code = extract_code(file, line)
167
+
168
+ # Emit the failure report.
169
+ @buf.puts
170
+ @buf.puts Col["FAIL: #{description}"].rb
171
+ @buf.puts code.___indent(4) if code
172
+ @buf.puts message.___indent(2)
173
+ @buf.puts " Backtrace\n" + bp.backtrace.join("\n").___indent(4)
174
+ end # report_failure
175
+
176
+
177
+
178
+ def report_uncaught_exception(description, exception, _calls, force_filter_bt=false)
179
+ filter_yn = @filter_backtrace_yn || force_filter_bt
180
+ bp = BacktraceProcessor.new(exception.backtrace, filter_yn)
181
+
182
+ # Determine the current test file, the line number that triggered the
183
+ # error, and extract the code surrounding that line.
184
+ file, line = bp.determine_file_and_lineno(_calls)
185
+ code = extract_code(file, line)
186
+
187
+ # Emit the error report.
188
+ @buf.puts
189
+ @buf.puts Col("ERROR: #{description}").fmt(:mb)
190
+ @buf.puts code.___indent(4) if code
191
+ @buf.puts Col.inline(" Class: ", :mb, exception.class, :yb)
192
+ @buf.puts Col.inline(" Message: ", :mb, exception.message, :yb)
193
+ @buf.puts " Backtrace\n" + bp.backtrace.join("\n").___indent(4)
194
+ end # report_uncaught_exception
195
+
196
+
197
+
198
+ def report_specification_error(e)
199
+ puts
200
+ puts "You have made an error in specifying one of your assertions."
201
+ puts "Details below; can't continue; exiting."
202
+ puts
203
+ puts Col.inline("Message: ", :_, e.message, :yb)
204
+ puts
205
+ puts "Filtered backtrace:"
206
+ filtered = BacktraceProcessor.new(e.backtrace, true).backtrace
207
+ puts filtered.join("\n").___indent(2)
208
+ puts
209
+ puts "Full backtrace:"
210
+ puts e.backtrace.join("\n").___indent(2)
211
+ puts
212
+ end
213
+
214
+
215
+
216
+ def extract_code(file, line)
217
+ return nil if file.nil? or line.nil? or file == "(eval)"
218
+ if source = @@files[file]
219
+ line = line.to_i
220
+ radius = 2 # number of surrounding lines to show
221
+ region1 = [line - radius, 1].max .. [line - 1, 1].max
222
+ region2 = [line]
223
+ region3 = [line + 1, source.length].min .. [line + radius, source.length].min
224
+
225
+ # ensure proper alignment by zero-padding line numbers
226
+ format = "%2s %0#{region3.last.to_s.length}d %s"
227
+
228
+ pretty1 = region1.map { |n|
229
+ format % [nil, n, source[n-1].chomp.___truncate(60)]
230
+ }
231
+ pretty2 = region2.map { |n|
232
+ string = format % ['=>', n, source[n-1].chomp.___truncate(60)]
233
+ Col[string].fmt(:yb)
234
+ }
235
+ pretty3 = region3.map { |n|
236
+ format % [nil, n, source[n-1].chomp.___truncate(60)]
237
+ }
238
+ pretty = pretty1 + pretty2 + pretty3
239
+
240
+ pretty.unshift Col[Output.relative_path(file)].y
241
+
242
+ pretty.join("\n")
243
+ end
244
+ end # extract_code
245
+ private :extract_code
246
+
247
+ end # module Output
248
+
249
+
250
+
251
+ # --------------------------------------------------------------section---- #
252
+ # #
253
+ # Output::BacktraceProcessor #
254
+ # #
255
+ # ------------------------------------------------------------------------- #
256
+
257
+ class Output
258
+ class BacktraceProcessor
259
+ INTERNALS_RE = (
260
+ libdir = File.dirname(__FILE__)
261
+ bindir = "bin/whitestone"
262
+ Regexp.union(libdir, bindir)
263
+ )
264
+
265
+ def initialize(backtrace, filter_yn)
266
+ @backtrace = filter(backtrace, filter_yn)
267
+ end
268
+
269
+ # +calls+ is an array of proc objects (scopes of the tests run so far,
270
+ # from top level to current nesting). The last of them is the context for
271
+ # the current test, like
272
+ #
273
+ # #<Proc:0x10b7b1b8@./test/shape/triangle/construct/various.rb:52>
274
+ #
275
+ # From this, we determine the current test file. _Then_ we look in the
276
+ # backtrace for the last mention of that test file. That stack frame,
277
+ # from the backtrace, tells us what line of code in the test file caused
278
+ # the failure/error.
279
+ #
280
+ # If _calls_ is not provided, we simply take the first frame of the
281
+ # backtrace. That will happen when reporting a _failure_. A failure is
282
+ # simply a false assertion, so no stack unwinding is necessary.
283
+ #
284
+ # If no appropriate frame is found (not sure why that would be), then the
285
+ # return values will be nil.
286
+ #
287
+ # Return:: file (String) and line number (Integer)
288
+ def determine_file_and_lineno(calls=nil)
289
+ frame =
290
+ if calls
291
+ current_test_file = calls.last.to_s.scan(/@(.+?):/).flatten.first
292
+ @backtrace.find { |str| str.index(current_test_file) }
293
+ else
294
+ @backtrace.first
295
+ end
296
+ file, line =
297
+ if frame
298
+ file, line = frame.scan(/(.+?):(\d+(?=:|\z))/).first
299
+ [file, line.to_i]
300
+ end
301
+ end
302
+
303
+ # Returns the backtrace (array of strings) with all paths converted to
304
+ # relative paths (where possible).
305
+ def backtrace
306
+ make_relative(@backtrace)
307
+ end
308
+
309
+ def filter(backtrace, filter_yn)
310
+ # TODO: remove (or update and move) following comment.
311
+ #
312
+ # It's up to the user whether we filter backtraces. That's set in
313
+ # @filter_backtrace_yn. However, there are times when it makes sense to
314
+ # _force_ a filter (if an AssertionSpecificationError is raised). That's
315
+ # what the parameter force_filter is for; it can override
316
+ # @filter_backtrace_yn.
317
+ if filter_yn
318
+ backtrace.reject { |str| str =~ INTERNALS_RE }.uniq
319
+ else
320
+ backtrace.dup
321
+ end
322
+ end
323
+ private :filter
324
+
325
+ # Turn absolute paths into relative paths if possible, to save space and
326
+ # ease reading.
327
+ def make_relative(backtrace)
328
+ backtrace.map { |path| Output.relative_path(path) }
329
+ end
330
+ private :make_relative
331
+
332
+ end # class BacktraceProcessor
333
+ end # module Output
334
+ end # module Whitestone
335
+
@@ -0,0 +1,29 @@
1
+
2
+ class String
3
+
4
+ def ___indent(n)
5
+ if n >= 0
6
+ gsub(/^/, ' ' * n)
7
+ else
8
+ gsub(/^ {0,#{-n}}/, "")
9
+ end
10
+ end
11
+
12
+ def ___truncate(n)
13
+ str = self
14
+ if str.length > n
15
+ str[0...n] + "..."
16
+ else
17
+ str
18
+ end
19
+ end
20
+
21
+ def ___margin # adapted from 'facets' project
22
+ d = ((/\A.*\n\s*(.)/.match(self)) ||
23
+ (/\A\s*(.)/.match(self)))[1]
24
+ return '' unless d
25
+ gsub(/\n\s*\Z/,'').gsub(/^\s*[#{d}]/, '')
26
+ end
27
+
28
+ end
29
+
@@ -0,0 +1,3 @@
1
+ module Whitestone
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,5 @@
1
+
2
+ # This line is useful for testing Whitestone, but it doesn't belong in released
3
+ # code.
4
+ #
5
+ #require 'ruby-debug'
@@ -0,0 +1,120 @@
1
+ require 'date'
2
+
3
+ # In this test file, we create a simple Person class so that we have something
4
+ # to test custom assertions on.
5
+ #
6
+ # We implement CSV parsing to give a little realism to the scenario:
7
+ # * we have a domain object (Person)
8
+ # * we expect to create a lot of these in some way (CSV) and that these
9
+ # objects will play a large part in our system
10
+ # * we will therefore need to check the correctness of lots of these objects
11
+ # * therefore, a custom assertion is handy
12
+
13
+ # Person consists of name (first, middle, last) and date of birth.
14
+ # Create them directly via Person.new(...) or indirectly by Person.from_csv "..."
15
+ class Person
16
+ attr_accessor :first, :middle, :last
17
+ attr_accessor :dob
18
+ def initialize(f, m, l, dob)
19
+ @first, @middle, @last, @dob = f, m, l, dob
20
+ end
21
+ # Reads multiple lines of CSV and returns an array of Person objects.
22
+ def Person.from_csv(text)
23
+ text.strip.split("\n").map { |line|
24
+ f, m, l, dob = line.strip.split(",")
25
+ m = nil if m.empty?
26
+ dob = Date.parse(dob)
27
+ Person.new(f,m,l,dob)
28
+ }
29
+ end
30
+ end
31
+
32
+ D "Custom assertions" do
33
+ D "Create :person custom assertion" do
34
+ E! do
35
+ Whitestone.custom :person, {
36
+ :description => "Person equality",
37
+ :parameters => [ [:person, Person], [:string, String] ],
38
+ :run => proc {
39
+ f, m, l, dob = string.split
40
+ m = nil if m == '-'
41
+ dob = Date.parse(dob)
42
+ test('first') { Eq person.first, f }
43
+ test('middle') { Eq person.middle, m }
44
+ test('last') { Eq person.last, l }
45
+ test('dob') { Eq person.dob, dob }
46
+ }
47
+ }
48
+ end
49
+ end
50
+
51
+ D "Use :person custom assertion" do
52
+ D.< do
53
+ @people = Person.from_csv %{
54
+ John,William,Smith,1974-03-19
55
+ Jane,,Galois,1941-12-23
56
+ Hans,Dieter,Flich,1963-11-01,
57
+ }
58
+ end
59
+
60
+ D "manual check that people were created properly" do
61
+ person = @people[1]
62
+ Eq person.first, "Jane"
63
+ N person.middle
64
+ Eq person.last, "Galois"
65
+ Eq person.dob, Date.new(1941,12,23)
66
+ end
67
+
68
+ D "check all three people using :person custom assertion" do
69
+ T :person, @people[0], 'John William Smith 1974-03-19'
70
+ T :person, @people[1], 'Jane - Galois 1941-12-23'
71
+ T :person, @people[2], 'Hans Dieter Flich 1963-11-01'
72
+ end
73
+ end
74
+
75
+ D "correct message when a failure occurs" do
76
+ @c = Term::ANSIColor
77
+ D.< do
78
+ @person = Person.new("Terrence", "James", "Hu", Date.new(1981,10,27))
79
+ end
80
+ D "in 'first' field" do
81
+ # In testing this person object, we'll accidentally mispell the first name,
82
+ # expect an error, and check that the message identifies the field ("first").
83
+ E { T :person, @person, "Terence James Hu 1981-10-27" }
84
+ Ko Whitestone.exception, Whitestone::FailureOccurred
85
+ message = @c.uncolored(Whitestone.exception.message)
86
+ Mt message, /Person equality test failed: first \(details below\)/
87
+ end
88
+ D "in 'middle' field" do
89
+ E { T :person, @person, "Terrence Janes Hu 1981-10-27" }
90
+ Ko Whitestone.exception, Whitestone::FailureOccurred
91
+ message = @c.uncolored(Whitestone.exception.message)
92
+ Mt message, /Person equality test failed: middle \(details below\)/
93
+ end
94
+ D "in 'last' field" do
95
+ E { T :person, @person, "Terrence James Hux 1981-10-27" }
96
+ Ko Whitestone.exception, Whitestone::FailureOccurred
97
+ message = @c.uncolored(Whitestone.exception.message)
98
+ Mt message, /Person equality test failed: last \(details below\)/
99
+ end
100
+ D "in 'dob' field" do
101
+ E { T :person, @person, "Terrence James Hu 1993-02-28" }
102
+ Ko Whitestone.exception, Whitestone::FailureOccurred
103
+ message = @c.uncolored(Whitestone.exception.message)
104
+ Mt message, /Person equality test failed: dob \(details below\)/
105
+ end
106
+ end
107
+
108
+ D "check correct number of assertions" do
109
+ # We are checking that the three 'T :person' assertions above only count as
110
+ # three assertions, that their consituent primitive assertions are not added
111
+ # to the total.
112
+ person = Person.new("Henrietta", nil, "Evangalier", Date.parse('2002-04-09'))
113
+ assertions = Whitestone.stats[:assertions]
114
+ T :person, person, "Henrietta - Evangalier 2002-04-09"
115
+ T :person, person, "Henrietta - Evangalier 2002-04-09"
116
+ T :person, person, "Henrietta - Evangalier 2002-04-09"
117
+ assertions = Whitestone.stats[:assertions] - assertions
118
+ Eq assertions, 3
119
+ end
120
+ end