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
@@ -0,0 +1,233 @@
1
+ require 'cucumber/messages'
2
+ require 'digest'
3
+
4
+ module Gherkin
5
+ module Pickles
6
+ class Compiler
7
+ def compile(gherkin_document, source)
8
+ pickles = []
9
+
10
+ return pickles unless gherkin_document.feature
11
+ feature = gherkin_document.feature
12
+ language = feature.language
13
+ tags = feature.tags
14
+
15
+ compile_feature(pickles, language, tags, feature, source)
16
+ pickles
17
+ end
18
+
19
+ private
20
+
21
+ def compile_feature(pickles, language, tags, feature, source)
22
+ background_steps = []
23
+ feature.children.each do |child|
24
+ if child.background
25
+ background_steps.concat(pickle_steps(child.background.steps))
26
+ elsif child.rule
27
+ compile_rule(pickles, language, tags, background_steps, child.rule, source)
28
+ else
29
+ scenario = child.scenario
30
+ if scenario.examples.empty?
31
+ compile_scenario(tags, background_steps, scenario, language, pickles, source)
32
+ else
33
+ compile_scenario_outline(tags, background_steps, scenario, language, pickles, source)
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def compile_rule(pickles, language, tags, feature_background_steps, rule, source)
40
+ background_steps = feature_background_steps.dup
41
+ rule.children.each do |child|
42
+ if child.background
43
+ background_steps.concat(pickle_steps(child.background.steps))
44
+ else
45
+ scenario = child.scenario
46
+ if scenario.examples.empty?
47
+ compile_scenario(tags, background_steps, scenario, language, pickles, source)
48
+ else
49
+ compile_scenario_outline(tags, background_steps, scenario, language, pickles, source)
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ def compile_scenario(feature_tags, background_steps, scenario, language, pickles, source)
56
+ steps = scenario.steps.empty? ? [] : [].concat(background_steps)
57
+
58
+ tags = [].concat(feature_tags).concat(scenario.tags)
59
+
60
+ scenario.steps.each do |step|
61
+ steps.push(pickle_step(step))
62
+ end
63
+
64
+ pickle = Cucumber::Messages::Pickle.new(
65
+ uri: source.uri,
66
+ id: calculate_id(source.data, [scenario.location]),
67
+ tags: pickle_tags(tags),
68
+ name: scenario.name,
69
+ language: language,
70
+ locations: [scenario.location],
71
+ steps: steps
72
+ )
73
+ pickles.push(pickle)
74
+ end
75
+
76
+ def compile_scenario_outline(feature_tags, background_steps, scenario, language, pickles, source)
77
+ scenario.examples.reject { |examples| examples.table_header.nil? }.each do |examples|
78
+ variable_cells = examples.table_header.cells
79
+ examples.table_body.each do |values|
80
+ value_cells = values.cells
81
+ steps = scenario.steps.empty? ? [] : [].concat(background_steps)
82
+ tags = [].concat(feature_tags).concat(scenario.tags).concat(examples.tags)
83
+
84
+ scenario.steps.each do |scenario_outline_step|
85
+ step_props = pickle_step_props(scenario_outline_step, variable_cells, value_cells)
86
+ step_props[:locations].push(values.location)
87
+ steps.push(Cucumber::Messages::Pickle::PickleStep.new(step_props))
88
+ end
89
+
90
+ pickle = Cucumber::Messages::Pickle.new(
91
+ uri: source.uri,
92
+ id: calculate_id(source.data, [scenario.location, values.location]),
93
+ name: interpolate(scenario.name, variable_cells, value_cells),
94
+ language: language,
95
+ steps: steps,
96
+ tags: pickle_tags(tags),
97
+ locations: [
98
+ scenario.location,
99
+ values.location
100
+ ]
101
+ )
102
+ pickles.push(pickle);
103
+
104
+ end
105
+ end
106
+ end
107
+
108
+ def create_pickle_arguments(argument, variable_cells, value_cells)
109
+ result = []
110
+ return result if argument.nil?
111
+ if (argument[:type] == :DataTable)
112
+ table = {
113
+ rows: argument[:rows].map do |row|
114
+ {
115
+ cells: row[:cells].map do |cell|
116
+ {
117
+ location: cell.location,
118
+ value: interpolate(cell.value, variable_cells, value_cells)
119
+ }
120
+ end
121
+ }
122
+ end
123
+ }
124
+ result.push(table)
125
+ elsif (argument[:type] == :DocString)
126
+ doc_string = {
127
+ location: pickle_location(argument.location),
128
+ content: interpolate(argument[:content], variable_cells, value_cells)
129
+ }
130
+ if argument.key?(:contentType)
131
+ doc_string[:contentType] = interpolate(argument[:contentType], variable_cells, value_cells)
132
+ end
133
+ result.push(doc_string)
134
+ else
135
+ raise 'Internal error'
136
+ end
137
+ result
138
+ end
139
+
140
+ def interpolate(name, variable_cells, value_cells)
141
+ variable_cells.each_with_index do |variable_cell, n|
142
+ value_cell = value_cells[n]
143
+ name = name.gsub('<' + variable_cell.value + '>', value_cell.value)
144
+ end
145
+ name
146
+ end
147
+
148
+ def calculate_id(source_data, locations)
149
+ sha1 = Digest::SHA1.new
150
+ sha1 << source_data
151
+ locations.each do |location|
152
+ sha1 << [location.line, location.column].pack('V*')
153
+ end
154
+ sha1.hexdigest
155
+ end
156
+
157
+ def pickle_steps(steps)
158
+ steps.map do |step|
159
+ pickle_step(step)
160
+ end
161
+ end
162
+
163
+ def pickle_step(step)
164
+ Cucumber::Messages::Pickle::PickleStep.new(pickle_step_props(step, [], []))
165
+ end
166
+
167
+ def pickle_step_props(step, variable_cells, value_cells)
168
+ props = {
169
+ text: interpolate(step.text, variable_cells, value_cells),
170
+ locations: [pickle_step_location(step)]
171
+ }
172
+
173
+ if step.data_table
174
+ data_table = Cucumber::Messages::PickleStepArgument.new(
175
+ data_table: pickle_data_table(step.data_table, variable_cells, value_cells)
176
+ )
177
+ props[:argument] = data_table
178
+ end
179
+ if step.doc_string
180
+ doc_string = Cucumber::Messages::PickleStepArgument.new(
181
+ doc_string: pickle_doc_string(step.doc_string, variable_cells, value_cells)
182
+ )
183
+ props[:argument] = doc_string
184
+ end
185
+ props
186
+ end
187
+
188
+ def pickle_data_table(data_table, variable_cells, value_cells)
189
+ Cucumber::Messages::PickleStepArgument::PickleTable.new(
190
+ rows: data_table.rows.map do |row|
191
+ Cucumber::Messages::PickleStepArgument::PickleTable::PickleTableRow.new(
192
+ cells: row.cells.map do |cell|
193
+ Cucumber::Messages::PickleStepArgument::PickleTable::PickleTableRow::PickleTableCell.new(
194
+ location: cell.location,
195
+ value: interpolate(cell.value, variable_cells, value_cells)
196
+ )
197
+ end
198
+ )
199
+ end
200
+ )
201
+ end
202
+
203
+ def pickle_doc_string(doc_string, variable_cells, value_cells)
204
+ props = {
205
+ location: doc_string.location,
206
+ content: interpolate(doc_string.content, variable_cells, value_cells)
207
+ }
208
+ if doc_string.content_type
209
+ props[:contentType] = interpolate(doc_string.content_type, variable_cells, value_cells)
210
+ end
211
+ Cucumber::Messages::PickleStepArgument::PickleDocString.new(props)
212
+ end
213
+
214
+ def pickle_step_location(step)
215
+ Cucumber::Messages::Location.new(
216
+ line: step.location.line,
217
+ column: step.location.column + (step.keyword ? step.keyword.length : 0)
218
+ )
219
+ end
220
+
221
+ def pickle_tags(tags)
222
+ tags.map {|tag| pickle_tag(tag)}
223
+ end
224
+
225
+ def pickle_tag(tag)
226
+ Cucumber::Messages::Pickle::PickleTag.new(
227
+ name: tag.name,
228
+ location: tag.location
229
+ )
230
+ end
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,93 @@
1
+ require 'cucumber/messages'
2
+ require 'gherkin/parser'
3
+ require 'gherkin/token_matcher'
4
+ require 'gherkin/pickles/compiler'
5
+
6
+ module Gherkin
7
+ module Stream
8
+ class ParserMessageStream
9
+ def initialize(paths, sources, options)
10
+ @paths = paths
11
+ @sources = sources
12
+ @options = options
13
+ @parser = Parser.new
14
+ @compiler = Pickles::Compiler.new
15
+ end
16
+
17
+ def messages
18
+ Enumerator.new do |y|
19
+ sources.each do |source|
20
+ y.yield(Cucumber::Messages::Envelope.new(source: source)) if @options[:include_source]
21
+ begin
22
+ gherkin_document = nil
23
+
24
+ if @options[:include_gherkin_document]
25
+ gherkin_document = build_gherkin_document(source)
26
+ y.yield(Cucumber::Messages::Envelope.new(gherkinDocument: gherkin_document))
27
+ end
28
+ if @options[:include_pickles]
29
+ gherkin_document ||= build_gherkin_document(source)
30
+ pickles = @compiler.compile(gherkin_document, source)
31
+ pickles.each do |pickle|
32
+ y.yield(Cucumber::Messages::Envelope.new(pickle: pickle))
33
+ end
34
+ end
35
+ rescue CompositeParserException => err
36
+ yield_error_attachments(y, err.errors, source.uri)
37
+ rescue ParserException => err
38
+ yield_error_attachments(y, [err], source.uri)
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def yield_error_attachments(y, errors, uri)
47
+ errors.each do |err|
48
+ attachment = Cucumber::Messages::Attachment.new(
49
+ source: Cucumber::Messages::SourceReference.new(
50
+ uri: uri,
51
+ location: Cucumber::Messages::Location.new(
52
+ line: err.location[:line],
53
+ column: err.location[:column]
54
+ )
55
+ ),
56
+ data: err.message
57
+ )
58
+ y.yield(Cucumber::Messages::Envelope.new(attachment: attachment))
59
+ end
60
+ end
61
+
62
+ def sources
63
+ Enumerator.new do |y|
64
+ @paths.each do |path|
65
+ source = Cucumber::Messages::Source.new(
66
+ uri: path,
67
+ data: File.open(path, 'r:UTF-8', &:read),
68
+ media: Cucumber::Messages::Media.new(
69
+ encoding: :UTF8,
70
+ content_type: 'text/x.cucumber.gherkin+plain'
71
+ )
72
+ )
73
+ y.yield(source)
74
+ end
75
+ @sources.each do |source|
76
+ y.yield(source)
77
+ end
78
+ end
79
+ end
80
+
81
+ def build_gherkin_document(source)
82
+ if @options[:default_dialect]
83
+ token_matcher = TokenMatcher.new(@options[:default_dialect])
84
+ gd = @parser.parse(source.data, token_matcher)
85
+ else
86
+ gd = @parser.parse(source.data)
87
+ end
88
+ gd[:uri] = source.uri
89
+ Cucumber::Messages::GherkinDocument.new(gd)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,21 @@
1
+ require 'cucumber/messages'
2
+
3
+ module Gherkin
4
+ module Stream
5
+ class ProtobufMessageStream
6
+ def initialize(io)
7
+ @io = io
8
+ end
9
+
10
+ def messages
11
+ Enumerator.new do |y|
12
+ until @io.eof?
13
+ STDERR.puts "HELLO"
14
+ envelope = Cucumber::Messages::Envelope.parse_delimited_from(@io)
15
+ y.yield envelope
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ require 'open3'
2
+ require 'gherkin/stream/protobuf_message_stream'
3
+
4
+ module Gherkin
5
+ module Stream
6
+ class SubprocessMessageStream
7
+ def initialize(gherkin_executable, paths, print_source, print_ast, print_pickles)
8
+ @gherkin_executable, @paths, @print_source, @print_ast, @print_pickles = gherkin_executable, paths, print_source, print_ast, print_pickles
9
+ end
10
+
11
+ def messages
12
+ args = [@gherkin_executable]
13
+ args.push('--no-source') unless @print_source
14
+ args.push('--no-ast') unless @print_ast
15
+ args.push('--no-pickles') unless @print_pickles
16
+ args = args.concat(@paths)
17
+ stdin, stdout, stderr, wait_thr = Open3.popen3(*args)
18
+ if(stdout.eof?)
19
+ error = stderr.read
20
+ raise error
21
+ end
22
+ ProtobufMessageStream.new(stdout).messages
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,18 @@
1
+ module Gherkin
2
+ class Token < Struct.new(:line, :location)
3
+ attr_accessor :matched_type, :matched_text, :matched_keyword, :matched_indent,
4
+ :matched_items, :matched_gherkin_dialect
5
+
6
+ def eof?
7
+ line.nil?
8
+ end
9
+
10
+ def detach
11
+ # TODO: detach line - is this needed?
12
+ end
13
+
14
+ def token_value
15
+ eof? ? "EOF" : line.get_line_text(-1)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,39 @@
1
+ module Gherkin
2
+ class TokenFormatterBuilder
3
+ def initialize
4
+ reset
5
+ end
6
+
7
+ def reset
8
+ @tokens_text = ""
9
+ end
10
+
11
+ def build(token)
12
+ @tokens_text << "#{format_token(token)}\n"
13
+ end
14
+
15
+ def start_rule(rule_type)
16
+ end
17
+
18
+ def end_rule(rule_type)
19
+ end
20
+
21
+ def get_result
22
+ @tokens_text
23
+ end
24
+
25
+ private
26
+ def format_token(token)
27
+ return "EOF" if token.eof?
28
+
29
+ sprintf "(%s:%s)%s:%s/%s/%s",
30
+ token.location[:line],
31
+ token.location[:column],
32
+ token.matched_type,
33
+ token.matched_keyword,
34
+ token.matched_text,
35
+ Array(token.matched_items).map { |i| "#{i.column}:#{i.text}"}.join(',')
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,169 @@
1
+ require 'gherkin/dialect'
2
+ require 'gherkin/errors'
3
+
4
+ module Gherkin
5
+ class TokenMatcher
6
+ LANGUAGE_PATTERN = /^\s*#\s*language\s*:\s*([a-zA-Z\-_]+)\s*$/
7
+
8
+ def initialize(dialect_name = 'en')
9
+ @default_dialect_name = dialect_name
10
+ change_dialect(dialect_name, nil)
11
+ reset
12
+ end
13
+
14
+ def reset
15
+ change_dialect(@default_dialect_name, nil) unless @dialect_name == @default_dialect_name
16
+ @active_doc_string_separator = nil
17
+ @indent_to_remove = 0
18
+ end
19
+
20
+ def match_TagLine(token)
21
+ return false unless token.line.start_with?('@')
22
+
23
+ set_token_matched(token, :TagLine, nil, nil, nil, token.line.tags)
24
+ true
25
+ end
26
+
27
+ def match_FeatureLine(token)
28
+ match_title_line(token, :FeatureLine, @dialect.feature_keywords)
29
+ end
30
+
31
+ def match_RuleLine(token)
32
+ match_title_line(token, :RuleLine, @dialect.rule_keywords)
33
+ end
34
+
35
+ def match_ScenarioLine(token)
36
+ match_title_line(token, :ScenarioLine, @dialect.scenario_keywords) ||
37
+ match_title_line(token, :ScenarioLine, @dialect.scenario_outline_keywords)
38
+ end
39
+
40
+ def match_BackgroundLine(token)
41
+ match_title_line(token, :BackgroundLine, @dialect.background_keywords)
42
+ end
43
+
44
+ def match_ExamplesLine(token)
45
+ match_title_line(token, :ExamplesLine, @dialect.examples_keywords)
46
+ end
47
+
48
+ def match_TableRow(token)
49
+ return false unless token.line.start_with?('|')
50
+ # TODO: indent
51
+ set_token_matched(token, :TableRow, nil, nil, nil, token.line.table_cells)
52
+ true
53
+ end
54
+
55
+ def match_Empty(token)
56
+ return false unless token.line.empty?
57
+ set_token_matched(token, :Empty, nil, nil, 0)
58
+ true
59
+ end
60
+
61
+ def match_Comment(token)
62
+ return false unless token.line.start_with?('#')
63
+ text = token.line.get_line_text(0) #take the entire line, including leading space
64
+ set_token_matched(token, :Comment, text, nil, 0)
65
+ true
66
+ end
67
+
68
+ def match_Language(token)
69
+ return false unless token.line.trimmed_line_text =~ LANGUAGE_PATTERN
70
+
71
+ dialect_name = $1
72
+ set_token_matched(token, :Language, dialect_name)
73
+
74
+ change_dialect(dialect_name, token.location)
75
+
76
+ true
77
+ end
78
+
79
+ def match_DocStringSeparator(token)
80
+ if @active_doc_string_separator.nil?
81
+ # open
82
+ _match_DocStringSeparator(token, '"""', true) ||
83
+ _match_DocStringSeparator(token, '```', true)
84
+ else
85
+ # close
86
+ _match_DocStringSeparator(token, @active_doc_string_separator, false)
87
+ end
88
+ end
89
+
90
+ def _match_DocStringSeparator(token, separator, is_open)
91
+ return false unless token.line.start_with?(separator)
92
+
93
+ content_type = nil
94
+ if is_open
95
+ content_type = token.line.get_rest_trimmed(separator.length)
96
+ @active_doc_string_separator = separator
97
+ @indent_to_remove = token.line.indent
98
+ else
99
+ @active_doc_string_separator = nil
100
+ @indent_to_remove = 0
101
+ end
102
+
103
+ set_token_matched(token, :DocStringSeparator, content_type, separator)
104
+ true
105
+ end
106
+
107
+ def match_EOF(token)
108
+ return false unless token.eof?
109
+ set_token_matched(token, :EOF)
110
+ true
111
+ end
112
+
113
+ def match_Other(token)
114
+ text = token.line.get_line_text(@indent_to_remove) # take the entire line, except removing DocString indents
115
+ set_token_matched(token, :Other, unescape_docstring(text), nil, 0)
116
+ true
117
+ end
118
+
119
+ def match_StepLine(token)
120
+ keywords = @dialect.given_keywords +
121
+ @dialect.when_keywords +
122
+ @dialect.then_keywords +
123
+ @dialect.and_keywords +
124
+ @dialect.but_keywords
125
+
126
+ keyword = keywords.detect { |k| token.line.start_with?(k) }
127
+
128
+ return false unless keyword
129
+
130
+ title = token.line.get_rest_trimmed(keyword.length)
131
+ set_token_matched(token, :StepLine, title, keyword)
132
+ return true
133
+ end
134
+
135
+ private
136
+
137
+ def change_dialect(dialect_name, location)
138
+ dialect = Dialect.for(dialect_name)
139
+ raise NoSuchLanguageException.new(dialect_name, location) if dialect.nil?
140
+
141
+ @dialect_name = dialect_name
142
+ @dialect = dialect
143
+ end
144
+
145
+ def match_title_line(token, token_type, keywords)
146
+ keyword = keywords.detect { |k| token.line.start_with_title_keyword?(k) }
147
+
148
+ return false unless keyword
149
+
150
+ title = token.line.get_rest_trimmed(keyword.length + ':'.length)
151
+ set_token_matched(token, token_type, title, keyword)
152
+ true
153
+ end
154
+
155
+ def set_token_matched(token, matched_type, text=nil, keyword=nil, indent=nil, items=[])
156
+ token.matched_type = matched_type
157
+ token.matched_text = text && text.chomp
158
+ token.matched_keyword = keyword
159
+ token.matched_indent = indent || (token.line && token.line.indent) || 0
160
+ token.matched_items = items
161
+ token.location[:column] = token.matched_indent + 1
162
+ token.matched_gherkin_dialect = @dialect_name
163
+ end
164
+
165
+ def unescape_docstring(text)
166
+ @active_doc_string_separator ? text.gsub("\\\"\\\"\\\"", "\"\"\"") : text
167
+ end
168
+ end
169
+ end