Guest User

Untitled

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