cucumber 2.0.0.beta.2 → 2.0.0.beta.3

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +25 -7
  3. data/cucumber.gemspec +1 -1
  4. data/features/docs/defining_steps/nested_steps.feature +0 -1
  5. data/features/docs/defining_steps/printing_messages.feature +1 -0
  6. data/features/docs/defining_steps/table_diffing.feature +9 -4
  7. data/features/docs/exception_in_after_step_hook.feature +1 -0
  8. data/features/docs/formatters/json_formatter.feature +51 -4
  9. data/features/docs/formatters/junit_formatter.feature +1 -0
  10. data/features/docs/gherkin/outlines.feature +4 -0
  11. data/features/docs/output_from_hooks.feature +128 -0
  12. data/features/docs/wire_protocol_table_diffing.feature +6 -2
  13. data/features/docs/writing_support_code/after_hooks.feature +56 -0
  14. data/lib/cucumber/cli/configuration.rb +0 -4
  15. data/lib/cucumber/cli/main.rb +0 -1
  16. data/lib/cucumber/cli/options.rb +0 -3
  17. data/lib/cucumber/formatter/console.rb +3 -1
  18. data/lib/cucumber/formatter/debug.rb +4 -0
  19. data/lib/cucumber/formatter/gherkin_formatter_adapter.rb +26 -3
  20. data/lib/cucumber/formatter/html.rb +6 -2
  21. data/lib/cucumber/formatter/usage.rb +1 -58
  22. data/lib/cucumber/mappings.rb +25 -7
  23. data/lib/cucumber/multiline_argument.rb +40 -82
  24. data/lib/cucumber/multiline_argument/data_table.rb +719 -0
  25. data/lib/cucumber/multiline_argument/doc_string.rb +10 -0
  26. data/lib/cucumber/platform.rb +1 -1
  27. data/lib/cucumber/rb_support/rb_world.rb +2 -4
  28. data/lib/cucumber/reports/legacy_formatter.rb +69 -22
  29. data/lib/cucumber/runtime.rb +0 -39
  30. data/lib/cucumber/runtime/for_programming_languages.rb +12 -10
  31. data/lib/cucumber/runtime/support_code.rb +11 -4
  32. data/lib/cucumber/wire_support/wire_protocol/requests.rb +2 -2
  33. data/spec/cucumber/formatter/pretty_spec.rb +5 -5
  34. data/spec/cucumber/mappings_spec.rb +137 -8
  35. data/spec/cucumber/multiline_argument/data_table_spec.rb +508 -0
  36. data/spec/cucumber/rb_support/rb_step_definition_spec.rb +3 -3
  37. data/spec/cucumber/rb_support/snippet_spec.rb +1 -1
  38. data/spec/cucumber/runtime/for_programming_languages_spec.rb +16 -12
  39. metadata +13 -6
  40. data/lib/cucumber/runtime/features_loader.rb +0 -62
