gherkin 7.0.4 → 8.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/bin/gherkin +34 -12
  3. data/lib/gherkin.rb +42 -0
  4. data/lib/gherkin/ast_builder.rb +260 -0
  5. data/lib/gherkin/ast_node.rb +30 -0
  6. data/lib/gherkin/dialect.rb +2 -12
  7. data/lib/gherkin/errors.rb +45 -0
  8. data/lib/gherkin/gherkin-languages.json +3520 -0
  9. data/lib/gherkin/gherkin_line.rb +97 -0
  10. data/lib/gherkin/parser.rb +3429 -0
  11. data/lib/gherkin/pickles/compiler.rb +233 -0
  12. data/lib/gherkin/stream/parser_message_stream.rb +93 -0
  13. data/lib/gherkin/stream/protobuf_message_stream.rb +21 -0
  14. data/lib/gherkin/stream/subprocess_message_stream.rb +26 -0
  15. data/lib/gherkin/token.rb +18 -0
  16. data/lib/gherkin/token_formatter_builder.rb +39 -0
  17. data/lib/gherkin/token_matcher.rb +169 -0
  18. data/lib/gherkin/token_scanner.rb +40 -0
  19. data/spec/gherkin/gherkin_spec.rb +36 -345
  20. data/spec/gherkin/stream/subprocess_message_stream_spec.rb +18 -0
  21. metadata +28 -53
  22. data/executables/gherkin-darwin-386 +0 -0
  23. data/executables/gherkin-darwin-amd64 +0 -0
  24. data/executables/gherkin-freebsd-386 +0 -0
  25. data/executables/gherkin-freebsd-amd64 +0 -0
  26. data/executables/gherkin-freebsd-arm +0 -0
  27. data/executables/gherkin-linux-386 +0 -0
  28. data/executables/gherkin-linux-amd64 +0 -0
  29. data/executables/gherkin-linux-arm +0 -0
  30. data/executables/gherkin-linux-mips +0 -0
  31. data/executables/gherkin-linux-mips64 +0 -0
  32. data/executables/gherkin-linux-mips64le +0 -0
  33. data/executables/gherkin-linux-mipsle +0 -0
  34. data/executables/gherkin-linux-s390x +0 -0
  35. data/executables/gherkin-netbsd-386 +0 -0
  36. data/executables/gherkin-netbsd-amd64 +0 -0
  37. data/executables/gherkin-netbsd-arm +0 -0
  38. data/executables/gherkin-openbsd-386 +0 -0
  39. data/executables/gherkin-openbsd-amd64 +0 -0
  40. data/executables/gherkin-windows-386.exe +0 -0
  41. data/executables/gherkin-windows-amd64.exe +0 -0
  42. data/lib/gherkin/exe_file_path.rb +0 -3
  43. data/lib/gherkin/gherkin.rb +0 -72
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e4b01f45decc72c0d17d4c1ea68c653671414abfb36c887140ef8a060bc4f277
4
- data.tar.gz: dee6817d1308ddab1bb14fda79e730c27eaf09130646f3e7947a51943f9d043a
3
+ metadata.gz: 03d427e820bf0e7c744cbe67f7f9173ade3a528dd7a3f02c94ef6b261d54fe7b
4
+ data.tar.gz: 01d2c0fca9b02b4a0c6c480ab47c10178210814c86e7dd6f4ffcdde9540dcfda
5
5
  SHA512:
6
- metadata.gz: 1dba2967b674ce4ad491da5cb289ece8e2c59d1a30988b3d9664aa378e2e1c69981c600eb6edc7561ef3dbe20ae74de8b656b9eaefeadc994ee1cc4ae5fc44d4
7
- data.tar.gz: 633ea2dc1e7459abd5aab2d0271c072c4788f6e59628b0e1a4c175375de3cdf818a898d5e6b2d41422af27020794794a81d081324c582dbdcc9edc39439f88b7
6
+ metadata.gz: c638788e4c6926086d5271cf414665d3fdaebf464643283238ca6f86cb000d1650c69017c5bc11260c2770a093b0c9538ab2ab0df73e44b2d9ef1c7e4f208475
7
+ data.tar.gz: 4960487bbdde80f6d74df8e81fd4d3983a1fe2c998ea5d9842cfc8db9ab4607cf0c4909f5c89a2b1f19a0e8614eaabd0b673ed27d79488651da1caf51606cecb
@@ -4,12 +4,16 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__),"../lib"))
4
4
 
