gherkin 7.0.4 → 8.0.0

Sign up to get free protection for your applications and to get access to all the features.
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