@@ -0,0 +1,719 @@
1
+ require 'gherkin/formatter/escaping'
2
+ require 'cucumber/core/ast/describes_itself'
3
+
4
+ module Cucumber
5
+ module MultilineArgument
6
+ # Step Definitions that match a plain text Step with a multiline argument table
7
+ # will receive it as an instance of Table. A Table object holds the data of a
8
+ # table parsed from a feature file and lets you access and manipulate the data
9
+ # in different ways.
10
+ #
11
+ # For example:
12
+ #
13
+ # Given I have:
14
+ # | a | b |
15
+ # | c | d |
16
+ #
17
+ # And a matching StepDefinition:
18
+ #
19
+ # Given /I have:/ do |table|
20
+ # data = table.raw
21
+ # end
22
+ #
23
+ # This will store <tt>[['a', 'b'], ['c', 'd']]</tt> in the <tt>data</tt> variable.
24
+ #
25
+ class DataTable
26
+ class Different < StandardError
27
+ def initialize(table)
28
+ super("Tables were not identical:\n#{table}")
29
+ end
30
+ end
31
+
32
+ class Builder
33
+ attr_reader :rows
34
+
35
+ def initialize
36
+ @rows = []
37
+ end
38
+
39
+ def row(row, line_number)
40
+ @rows << row
41
+ end
42
+
43
+ def eof
44
+ end
45
+ end
46
+
47
+ include Enumerable
48
+ include Core::Ast::DescribesItself
49
+
50
+ NULL_CONVERSIONS = Hash.new({ :strict => false, :proc => lambda{ |cell_value| cell_value } }).freeze
51
+
52
+ attr_accessor :file
53
+
54
+ def self.default_arg_name #:nodoc:
55
+ "table"
56
+ end
57
+
58
+ def self.parse(text, uri, offset)
59
+ builder = Builder.new
60
+ lexer = Gherkin::Lexer::I18nLexer.new(builder)
61
+ lexer.scan(text)
62
+ new(builder.rows)
63
+ end
64
+
65
+ # Creates a new instance. +raw+ should be an Array of Array of String
66
+ # or an Array of Hash (similar to what #hashes returns).
67
+ # You don't typically create your own Table objects - Cucumber will do
68
+ # it internally and pass them to your Step Definitions.
69
+ #
70
+ def initialize(data, conversion_procs = NULL_CONVERSIONS.dup, header_mappings = {}, header_conversion_proc = nil)
71
+ ast_table = case data
72
+ when Core::Ast::DataTable
73
+ data
74
+ when Array
75
+ Core::Ast::DataTable.new(data, Core::Ast::Location.of_caller)
76
+ end
77
+ # Verify that it's square
78
+ ast_table.transpose
79
+ @cell_matrix = create_cell_matrix(ast_table)
80
+ @conversion_procs = conversion_procs
81
+ @header_mappings = header_mappings
82
+ @header_conversion_proc = header_conversion_proc
83
+ @ast_table = ast_table
84
+ end
85
+
86
+ def append_to(array)
87
+ array << self
88
+ end
89
+
90
+ def to_step_definition_arg
91
+ dup
92
+ end
93
+
94
+ # Creates a copy of this table, inheriting any column and header mappings
95
+ # registered with #map_column! and #map_headers!.
96
+ #
97
+ def dup
98
+ self.class.new(raw.dup, @conversion_procs.dup, @header_mappings.dup, @header_conversion_proc)
99
+ end
100
+
101
+ # Returns a new, transposed table. Example:
102
+ #
103
+ # | a | 7 | 4 |
104
+ # | b | 9 | 2 |
105
+ #
106
+ # Gets converted into the following:
107
+ #
108
+ # | a | b |
109
+ # | 7 | 9 |
110
+ # | 4 | 2 |
111
+ #
112
+ def transpose
113
+ self.class.new(raw.transpose, @conversion_procs.dup, @header_mappings.dup, @header_conversion_proc)
114
+ end
115
+
116
+ # Converts this table into an Array of Hash where the keys of each
117
+ # Hash are the headers in the table. For example, a Table built from
118
+ # the following plain text:
119
+ #
120
+ # | a | b | sum |
121
+ # | 2 | 3 | 5 |
122
+ # | 7 | 9 | 16 |
123
+ #
124
+ # Gets converted into the following:
125
+ #
126
+ # [{'a' => '2', 'b' => '3', 'sum' => '5'}, {'a' => '7', 'b' => '9', 'sum' => '16'}]
127
+ #
128
+ # Use #map_column! to specify how values in a column are converted.
129
+ #
130
+ def hashes
131
+ @hashes ||= build_hashes
132
+ end
133
+
134
+ # Converts this table into a Hash where the first column is
135
+ # used as keys and the second column is used as values
136
+ #
137
+ # | a | 2 |
138
+ # | b | 3 |
139
+ #
140
+ # Gets converted into the following:
141
+ #
142
+ # {'a' => '2', 'b' => '3'}
143
+ #
144
+ # The table must be exactly two columns wide
145
+ #
146
+ def rows_hash
147
+ return @rows_hash if @rows_hash
148
+ verify_table_width(2)
149
+ @rows_hash = self.transpose.hashes[0]
150
+ end
151
+
152
+ # Gets the raw data of this table. For example, a Table built from
153
+ # the following plain text:
154
+ #
155
+ # | a | b |
156
+ # | c | d |
157
+ #
158
+ # gets converted into the following:
159
+ #
160
+ # [['a', 'b'], ['c', 'd']]
161
+ #
162
+ def raw
163
+ cell_matrix.map do |row|
164
+ row.map do |cell|
165
+ cell.value
166
+ end
167
+ end
168
+ end
169
+
170
+ def column_names #:nodoc:
171
+ @col_names ||= cell_matrix[0].map { |cell| cell.value }
172
+ end
173
+
174
+ def rows
175
+ hashes.map do |hash|
176
+ hash.values_at *headers
177
+ end
178
+ end
179
+
180
+ def each_cells_row(&proc) #:nodoc:
181
+ cells_rows.each(&proc)
182
+ end
183
+
184
+ # Matches +pattern+ against the header row of the table.
185
+ # This is used especially for argument transforms.
186
+ #
187
+ # Example:
188
+ # | column_1_name | column_2_name |
189
+ # | x | y |
190
+ #
191
+ # table.match(/table:column_1_name,column_2_name/) #=> non-nil
192
+ #
193
+ # Note: must use 'table:' prefix on match
194
+ def match(pattern)
195
+ header_to_match = "table:#{headers.join(',')}"
196
+ pattern.match(header_to_match)
197
+ end
198
+
199
+ # Redefines the table headers. This makes it possible to use
200
+ # prettier and more flexible header names in the features. The
201
+ # keys of +mappings+ are Strings or regular expressions
202
+ # (anything that responds to #=== will work) that may match
203
+ # column headings in the table. The values of +mappings+ are
204
+ # desired names for the columns.
205
+ #
206
+ # Example:
207
+ #
208
+ # | Phone Number | Address |
209
+ # | 123456 | xyz |
210
+ # | 345678 | abc |
211
+ #
212
+ # A StepDefinition receiving this table can then map the columns
213
+ # with both Regexp and String:
214
+ #
215
+ # table.map_headers!(/phone( number)?/i => :phone, 'Address' => :address)
216
+ # table.hashes
217
+ # # => [{:phone => '123456', :address => 'xyz'}, {:phone => '345678', :address => 'abc'}]
218
+ #
219
+ # You may also pass in a block if you wish to convert all of the headers:
220
+ #
221
+ # table.map_headers! { |header| header.downcase }
222
+ # table.hashes.keys
223
+ # # => ['phone number', 'address']
224
+ #
225
+ # When a block is passed in along with a hash then the mappings in the hash take precendence:
226
+ #
227
+ # table.map_headers!('Address' => 'ADDRESS') { |header| header.downcase }
228
+ # table.hashes.keys
229
+ # # => ['phone number', 'ADDRESS']
230
+ #
231
+ def map_headers!(mappings={}, &block)
232
+ # TODO: Remove this method for 2.0
233
+ clear_cache!
234
+ @header_mappings = mappings
235
+ @header_conversion_proc = block
236
+ end
237
+
238
+ # Returns a new Table where the headers are redefined. See #map_headers!
239
+ def map_headers(mappings={}, &block)
240
+ self.class.new raw.dup, @conversion_procs.dup, mappings, block
241
+ end
242
+
243
+ # Change how #hashes converts column values. The +column_name+ argument identifies the column
244
+ # and +conversion_proc+ performs the conversion for each cell in that column. If +strict+ is
245
+ # true, an error will be raised if the column named +column_name+ is not found. If +strict+
246
+ # is false, no error will be raised. Example:
247
+ #
248
+ # Given /^an expense report for (.*) with the following posts:$/ do |table|
249
+ # posts_table.map_column!('amount') { |a| a.to_i }
250
+ # posts_table.hashes.each do |post|
251
+ # # post['amount'] is a Fixnum, rather than a String
252
+ # end
253
+ # end
254
+ #
255
+ def map_column!(column_name, strict=true, &conversion_proc)
256
+ # TODO: Remove this method for 2.0
257
+ @conversion_procs[column_name.to_s] = { :strict => strict, :proc => conversion_proc }
258
+ self
259
+ end
260
+
261
+ # Returns a new Table with an additional column mapping. See #map_column!
262
+ def map_column(column_name, strict=true, &conversion_proc)
263
+ conversion_procs = @conversion_procs.dup
264
+ conversion_procs[column_name.to_s] = { :strict => strict, :proc => conversion_proc }
265
+ self.class.new(raw.dup, conversion_procs, @header_mappings.dup, @header_conversion_proc)
266
+ end
267
+
268
+ # Compares +other_table+ to self. If +other_table+ contains columns
269
+ # and/or rows that are not in self, new columns/rows are added at the
270
+ # relevant positions, marking the cells in those rows/columns as
271
+ # <tt>surplus</tt>. Likewise, if +other_table+ lacks columns and/or
272
+ # rows that are present in self, these are marked as <tt>missing</tt>.
273
+ #
274
+ # <tt>surplus</tt> and <tt>missing</tt> cells are recognised by formatters
275
+ # and displayed so that it's easy to read the differences.
276
+ #
277
+ # Cells that are different, but <em>look</em> identical (for example the
278
+ # boolean true and the string "true") are converted to their Object#inspect
279
+ # representation and preceded with (i) - to make it easier to identify
280
+ # where the difference actually is.
281
+ #
282
+ # Since all tables that are passed to StepDefinitions always have String
283
+ # objects in their cells, you may want to use #map_column! before calling
284
+ # #diff!. You can use #map_column! on either of the tables.
285
+ #
286
+ # A Different error is raised if there are missing rows or columns, or
287
+ # surplus rows. An error is <em>not</em> raised for surplus columns. An
288
+ # error is <em>not</em> raised for misplaced (out of sequence) columns.
289
+ # Whether to raise or not raise can be changed by setting values in
290
+ # +options+ to true or false:
291
+ #
292
+ # * <tt>missing_row</tt> : Raise on missing rows (defaults to true)
293
+ # * <tt>surplus_row</tt> : Raise on surplus rows (defaults to true)
294
+ # * <tt>missing_col</tt> : Raise on missing columns (defaults to true)
295
+ # * <tt>surplus_col</tt> : Raise on surplus columns (defaults to false)
296
+ # * <tt>misplaced_col</tt> : Raise on misplaced columns (defaults to false)
297
+ #
298
+ # The +other_table+ argument can be another Table, an Array of Array or
299
+ # an Array of Hash (similar to the structure returned by #hashes).
300
+ #
301
+ # Calling this method is particularly useful in <tt>Then</tt> steps that take
302
+ # a Table argument, if you want to compare that table to some actual values.
303
+ #
304
+ def diff!(other_table, options={})
305
+ options = {
306
+ :missing_row => true,
307
+ :surplus_row => true,
308
+ :missing_col => true,
309
+ :surplus_col => false,
310
+ :misplaced_col => false
311
+ }.merge(options)
312
+
313
+ other_table = ensure_table(other_table)
314
+ other_table.convert_headers!
315
+ other_table.convert_columns!
316
+ ensure_green!
317
+
318
+ convert_headers!
319
+ convert_columns!
320
+
321
+ original_width = cell_matrix[0].length
322
+ other_table_cell_matrix = pad!(other_table.cell_matrix)
323
+ padded_width = cell_matrix[0].length
324
+
325
+ missing_col = cell_matrix[0].detect{|cell| cell.status == :undefined}
326
+ surplus_col = padded_width > original_width
327
+ misplaced_col = cell_matrix[0] != other_table.cell_matrix[0]
328
+
329
+ require_diff_lcs
330
+ cell_matrix.extend(Diff::LCS)
331
+ changes = cell_matrix.diff(other_table_cell_matrix).flatten
332
+
333
+ inserted = 0
334
+ missing = 0
335
+
336
+ row_indices = Array.new(other_table_cell_matrix.length) {|n| n}
337
+
338
+ last_change = nil
339
+ missing_row_pos = nil
340
+ insert_row_pos = nil
341
+
342
+ changes.each do |change|
343
+ if(change.action == '-')
344
+ missing_row_pos = change.position + inserted
345
+ cell_matrix[missing_row_pos].each{|cell| cell.status = :undefined}
346
+ row_indices.insert(missing_row_pos, nil)
347
+ missing += 1
348
+ else # '+'
349
+ insert_row_pos = change.position + missing
350
+ inserted_row = change.element
351
+ inserted_row.each{|cell| cell.status = :comment}
352
+ cell_matrix.insert(insert_row_pos, inserted_row)
353
+ row_indices[insert_row_pos] = nil
354
+ inspect_rows(cell_matrix[missing_row_pos], inserted_row) if last_change && last_change.action == '-'
355
+ inserted += 1
356
+ end
357
+ last_change = change
358
+ end
359
+
360
+ other_table_cell_matrix.each_with_index do |other_row, i|
361
+ row_index = row_indices.index(i)
362
+ row = cell_matrix[row_index] if row_index
363
+ if row
364
+ (original_width..padded_width).each do |col_index|
365
+ surplus_cell = other_row[col_index]
366
+ row[col_index].value = surplus_cell.value if row[col_index]
367
+ end
368
+ end
369
+ end
370
+
371
+ clear_cache!
372
+ should_raise =
373
+ missing_row_pos && options[:missing_row] ||
374
+ insert_row_pos && options[:surplus_row] ||
375
+ missing_col && options[:missing_col] ||
376
+ surplus_col && options[:surplus_col] ||
377
+ misplaced_col && options[:misplaced_col]
378
+ raise Different.new(self) if should_raise
379
+ end
380
+
381
+ def to_hash(cells) #:nodoc:
382
+ hash = Hash.new do |hash, key|
383
+ hash[key.to_s] if key.is_a?(Symbol)
384
+ end
385
+ column_names.each_with_index do |column_name, column_index|
386
+ hash[column_name] = cells.value(column_index)
387
+ end
388
+ hash
389
+ end
390
+
391
+ def index(cells) #:nodoc:
392
+ cells_rows.index(cells)
393
+ end
394
+
395
+ def verify_column(column_name) #:nodoc:
396
+ raise %{The column named "#{column_name}" does not exist} unless raw[0].include?(column_name)
397
+ end
398
+
399
+ def verify_table_width(width) #:nodoc:
400
+ raise %{The table must have exactly #{width} columns} unless raw[0].size == width
401
+ end
402
+
403
+ def has_text?(text) #:nodoc:
404
+ raw.flatten.compact.detect{|cell_value| cell_value.index(text)}
405
+ end
406
+
407
+ def cells_rows #:nodoc:
408
+ @rows ||= cell_matrix.map do |cell_row|
409
+ Cells.new(self, cell_row)
410
+ end
411
+ end
412
+
413
+ def headers #:nodoc:
414
+ raw.first
415
+ end
416
+
417
+ def header_cell(col) #:nodoc:
418
+ cells_rows[0][col]
419
+ end
420
+
421
+ def cell_matrix #:nodoc:
422
+ @cell_matrix
423
+ end
424
+
425
+ def col_width(col) #:nodoc:
426
+ columns[col].__send__(:width)
427
+ end
428
+
429
+ def to_s(options = {}) #:nodoc:
430
+ require 'cucumber/formatter/pretty'
431
+ require 'cucumber/reports/legacy_formatter'
432
+ options = {:color => true, :indent => 2, :prefixes => TO_S_PREFIXES}.merge(options)
433
+ io = StringIO.new
434
+
435
+ c = Cucumber::Term::ANSIColor.coloring?
436
+ Cucumber::Term::ANSIColor.coloring = options[:color]
437
+ formatter = Formatter::Pretty.new(nil, io, options)
438
+ formatter.instance_variable_set('@indent', options[:indent])
439
+
440
+ Reports::Legacy::Ast::MultilineArg.for(self).accept(Reports::FormatterWrapper.new([formatter]))
441
+
442
+ Cucumber::Term::ANSIColor.coloring = c
443
+ io.rewind
444
+ s = "\n" + io.read + (" " * (options[:indent] - 2))
445
+ s
446
+ end
447
+
448
+ def location
449
+ @ast_table.location
450
+ end
451
+
452
+ def description_for_visitors
453
+ :legacy_table
454
+ end
455
+
456
+ def columns #:nodoc:
457
+ @columns ||= cell_matrix.transpose.map do |cell_row|
458
+ Cells.new(self, cell_row)
459
+ end
460
+ end
461
+
462
+ def to_json(*args)
463
+ raw.to_json(*args)
464
+ end
465
+
466
+ private
467
+
468
+ TO_S_PREFIXES = Hash.new(' ')
469
+ TO_S_PREFIXES[:comment] = '(+) '
470
+ TO_S_PREFIXES[:undefined] = '(-) '
471
+
472
+ protected
473
+
474
+ def build_hashes
475
+ convert_headers!
476
+ convert_columns!
477
+ cells_rows[1..-1].map do |row|
478
+ row.to_hash
479
+ end
480
+ end
481
+
482
+ def inspect_rows(missing_row, inserted_row) #:nodoc:
483
+ missing_row.each_with_index do |missing_cell, col|
484
+ inserted_cell = inserted_row[col]
485
+ if(missing_cell.value != inserted_cell.value && (missing_cell.value.to_s == inserted_cell.value.to_s))
486
+ missing_cell.inspect!
487
+ inserted_cell.inspect!
488
+ end
489
+ end
490
+ end
491
+
492
+ def create_cell_matrix(ast_table) #:nodoc:
493
+ ast_table.raw.map do |raw_row|
494
+ line = raw_row.line rescue -1
495
+ raw_row.map do |raw_cell|
496
+ Cell.new(raw_cell, self, line)
497
+ end
498
+ end
499
+ end
500
+
501
+ def convert_columns! #:nodoc:
502
+ @conversion_procs.each do |column_name, conversion_proc|
503
+ verify_column(column_name) if conversion_proc[:strict]
504
+ end
505
+
506
+ cell_matrix.transpose.each do |col|
507
+ column_name = col[0].value
508
+ conversion_proc = @conversion_procs[column_name][:proc]
509
+ col[1..-1].each do |cell|
510
+ cell.value = conversion_proc.call(cell.value)
511
+ end
512
+ end
513
+ end
514
+
515
+ def convert_headers! #:nodoc:
516
+ header_cells = cell_matrix[0]
517
+
518
+ if @header_conversion_proc
519
+ header_values = header_cells.map { |cell| cell.value } - @header_mappings.keys
520
+ @header_mappings = @header_mappings.merge(Hash[*header_values.zip(header_values.map(&@header_conversion_proc)).flatten])
521
+ end
522
+
523
+ @header_mappings.each_pair do |pre, post|
524
+ mapped_cells = header_cells.select { |cell| pre === cell.value }
525
+ raise "No headers matched #{pre.inspect}" if mapped_cells.empty?
526
+ raise "#{mapped_cells.length} headers matched #{pre.inspect}: #{mapped_cells.map { |c| c.value }.inspect}" if mapped_cells.length > 1
527
+ mapped_cells[0].value = post
528
+ if @conversion_procs.has_key?(pre)
529
+ @conversion_procs[post] = @conversion_procs.delete(pre)
530
+ end
531
+ end
532
+ end
533
+
534
+ def require_diff_lcs #:nodoc:
535
+ begin
536
+ require 'diff/lcs'
537
+ rescue LoadError => e
538
+ e.message << "\n Please gem install diff-lcs\n"
539
+ raise e
540
+ end
541
+ end
542
+
543
+ def clear_cache! #:nodoc:
544
+ @hashes = @rows_hash = @col_names = @rows = @columns = nil
545
+ end
546
+
547
+ # Pads our own cell_matrix and returns a cell matrix of same
548
+ # column width that can be used for diffing
549
+ def pad!(other_cell_matrix) #:nodoc:
550
+ clear_cache!
551
+ cols = cell_matrix.transpose
552
+ unmapped_cols = other_cell_matrix.transpose
553
+
554
+ mapped_cols = []
555
+
556
+ cols.each_with_index do |col, col_index|
557
+ header = col[0]
558
+ candidate_cols, unmapped_cols = unmapped_cols.partition do |other_col|
559
+ other_col[0] == header
560
+ end
561
+ raise "More than one column has the header #{header}" if candidate_cols.size > 2
562
+
563
+ other_padded_col = if candidate_cols.size == 1
564
+ # Found a matching column
565
+ candidate_cols[0]
566
+ else
567
+ mark_as_missing(cols[col_index])
568
+ (0...other_cell_matrix.length).map do |row|
569
+ val = row == 0 ? header.value : nil
570
+ SurplusCell.new(val, self, -1)
571
+ end
572
+ end
573
+ mapped_cols.insert(col_index, other_padded_col)
574
+ end
575
+
576
+ unmapped_cols.each_with_index do |col, col_index|
577
+ empty_col = (0...cell_matrix.length).map do |row|
578
+ SurplusCell.new(nil, self, -1)
579
+ end
580
+ cols << empty_col
581
+ end
582
+
583
+ @cell_matrix = cols.transpose
584
+ (mapped_cols + unmapped_cols).transpose
585
+ end
586
+
587
+ def ensure_table(table_or_array) #:nodoc:
588
+ return table_or_array if DataTable === table_or_array
589
+ DataTable.new(table_or_array)
590
+ end
591
+
592
+ def ensure_array_of_array(array)
593
+ Hash === array[0] ? hashes_to_array(array) : array
594
+ end
595
+
596
+ def hashes_to_array(hashes) #:nodoc:
597
+ header = hashes[0].keys.sort
598
+ [header] + hashes.map{|hash| header.map{|key| hash[key]}}
599
+ end
600
+
601
+ def ensure_green! #:nodoc:
602
+ each_cell{|cell| cell.status = :passed}
603
+ end
604
+
605
+ def each_cell(&proc) #:nodoc:
606
+ cell_matrix.each{|row| row.each(&proc)}
607
+ end
608
+
609
+ def mark_as_missing(col) #:nodoc:
610
+ col.each do |cell|
611
+ cell.status = :undefined
612
+ end
613
+ end
614
+
615
+ # Represents a row of cells or columns of cells
616
+ class Cells #:nodoc:
617
+ include Enumerable
618
+ include Gherkin::Formatter::Escaping
619
+
620
+ attr_reader :exception
621
+
622
+ def initialize(table, cells)
623
+ @table, @cells = table, cells
624
+ end
625
+
626
+ def accept(visitor)
627
+ return if Cucumber.wants_to_quit
628
+ each do |cell|
629
+ visitor.visit_table_cell(cell)
630
+ end
631
+ nil
632
+ end
633
+
634
+ # For testing only
635
+ def to_sexp #:nodoc:
636
+ [:row, line, *@cells.map{|cell| cell.to_sexp}]
637
+ end
638
+
639
+ def to_hash #:nodoc:
640
+ @to_hash ||= @table.to_hash(self)
641
+ end
642
+
643
+ def value(n) #:nodoc:
644
+ self[n].value
645
+ end
646
+
647
+ def [](n)
648
+ @cells[n]
649
+ end
650
+
651
+ def line
652
+ @cells[0].line
653
+ end
654
+
655
+ def dom_id
656
+ "row_#{line}"
657
+ end
658
+
659
+ def each(&proc)
660
+ @cells.each(&proc)
661
+ end
662
+
663
+ private
664
+
665
+ def index
666
+ @table.index(self)
667
+ end
668
+
669
+ def width
670
+ map{|cell| cell.value ? escape_cell(cell.value.to_s).unpack('U*').length : 0}.max
671
+ end
672
+ end
673
+
674
+ class Cell #:nodoc:
675
+ attr_reader :line, :table
676
+ attr_accessor :status, :value
677
+
678
+ def initialize(value, table, line)
679
+ @value, @table, @line = value, table, line
680
+ end
681
+
682
+ def inspect!
683
+ @value = "(i) #{value.inspect}"
684
+ end
685
+
686
+ def ==(o)
687
+ SurplusCell === o || value == o.value
688
+ end
689
+
690
+ def eql?(o)
691
+ self == o
692
+ end
693
+
694
+ def hash
695
+ 0
696
+ end
697
+
698
+ # For testing only
699
+ def to_sexp #:nodoc:
700
+ [:cell, @value]
701
+ end
702
+ end
703
+
704
+ class SurplusCell < Cell #:nodoc:
705
+ def status
706
+ :comment
707
+ end
708
+
709
+ def ==(o)
710
+ true
711
+ end
712
+
713
+ def hash
714
+ 0
715
+ end
716
+ end
717
+ end
718
+ end
719
+ end