5
5
  require 'optparse'
6
6
  require 'json'
7
- require 'gherkin/gherkin'
7
+ require 'cucumber/messages'
8
+ require 'gherkin'
9
+ require 'gherkin/stream/subprocess_message_stream'
10
+ require 'gherkin/stream/protobuf_message_stream'
8
11
 
9
12
  options = {
10
13
  include_source: true,
11
14
  include_gherkin_document: true,
12
- include_pickles: true
15
+ include_pickles: true,
16
+ json: false
13
17
  }
14
18
 
15
19
  OptionParser.new do |opts|
@@ -22,14 +26,21 @@ OptionParser.new do |opts|
22
26
  opts.on("--[no-]pickles", "Don't print pickle messages") do |v|
23
27
  options[:include_pickles] = v
24
28
  end
29
+ opts.on("--json", "Print messages as JSON") do |v|
30
+ options[:json] = v
31
+ end
25
32
  end.parse!
26
33
 
27
- def include_messages(messages)
34
+ def process_messages(messages, options)
28
35
  messages.each do |message|
29
- json = message.class.encode_json(message)
30
- ob = JSON.parse(json)
31
- remove_empties(ob)
32
- puts JSON.generate(ob)
36
+ if options[:json]
37
+ json = message.class.encode_json(message)
38
+ ob = JSON.parse(json)
39
+ remove_empties(ob)
40
+ puts JSON.generate(ob)
41
+ else
42
+ message.write_delimited_to(STDOUT)
43
+ end
33
44
  end
34
45
  end
35
46
 
@@ -49,12 +60,23 @@ def remove_empties(ob)
49
60
  end
50
61
  end
51
62
 
52
- messages = ARGV.empty? ?
63
+ gherkin_executable = ENV['GHERKIN_EXECUTABLE']
64
+ if ARGV.empty?
65
+ # Read protobuf from STDIN
66
+ messages = Gherkin::Stream::ProtobufMessageStream.new(STDIN).messages
67
+ elsif gherkin_executable
53
68
  # Read protobuf from STDIN
54
- Gherkin::ProtobufCucumberMessages.new(STDIN).messages
55
- :
56
- Gherkin::Gherkin.from_paths(
69
+ messages = Gherkin::Stream::SubprocessMessageStream.new(
70
+ gherkin_executable,
71
+ ARGV,
72
+ options[:include_source],
73
+ options[:include_gherkin_document],
74
+ options[:include_pickles]
75
+ ).messages
76
+ else
77
+ messages = Gherkin.from_paths(
57
78
  ARGV,
58
79
  options
59
80
  )
60
- include_messages(messages)
81
+ end
82
+ process_messages(messages, options)
@@ -0,0 +1,42 @@
1
+ require 'gherkin/stream/parser_message_stream'
2
+
3
+ module Gherkin
4
+ DEFAULT_OPTIONS = {
5
+ include_source: true,
6
+ include_gherkin_document: true,
7
+ include_pickles: true
8
+ }.freeze
9
+
10
+ def self.from_paths(paths, options={})
11
+ Stream::ParserMessageStream.new(
12
+ paths,
13
+ [],
14
+ options
15
+ ).messages
16
+ end
17
+
18
+ def self.from_sources(sources, options={})
19
+ Stream::ParserMessageStream.new(
20
+ [],
21
+ sources,
22
+ options
23
+ ).messages
24
+ end
25
+
26
+ def self.from_source(uri, data, options={})
27
+ from_sources([encode_source_message(uri, data)], options)
28
+ end
29
+
30
+ private
31
+
32
+ def self.encode_source_message(uri, data)
33
+ Cucumber::Messages::Source.new({
34
+ uri: uri,
35
+ data: data,
36
+ media: Cucumber::Messages::Media.new({
37
+ encoding: :UTF8,
38
+ content_type: 'text/x.cucumber.gherkin+plain'
39
+ })
40
+ })
41
+ end
42
+ end
@@ -0,0 +1,260 @@
1
+ require 'cucumber/messages'
2
+ require 'gherkin/ast_node'
3
+
4
+ module Gherkin
5
+ class AstBuilder
6
+ def initialize
7
+ reset
8
+ end
9
+
10
+ def reset
11
+ @stack = [AstNode.new(:None)]
12
+ @comments = []
13
+ end
14
+
15
+ def start_rule(rule_type)
16
+ @stack.push AstNode.new(rule_type)
17
+ end
18
+
19
+ def end_rule(rule_type)
20
+ node = @stack.pop
21
+ current_node.add(node.rule_type, transform_node(node))
22
+ end
23
+
24
+ def build(token)
25
+ if token.matched_type == :Comment
26
+ @comments.push(Cucumber::Messages::GherkinDocument::Comment.new(
27
+ location: get_location(token, 0),
28
+ text: token.matched_text
29
+ ))
30
+ else
31
+ current_node.add(token.matched_type, token)
32
+ end
33
+ end
34
+
35
+ def get_result
36
+ current_node.get_single(:GherkinDocument)
37
+ end
38
+
39
+ def current_node
40
+ @stack.last
41
+ end
42
+
43
+ def get_location(token, column)
44
+ column = column == 0 ? token.location[:column] : column
45
+ Cucumber::Messages::Location.new(
46
+ line: token.location[:line],
47
+ column: column
48
+ )
49
+ end
50
+
51
+ def get_tags(node)
52
+ tags = []
53
+ tags_node = node.get_single(:Tags)
54
+ return tags unless tags_node
55
+
56
+ tags_node.get_tokens(:TagLine).each do |token|
57
+ token.matched_items.each do |tag_item|
58
+ tags.push(Cucumber::Messages::GherkinDocument::Feature::Tag.new(
59
+ location: get_location(token, tag_item.column),
60
+ name: tag_item.text
61
+ ))
62
+ end
63
+ end
64
+
65
+ tags
66
+ end
67
+
68
+ def get_table_rows(node)
69
+ rows = node.get_tokens(:TableRow).map do |token|
70
+ Cucumber::Messages::GherkinDocument::Feature::TableRow.new(
71
+ location: get_location(token, 0),
72
+ cells: get_cells(token)
73
+ )
74
+ end
75
+ ensure_cell_count(rows)
76
+ rows
77
+ end
78
+
79
+ def ensure_cell_count(rows)
80
+ return if rows.empty?
81
+ cell_count = rows[0].cells.length
82
+ rows.each do |row|
83
+ if row.cells.length != cell_count
84
+ location = {line: row.location.line, column: row.location.column}
85
+ raise AstBuilderException.new("inconsistent cell count within the table", location)
86
+ end
87
+ end
88
+ end
89
+
90
+ def get_cells(table_row_token)
91
+ table_row_token.matched_items.map do |cell_item|
92
+ Cucumber::Messages::GherkinDocument::Feature::TableRow::TableCell.new(
93
+ location: get_location(table_row_token, cell_item.column),
94
+ value: cell_item.text
95
+ )
96
+ end
97
+ end
98
+
99
+ def get_description(node)
100
+ node.get_single(:Description)
101
+ end
102
+
103
+ def get_steps(node)
104
+ node.get_items(:Step)
105
+ end
106
+
107
+ def transform_node(node)
108
+ case node.rule_type
109
+ when :Step
110
+ step_line = node.get_token(:StepLine)
111
+ data_table = node.get_single(:DataTable)
112
+ doc_string = node.get_single(:DocString)
113
+
114
+ props = {
115
+ location: get_location(step_line, 0),
116
+ keyword: step_line.matched_keyword,
117
+ text: step_line.matched_text
118
+ }
119
+ props[:data_table] = data_table if data_table
120
+ props[:doc_string] = doc_string if doc_string
121
+
122
+ Cucumber::Messages::GherkinDocument::Feature::Step.new(props)
123
+ when :DocString
124
+ separator_token = node.get_tokens(:DocStringSeparator)[0]
125
+ content_type = separator_token.matched_text == '' ? nil : separator_token.matched_text
126
+ line_tokens = node.get_tokens(:Other)
127
+ content = line_tokens.map { |t| t.matched_text }.join("\n")
128
+
129
+ props = {
130
+ location: get_location(separator_token, 0),
131
+ content: content,
132
+ delimiter: separator_token.matched_keyword
133
+ }
134
+ props[:content_type] = content_type if content_type
135
+ Cucumber::Messages::GherkinDocument::Feature::Step::DocString.new(props)
136
+ when :DataTable
137
+ rows = get_table_rows(node)
138
+ Cucumber::Messages::GherkinDocument::Feature::Step::DataTable.new(
139
+ location: rows[0].location,
140
+ rows: rows,
141
+ )
142
+ when :Background
143
+ background_line = node.get_token(:BackgroundLine)
144
+ description = get_description(node)
145
+ steps = get_steps(node)
146
+
147
+ props = {
148
+ location: get_location(background_line, 0),
149
+ keyword: background_line.matched_keyword,
150
+ name: background_line.matched_text,
151
+ steps: steps
152
+ }
153
+ props[:description] = description if description
154
+ Cucumber::Messages::GherkinDocument::Feature::Background.new(props)
155
+ when :ScenarioDefinition
156
+ tags = get_tags(node)
157
+ scenario_node = node.get_single(:Scenario)
158
+ scenario_line = scenario_node.get_token(:ScenarioLine)
159
+ description = get_description(scenario_node)
160
+ steps = get_steps(scenario_node)
161
+ examples = scenario_node.get_items(:ExamplesDefinition)
162
+ props = {
163
+ tags: tags,
164
+ location: get_location(scenario_line, 0),
165
+ keyword: scenario_line.matched_keyword,
166
+ name: scenario_line.matched_text,
167
+ steps: steps,
168
+ examples: examples
169
+ }
170
+ props[:description] = description if description
171
+ Cucumber::Messages::GherkinDocument::Feature::Scenario.new(props)
172
+ when :ExamplesDefinition
173
+ tags = get_tags(node)
174
+ examples_node = node.get_single(:Examples)
175
+ examples_line = examples_node.get_token(:ExamplesLine)
176
+ description = get_description(examples_node)
177
+ rows = examples_node.get_single(:ExamplesTable)
178
+
179
+ table_header = rows.nil? ? nil : rows.first
180
+ table_body = rows.nil? ? nil : rows[1..-1]
181
+
182
+ props = {
183
+ tags: tags,
184
+ location: get_location(examples_line, 0),
185
+ keyword: examples_line.matched_keyword,
186
+ name: examples_line.matched_text,
187
+ }
188
+ props[:table_header] = table_header if table_header
189
+ props[:table_body] = table_body if table_body
190
+ props[:description] = description if description
191
+ Cucumber::Messages::GherkinDocument::Feature::Scenario::Examples.new(props)
192
+ when :ExamplesTable
193
+ get_table_rows(node)
194
+ when :Description
195
+ line_tokens = node.get_tokens(:Other)
196
+ # Trim trailing empty lines
197
+ last_non_empty = line_tokens.rindex { |token| !token.line.trimmed_line_text.empty? }
198
+ description = line_tokens[0..last_non_empty].map { |token| token.matched_text }.join("\n")
199
+ return description
200
+ when :Feature
201
+ header = node.get_single(:FeatureHeader)
202
+ return unless header
203
+ tags = get_tags(header)
204
+ feature_line = header.get_token(:FeatureLine)
205
+ return unless feature_line
206
+ children = []
207
+ background = node.get_single(:Background)
208
+ children.push(Cucumber::Messages::GherkinDocument::Feature::FeatureChild.new(background: background)) if background
209
+ node.get_items(:ScenarioDefinition).each do |scenario|
210
+ children.push(Cucumber::Messages::GherkinDocument::Feature::FeatureChild.new(scenario: scenario))
211
+ end
212
+ node.get_items(:Rule).each do |rule|
213
+ children.push(Cucumber::Messages::GherkinDocument::Feature::FeatureChild.new(rule: rule))
214
+ end
215
+ description = get_description(header)
216
+ language = feature_line.matched_gherkin_dialect
217
+
218
+ props = {
219
+ tags: tags,
220
+ location: get_location(feature_line, 0),
221
+ language: language,
222
+ keyword: feature_line.matched_keyword,
223
+ name: feature_line.matched_text,
224
+ children: children,
225
+ }
226
+ props[:description] = description if description
227
+
228
+ Cucumber::Messages::GherkinDocument::Feature.new(props)
229
+ when :Rule
230
+ header = node.get_single(:RuleHeader)
231
+ return unless header
232
+ rule_line = header.get_token(:RuleLine)
233
+ return unless rule_line
234
+ children = []
235
+ background = node.get_single(:Background)
236
+ children.push(Cucumber::Messages::GherkinDocument::Feature::FeatureChild::RuleChild.new(background: background)) if background
237
+ node.get_items(:ScenarioDefinition).each do |scenario|
238
+ children.push(Cucumber::Messages::GherkinDocument::Feature::FeatureChild::RuleChild.new(scenario: scenario))
239
+ end
240
+ description = get_description(header)
241
+
242
+ Cucumber::Messages::GherkinDocument::Feature::FeatureChild::Rule.new(
243
+ location: get_location(rule_line, 0),
244
+ keyword: rule_line.matched_keyword,
245
+ name: rule_line.matched_text,
246
+ description: description,
247
+ children: children,
248
+ )
249
+ when :GherkinDocument
250
+ feature = node.get_single(:Feature)
251
+ {
252
+ feature: feature,
253
+ comments: @comments
254
+ }
255
+ else
256
+ return node
257
+ end
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,30 @@
1
+ module Gherkin
2
+ class AstNode
3
+ attr_reader :rule_type
4
+
5
+ def initialize(rule_type)
6
+ @rule_type = rule_type
7
+ @_sub_items = Hash.new { |hash, key| hash[key] = [] } # returns [] for unknown key
8
+ end
9
+
10
+ def add(rule_type, obj)
11
+ @_sub_items[rule_type].push(obj)
12
+ end
13
+
14
+ def get_single(rule_type)
15
+ @_sub_items[rule_type].first
16
+ end
17
+
18
+ def get_items(rule_type)
19
+ @_sub_items[rule_type]
20
+ end
21
+
22
+ def get_token(token_type)
23
+ get_single(token_type)
24
+ end
25
+
26
+ def get_tokens(token_type)
27
+ @_sub_items[token_type]
28
+ end
29
+ end
30
+ end
@@ -1,18 +1,8 @@
1
1
  require 'json'
2
- require 'open3'
3
- require 'c21e/exe_file'
4
- require 'gherkin/exe_file_path'
5
2
 
6
3
  module Gherkin
7
- def self.dialects_json
8
- gherkin_executable = C21e::ExeFile.new(EXE_FILE_PATH).target_file
9
- data, = Open3.capture2e(gherkin_executable, '--dialects')
10
- data
11
- end
12
-
13
- private_class_method :dialects_json
14
-
15
- DIALECTS = JSON.parse(dialects_json)
4
+ DIALECT_FILE_PATH = File.expand_path("gherkin-languages.json", File.dirname(__FILE__))
5
+ DIALECTS = JSON.parse File.open(DIALECT_FILE_PATH, 'r:UTF-8').read
16
6
 
17
7
  class Dialect
18
8
  def self.for(name)
@@ -0,0 +1,45 @@
1
+ module Gherkin
2
+ class ParserError < StandardError; end
3
+
4
+ class ParserException < ParserError
5
+ attr_reader :location
6
+
7
+ def initialize(message, location)
8
+ @location = location
9
+ super("(#{location[:line]}:#{location[:column] || 0}): #{message}")
10
+ end
11
+ end
12
+
13
+ class NoSuchLanguageException < ParserException
14
+ def initialize(language, location)
15
+ super "Language not supported: #{language}", location
16
+ end
17
+ end
18
+
19
+ class AstBuilderException < ParserException; end
20
+
21
+ class CompositeParserException < ParserError
22
+ attr_reader :errors
23
+
24
+ def initialize(errors)
25
+ @errors = errors
26
+ super "Parser errors:\n" + errors.map(&:message).join("\n")
27
+ end
28
+ end
29
+
30
+ class UnexpectedTokenException < ParserException
31
+ def initialize(received_token, expected_token_types, state_comment)
32
+ message = "expected: #{expected_token_types.join(", ")}, got '#{received_token.token_value.strip}'"
33
+ column = received_token.location[:column]
34
+ location = (column.nil? || column.zero?) ? {line: received_token.location[:line], column: received_token.line.indent + 1} : received_token.location
35
+ super(message, location)
36
+ end
37
+ end
38
+
39
+ class UnexpectedEOFException < ParserException
40
+ def initialize(received_token, expected_token_types, state_comment)
41
+ message = "unexpected end of file, expected: #{expected_token_types.join(", ")}"
42
+ super(message, received_token.location)
43
+ end
44
+ end
45
+ end