cucumber 3.1.2 → 7.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +1780 -1146
  3. data/CONTRIBUTING.md +224 -61
  4. data/README.md +144 -22
  5. data/bin/cucumber +1 -1
  6. data/lib/autotest/cucumber_mixin.rb +46 -53
  7. data/lib/cucumber/cli/configuration.rb +28 -6
  8. data/lib/cucumber/cli/main.rb +12 -12
  9. data/lib/cucumber/cli/options.rb +103 -77
  10. data/lib/cucumber/cli/profile_loader.rb +49 -26
  11. data/lib/cucumber/configuration.rb +44 -29
  12. data/lib/cucumber/constantize.rb +2 -5
  13. data/lib/cucumber/deprecate.rb +31 -7
  14. data/lib/cucumber/errors.rb +5 -7
  15. data/lib/cucumber/events/envelope.rb +9 -0
  16. data/lib/cucumber/events/gherkin_source_parsed.rb +11 -0
  17. data/lib/cucumber/events/hook_test_step_created.rb +13 -0
  18. data/lib/cucumber/events/step_activated.rb +2 -1
  19. data/lib/cucumber/events/test_case_created.rb +13 -0
  20. data/lib/cucumber/events/test_case_ready.rb +12 -0
  21. data/lib/cucumber/events/test_step_created.rb +13 -0
  22. data/lib/cucumber/events/undefined_parameter_type.rb +10 -0
  23. data/lib/cucumber/events.rb +14 -7
  24. data/lib/cucumber/file_specs.rb +6 -6
  25. data/lib/cucumber/filters/activate_steps.rb +5 -3
  26. data/lib/cucumber/filters/broadcast_test_case_ready_event.rb +12 -0
  27. data/lib/cucumber/filters/prepare_world.rb +5 -9
  28. data/lib/cucumber/filters/quit.rb +1 -3
  29. data/lib/cucumber/filters/tag_limits/verifier.rb +2 -4
  30. data/lib/cucumber/filters.rb +1 -0
  31. data/lib/cucumber/formatter/ansicolor.rb +40 -52
  32. data/lib/cucumber/formatter/ast_lookup.rb +163 -0
  33. data/lib/cucumber/formatter/backtrace_filter.rb +10 -8
  34. data/lib/cucumber/formatter/console.rb +69 -69
  35. data/lib/cucumber/formatter/console_counts.rb +4 -9
  36. data/lib/cucumber/formatter/console_issues.rb +6 -3
  37. data/lib/cucumber/formatter/duration.rb +1 -1
  38. data/lib/cucumber/formatter/duration_extractor.rb +3 -1
  39. data/lib/cucumber/formatter/errors.rb +6 -0
  40. data/lib/cucumber/formatter/fanout.rb +2 -0
  41. data/lib/cucumber/formatter/html.rb +11 -598
  42. data/lib/cucumber/formatter/http_io.rb +147 -0
  43. data/lib/cucumber/formatter/ignore_missing_messages.rb +1 -1
  44. data/lib/cucumber/formatter/interceptor.rb +11 -30
  45. data/lib/cucumber/formatter/io.rb +55 -13
  46. data/lib/cucumber/formatter/json.rb +115 -122
  47. data/lib/cucumber/formatter/junit.rb +72 -55
  48. data/lib/cucumber/formatter/message.rb +23 -0
  49. data/lib/cucumber/formatter/message_builder.rb +255 -0
  50. data/lib/cucumber/formatter/pretty.rb +360 -153
  51. data/lib/cucumber/formatter/progress.rb +30 -32
  52. data/lib/cucumber/formatter/publish_banner_printer.rb +77 -0
  53. data/lib/cucumber/formatter/query/hook_by_test_step.rb +31 -0
  54. data/lib/cucumber/formatter/query/pickle_by_test.rb +26 -0
  55. data/lib/cucumber/formatter/query/pickle_step_by_test_step.rb +26 -0
  56. data/lib/cucumber/formatter/query/step_definitions_by_test_step.rb +40 -0
  57. data/lib/cucumber/formatter/query/test_case_started_by_test_case.rb +40 -0
  58. data/lib/cucumber/formatter/rerun.rb +22 -4
  59. data/lib/cucumber/formatter/stepdefs.rb +1 -2
  60. data/lib/cucumber/formatter/steps.rb +8 -6
  61. data/lib/cucumber/formatter/summary.rb +16 -8
  62. data/lib/cucumber/formatter/unicode.rb +15 -17
  63. data/lib/cucumber/formatter/url_reporter.rb +17 -0
  64. data/lib/cucumber/formatter/usage.rb +17 -14
  65. data/lib/cucumber/gherkin/data_table_parser.rb +17 -6
  66. data/lib/cucumber/gherkin/formatter/ansi_escapes.rb +13 -17
  67. data/lib/cucumber/gherkin/formatter/escaping.rb +2 -2
  68. data/lib/cucumber/gherkin/steps_parser.rb +17 -8
  69. data/lib/cucumber/glue/dsl.rb +19 -0
  70. data/lib/cucumber/glue/hook.rb +34 -11
  71. data/lib/cucumber/glue/invoke_in_world.rb +13 -18
  72. data/lib/cucumber/glue/proto_world.rb +37 -44
  73. data/lib/cucumber/glue/registry_and_more.rb +71 -12
  74. data/lib/cucumber/glue/registry_wrapper.rb +31 -0
  75. data/lib/cucumber/glue/snippet.rb +23 -22
  76. data/lib/cucumber/glue/step_definition.rb +42 -20
  77. data/lib/cucumber/glue/world_factory.rb +1 -1
  78. data/lib/cucumber/hooks.rb +11 -11
  79. data/lib/cucumber/multiline_argument/data_table/diff_matrices.rb +2 -2
  80. data/lib/cucumber/multiline_argument/data_table.rb +97 -64
  81. data/lib/cucumber/multiline_argument/doc_string.rb +1 -1
  82. data/lib/cucumber/multiline_argument.rb +4 -6
  83. data/lib/cucumber/platform.rb +3 -3
  84. data/lib/cucumber/rake/task.rb +16 -18
  85. data/lib/cucumber/rspec/disable_option_parser.rb +9 -8
  86. data/lib/cucumber/rspec/doubles.rb +3 -5
  87. data/lib/cucumber/running_test_case.rb +2 -53
  88. data/lib/cucumber/runtime/after_hooks.rb +8 -4
  89. data/lib/cucumber/runtime/before_hooks.rb +8 -4
  90. data/lib/cucumber/runtime/for_programming_languages.rb +4 -2
  91. data/lib/cucumber/runtime/step_hooks.rb +6 -2
  92. data/lib/cucumber/runtime/support_code.rb +13 -15
  93. data/lib/cucumber/runtime/user_interface.rb +6 -16
  94. data/lib/cucumber/runtime.rb +77 -59
  95. data/lib/cucumber/step_definition_light.rb +4 -3
  96. data/lib/cucumber/step_definitions.rb +2 -2
  97. data/lib/cucumber/step_match.rb +12 -17
  98. data/lib/cucumber/step_match_search.rb +2 -1
  99. data/lib/cucumber/term/ansicolor.rb +9 -9
  100. data/lib/cucumber/term/banner.rb +56 -0
  101. data/lib/cucumber/version +1 -1
  102. data/lib/cucumber.rb +1 -1
  103. metadata +272 -81
  104. data/lib/cucumber/core_ext/string.rb +0 -11
  105. data/lib/cucumber/events/gherkin_source_parsed.rb~ +0 -14
  106. data/lib/cucumber/formatter/ast_lookup.rb~ +0 -9
  107. data/lib/cucumber/formatter/cucumber.css +0 -286
  108. data/lib/cucumber/formatter/cucumber.sass +0 -247
  109. data/lib/cucumber/formatter/hook_query_visitor.rb +0 -42
  110. data/lib/cucumber/formatter/html_builder.rb +0 -121
  111. data/lib/cucumber/formatter/inline-js.js +0 -30
  112. data/lib/cucumber/formatter/jquery-min.js +0 -154
  113. data/lib/cucumber/formatter/json_pretty.rb +0 -11
  114. data/lib/cucumber/formatter/legacy_api/adapter.rb +0 -1028
  115. data/lib/cucumber/formatter/legacy_api/ast.rb +0 -394
  116. data/lib/cucumber/formatter/legacy_api/results.rb +0 -50
  117. data/lib/cucumber/formatter/legacy_api/runtime_facade.rb +0 -32
  118. data/lib/cucumber/step_argument.rb +0 -25
@@ -20,7 +20,7 @@ module Cucumber
20
20
  def initialize(out_stream = STDOUT, error_stream = STDERR)
21
21
  @out_stream = out_stream
22
22
  @error_stream = error_stream
23
- @options = Options.new(@out_stream, @error_stream, :default_profile => 'default')
23
+ @options = Options.new(@out_stream, @error_stream, default_profile: 'default')
24
24
  end
25
25
 
26
26
  def parse!(args)
@@ -64,7 +64,7 @@ module Cucumber
64
64
  end
65
65
 
66
66
  def fail_fast?
67
- !!@options[:fail_fast]
67
+ @options[:fail_fast]
68
68
  end
69
69
 
70
70
  def retry_attempts
@@ -79,7 +79,7 @@ module Cucumber
79
79
  logger = Logger.new(@out_stream)
80
80
  logger.formatter = LogFormatter.new
81
81
  logger.level = Logger::INFO
82
- logger.level = Logger::DEBUG if self.verbose?
82
+ logger.level = Logger::DEBUG if verbose?
83
83
  logger
84
84
  end
85
85
 
@@ -108,7 +108,7 @@ module Cucumber
108
108
  end
109
109
 
110
110
  def to_hash
111
- Hash(@options).merge(out_stream: @out_stream, error_stream: @error_stream)
111
+ Hash(@options).merge(out_stream: @out_stream, error_stream: @error_stream, seed: seed)
112
112
  end
113
113
 
114
114
  private
@@ -126,12 +126,34 @@ module Cucumber
126
126
  end
127
127
 
128
128
  def arrange_formats
129
- @options[:formats] << ['pretty', {}, @out_stream] if @options[:formats].empty?
129
+ add_default_formatter if needs_default_formatter?
130
+
130
131
  @options[:formats] = @options[:formats].sort_by do |f|
131
132
  f[2] == @out_stream ? -1 : 1
132
133
  end
133
134
  @options[:formats].uniq!
134
- @options.check_formatter_stream_conflicts()
135
+ @options.check_formatter_stream_conflicts
136
+ end
137
+
138
+ def add_default_formatter
139
+ @options[:formats] << ['pretty', {}, @out_stream]
140
+ end
141
+
142
+ def needs_default_formatter?
143
+ formatter_missing? || publish_only?
144
+ end
145
+
146
+ def formatter_missing?
147
+ @options[:formats].empty?
148
+ end
149
+
150
+ def publish_only?
151
+ @options[:formats]
152
+ .uniq
153
+ .map { |formatter, _, stream| [formatter, stream] }
154
+ .uniq
155
+ .reject { |formatter, stream| formatter == 'message' && stream != @out_stream }
156
+ .empty?
135
157
  end
136
158
  end
137
159
  end
@@ -24,22 +24,15 @@ module Cucumber
24
24
  def execute!(existing_runtime = nil)
25
25
  trap_interrupt
26
26
 
27
- runtime = if existing_runtime
28
- existing_runtime.configure(configuration)
29
- existing_runtime
30
- else
31
- Runtime.new(configuration)
32
- end
27
+ runtime = runtime(existing_runtime)
33
28
 
34
29
  runtime.run!
35
30
  if Cucumber.wants_to_quit
36
31
  exit_unable_to_finish
32
+ elsif runtime.failure?
33
+ exit_tests_failed
37
34
  else
38
- if runtime.failure?
39
- exit_tests_failed
40
- else
41
- exit_ok
42
- end
35
+ exit_ok
43
36
  end
44
37
  rescue SystemExit => e
45
38
  @kernel.exit(e.status)
@@ -56,7 +49,7 @@ module Cucumber
56
49
  rescue Errno::EACCES, Errno::ENOENT => e
57
50
  @err.puts("#{e.message} (#{e.class})")
58
51
  exit_unable_to_finish
59
- rescue Exception => e
52
+ rescue Exception => e # rubocop:disable Lint/RescueException
60
53
  @err.puts("#{e.message} (#{e.class})")
61
54
  @err.puts(e.backtrace.join("\n"))
62
55
  exit_unable_to_finish
@@ -93,8 +86,15 @@ module Cucumber
93
86
  exit_unable_to_finish! if Cucumber.wants_to_quit
94
87
  Cucumber.wants_to_quit = true
95
88
  STDERR.puts "\nExiting... Interrupt again to exit immediately."
89
+ exit_unable_to_finish
96
90
  end
97
91
  end
92
+
93
+ def runtime(existing_runtime)
94
+ return Runtime.new(configuration) unless existing_runtime
95
+ existing_runtime.configure(configuration)
96
+ existing_runtime
97
+ end
98
98
  end
99
99
  end
100
100
  end
@@ -9,26 +9,30 @@ require 'cucumber/core/test/result'
9
9
  module Cucumber
10
10
  module Cli
11
11
  class Options
12
+ CUCUMBER_PUBLISH_URL = ENV['CUCUMBER_PUBLISH_URL'] || 'https://messages.cucumber.io/api/reports -X GET'
12
13
  INDENT = ' ' * 53
13
- # rubocop:disable Layout/MultilineOperationIndentation
14
14
  BUILTIN_FORMATS = {
15
- 'html' => ['Cucumber::Formatter::Html', 'Generates a nice looking HTML report.'],
16
15
  'pretty' => ['Cucumber::Formatter::Pretty', 'Prints the feature as is - in colours.'],
17
16
  'progress' => ['Cucumber::Formatter::Progress', 'Prints one character per scenario.'],
18
17
  'rerun' => ['Cucumber::Formatter::Rerun', 'Prints failing files with line numbers.'],
19
- 'usage' => ['Cucumber::Formatter::Usage', "Prints where step definitions are used.\n" +
20
- "#{INDENT}The slowest step definitions (with duration) are\n" +
21
- "#{INDENT}listed first. If --dry-run is used the duration\n" +
22
- "#{INDENT}is not shown, and step definitions are sorted by\n" +
18
+ 'usage' => ['Cucumber::Formatter::Usage', "Prints where step definitions are used.\n" \
19
+ "#{INDENT}The slowest step definitions (with duration) are\n" \
20
+ "#{INDENT}listed first. If --dry-run is used the duration\n" \
21
+ "#{INDENT}is not shown, and step definitions are sorted by\n" \
23
22
  "#{INDENT}filename instead."],
24
- 'stepdefs' => ['Cucumber::Formatter::Stepdefs', "Prints All step definitions with their locations. Same as\n" +
23
+ 'stepdefs' => ['Cucumber::Formatter::Stepdefs', "Prints All step definitions with their locations. Same as\n" \
25
24
  "#{INDENT}the usage formatter, except that steps are not printed."],
26
- 'junit' => ['Cucumber::Formatter::Junit', 'Generates a report similar to Ant+JUnit.'],
27
- 'json' => ['Cucumber::Formatter::Json', 'Prints the feature as JSON'],
28
- 'json_pretty' => ['Cucumber::Formatter::JsonPretty', 'Prints the feature as prettified JSON'],
25
+ 'junit' => ['Cucumber::Formatter::Junit', "Generates a report similar to Ant+JUnit. Use\n" \
26
+ "#{INDENT}junit,fileattribute=true to include a file attribute."],
27
+ 'json' => ['Cucumber::Formatter::Json', "Prints the feature as JSON.\n" \
28
+ "#{INDENT}The JSON format is in maintenance mode.\n" \
29
+ "#{INDENT}Please consider using the message formatter\n"\
30
+ "#{INDENT}with the standalone json-formatter\n" \
31
+ "#{INDENT}(https://github.com/cucumber/cucumber/tree/master/json-formatter)."],
32
+ 'message' => ['Cucumber::Formatter::Message', 'Prints each message in NDJSON form, which can then be consumed by other tools.'],
33
+ 'html' => ['Cucumber::Formatter::HTML', 'Outputs HTML report'],
29
34
  'summary' => ['Cucumber::Formatter::Summary', 'Summary output of feature and scenarios']
30
- }
31
- # rubocop:enable Layout/MultilineOperationIndentation
35
+ }.freeze
32
36
  max = BUILTIN_FORMATS.keys.map(&:length).max
33
37
  FORMAT_HELP_MSG = [
34
38
  'Use --format rerun --out rerun.txt to write out failing',
@@ -41,24 +45,24 @@ module Cucumber
41
45
  'foo/bar_zap.rb. You can place the file with this relative',
42
46
  'path underneath your features/support directory or anywhere',
43
47
  "on Ruby's LOAD_PATH, for example in a Ruby gem."
44
- ]
48
+ ].freeze
45
49
 
46
50
  FORMAT_HELP = (BUILTIN_FORMATS.keys.sort.map do |key|
47
51
  " #{key}#{' ' * (max - key.length)} : #{BUILTIN_FORMATS[key][1]}"
48
52
  end) + FORMAT_HELP_MSG
49
- PROFILE_SHORT_FLAG = '-p'
50
- NO_PROFILE_SHORT_FLAG = '-P'
51
- PROFILE_LONG_FLAG = '--profile'
52
- NO_PROFILE_LONG_FLAG = '--no-profile'
53
- FAIL_FAST_FLAG = '--fail-fast'
54
- RETRY_FLAG = '--retry'
53
+ PROFILE_SHORT_FLAG = '-p'.freeze
54
+ NO_PROFILE_SHORT_FLAG = '-P'.freeze
55
+ PROFILE_LONG_FLAG = '--profile'.freeze
56
+ NO_PROFILE_LONG_FLAG = '--no-profile'.freeze
57
+ FAIL_FAST_FLAG = '--fail-fast'.freeze
58
+ RETRY_FLAG = '--retry'.freeze
55
59
  OPTIONS_WITH_ARGS = [
56
60
  '-r', '--require', '--i18n-keywords', '-f', '--format', '-o',
57
61
  '--out', '-t', '--tags', '-n', '--name', '-e', '--exclude',
58
62
  PROFILE_SHORT_FLAG, PROFILE_LONG_FLAG, RETRY_FLAG, '-l',
59
63
  '--lines', '--port', '-I', '--snippet-type'
60
- ]
61
- ORDER_TYPES = %w{defined random}
64
+ ].freeze
65
+ ORDER_TYPES = %w[defined random].freeze
62
66
  TAG_LIMIT_MATCHER = /(?<tag_name>\@\w+):(?<limit>\d+)/x
63
67
 
64
68
  def self.parse(args, out_stream, error_stream, options = {})
@@ -87,19 +91,21 @@ module Cucumber
87
91
  @options[key] = value
88
92
  end
89
93
 
90
- def parse!(args) # rubocop:disable Metrics/AbcSize
94
+ def parse!(args) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
91
95
  @args = args
92
96
  @expanded_args = @args.dup
93
97
 
94
98
  @args.extend(::OptionParser::Arguable)
95
99
 
96
- @args.options do |opts|
100
+ @args.options do |opts| # rubocop:disable Metrics/BlockLength
97
101
  opts.banner = banner
102
+ opts.on('--publish', 'Publish a report to https://reports.cucumber.io') do
103
+ set_option :publish_enabled, true
104
+ end
105
+ opts.on('--publish-quiet', 'Don\'t print information banner about publishing reports') { set_option :publish_quiet }
98
106
  opts.on('-r LIBRARY|DIR', '--require LIBRARY|DIR', *require_files_msg) { |lib| require_files(lib) }
99
107
 
100
- if Cucumber::JRUBY
101
- opts.on('-j DIR', '--jars DIR', 'Load all the jars under DIR') { |jars| load_jars(jars) }
102
- end
108
+ opts.on('-j DIR', '--jars DIR', 'Load all the jars under DIR') { |jars| load_jars(jars) } if Cucumber::JRUBY
103
109
 
104
110
  opts.on("#{RETRY_FLAG} ATTEMPTS", *retry_msg) { |v| set_option :retry, v.to_i }
105
111
  opts.on('--i18n-languages', *i18n_languages_msg) { list_languages_and_exit }
@@ -108,20 +114,20 @@ module Cucumber
108
114
  opts.on('-f FORMAT', '--format FORMAT', *format_msg, *FORMAT_HELP) do |v|
109
115
  add_option :formats, [*parse_formats(v), @out_stream]
110
116
  end
111
- opts.on('--init', *init_msg) { |v| initialize_project }
112
- opts.on('-o', '--out [FILE|DIR]', *out_msg) { |v| out_stream v }
117
+ opts.on('--init', *init_msg) { |_v| initialize_project }
118
+ opts.on('-o', '--out [FILE|DIR|URL]', *out_msg) { |v| out_stream v }
113
119
  opts.on('-t TAG_EXPRESSION', '--tags TAG_EXPRESSION', *tags_msg) { |v| add_tag v }
114
120
  opts.on('-n NAME', '--name NAME', *name_msg) { |v| add_option :name_regexps, /#{v}/ }
115
121
  opts.on('-e', '--exclude PATTERN', *exclude_msg) { |v| add_option :excludes, Regexp.new(v) }
116
122
  opts.on(PROFILE_SHORT_FLAG, "#{PROFILE_LONG_FLAG} PROFILE", *profile_short_flag_msg) { |v| add_profile v }
117
- opts.on(NO_PROFILE_SHORT_FLAG, NO_PROFILE_LONG_FLAG, *no_profile_short_flag_msg) { |v| disable_profile_loading }
123
+ opts.on(NO_PROFILE_SHORT_FLAG, NO_PROFILE_LONG_FLAG, *no_profile_short_flag_msg) { |_v| disable_profile_loading }
118
124
  opts.on('-c', '--[no-]color', *color_msg) { |v| color v }
119
125
  opts.on('-d', '--dry-run', *dry_run_msg) { set_dry_run_and_duration }
120
126
  opts.on('-m', '--no-multiline', "Don't print multiline strings and tables under steps.") { set_option :no_multiline }
121
127
  opts.on('-s', '--no-source', "Don't print the file and line of the step definition with the steps.") { set_option :source, false }
122
128
  opts.on('-i', '--no-snippets', "Don't print snippets for pending steps.") { set_option :snippets, false }
123
129
  opts.on('-I', '--snippet-type TYPE', *snippet_type_msg) { |v| set_option :snippet_type, v.to_sym }
124
- opts.on('-q', '--quiet', 'Alias for --no-snippets --no-source.') { shut_up }
130
+ opts.on('-q', '--quiet', 'Alias for --no-snippets --no-source --no-duration --publish-quiet.') { shut_up }
125
131
  opts.on('--no-duration', "Don't print the duration at the end of the summary") { set_option :duration, false }
126
132
  opts.on('-b', '--backtrace', 'Show full backtrace for all errors.') { Cucumber.use_full_backtrace = true }
127
133
  opts.on('-S', '--[no-]strict', *strict_msg) { |setting| set_strict(setting) }
@@ -140,23 +146,23 @@ module Cucumber
140
146
  [random] Shuffle scenarios before running.
141
147
  Specify SEED to reproduce the shuffling from a previous run.
142
148
  e.g. --order random:5738
143
- TEXT
149
+ TEXT
144
150
  @options[:order], @options[:seed] = *order.split(':')
145
- unless ORDER_TYPES.include?(@options[:order])
146
- fail "'#{@options[:order]}' is not a recognised order type. Please use one of #{ORDER_TYPES.join(", ")}."
147
- end
151
+ raise "'#{@options[:order]}' is not a recognised order type. Please use one of #{ORDER_TYPES.join(', ')}." unless ORDER_TYPES.include?(@options[:order])
148
152
  end
149
153
 
150
154
  opts.on_tail('--version', 'Show version.') { exit_ok(Cucumber::VERSION) }
151
155
  opts.on_tail('-h', '--help', "You're looking at it.") { exit_ok(opts.help) }
152
156
  end.parse!
153
157
 
158
+ process_publish_options
159
+
154
160
  @args.map! { |a| "#{a}:#{@options[:lines]}" } if @options[:lines]
155
161
 
156
162
  extract_environment_variables
157
163
  @options[:paths] = @args.dup # whatver is left over
158
164
 
159
- check_formatter_stream_conflicts()
165
+ check_formatter_stream_conflicts
160
166
 
161
167
  merge_profiles
162
168
 
@@ -171,7 +177,7 @@ TEXT
171
177
  @options[:filters] ||= []
172
178
  end
173
179
 
174
- def check_formatter_stream_conflicts()
180
+ def check_formatter_stream_conflicts
175
181
  streams = @options[:formats].uniq.map { |(_, _, stream)| stream }
176
182
  return if streams == streams.uniq
177
183
  raise 'All but one formatter must use --out, only one can print to each stream (or STDOUT)'
@@ -188,6 +194,18 @@ TEXT
188
194
 
189
195
  private
190
196
 
197
+ def process_publish_options
198
+ @options[:publish_enabled] = true if truthy_string?(ENV['CUCUMBER_PUBLISH_ENABLED']) || ENV['CUCUMBER_PUBLISH_TOKEN']
199
+ @options[:formats] << publisher if @options[:publish_enabled]
200
+
201
+ @options[:publish_quiet] = true if truthy_string?(ENV['CUCUMBER_PUBLISH_QUIET'])
202
+ end
203
+
204
+ def truthy_string?(str)
205
+ return false if str.nil?
206
+ str !~ /^(false|no|0)$/i
207
+ end
208
+
191
209
  def color_msg
192
210
  [
193
211
  'Whether or not to use ANSI color in the output. Cucumber decides',
@@ -196,10 +214,7 @@ TEXT
196
214
  end
197
215
 
198
216
  def dry_run_msg
199
- [
200
- 'Invokes formatters without executing the steps.',
201
- 'This also omits the loading of your support/env.rb file if it exists.'
202
- ]
217
+ ['Invokes formatters without executing the steps.']
203
218
  end
204
219
 
205
220
  def exclude_msg
@@ -219,7 +234,7 @@ TEXT
219
234
  def i18n_keywords_msg
220
235
  [
221
236
  'List keywords for in a particular language',
222
- %{Run with "--i18n help" to see all languages}
237
+ %(Run with "--i18n help" to see all languages)
223
238
  ]
224
239
  end
225
240
 
@@ -305,10 +320,14 @@ TEXT
305
320
 
306
321
  def out_msg
307
322
  [
308
- 'Write output to a file/directory instead of STDOUT. This option',
323
+ 'Write output to a file/directory/URL instead of STDOUT. This option',
309
324
  'applies to the previously specified --format, or the',
310
325
  'default format if no format is specified. Check the specific',
311
- "formatter's docs to see whether to pass a file or a dir."
326
+ "formatter's docs to see whether to pass a file, dir or URL.",
327
+ "\n",
328
+ 'When using a URL, the output of the formatter will be sent as the HTTP request body.',
329
+ 'HTTP headers and request method can be set with cURL like options.',
330
+ 'Example: --out "http://example.com -X POST -H Content-Type:text/json"'
312
331
  ]
313
332
  end
314
333
 
@@ -316,11 +335,13 @@ TEXT
316
335
  [
317
336
  'Require files before executing the features. If this',
318
337
  'option is not specified, all *.rb files that are',
319
- 'siblings or below the features will be loaded auto-',
338
+ 'siblings of or below the features will be loaded auto-',
320
339
  'matically. Automatic loading is disabled when this',
321
- 'option is specified, and all loading becomes explicit.',
322
- 'Files under directories named "support" are always',
323
- 'loaded first.',
340
+ 'option is specified; all loading becomes explicit.',
341
+ 'Files in directories named "support" are still always',
342
+ 'loaded first when their parent directories are',
343
+ 'required or if the "support" directories themselves are',
344
+ 'explicitly required.',
324
345
  'This option can be specified multiple times.'
325
346
  ]
326
347
  end
@@ -351,13 +372,19 @@ TEXT
351
372
  end
352
373
 
353
374
  def require_jars(jars)
354
- Dir["#{jars}/**/*.jar"].each { |jar| require jar }
375
+ Dir["#{jars}/**/*.jar"].sort.each { |jar| require jar }
376
+ end
377
+
378
+ def publisher
379
+ url = CUCUMBER_PUBLISH_URL
380
+ url += %( -H "Authorization: Bearer #{ENV['CUCUMBER_PUBLISH_TOKEN']}") if ENV['CUCUMBER_PUBLISH_TOKEN']
381
+ ['message', {}, url]
355
382
  end
356
383
 
357
384
  def language(lang)
358
385
  require 'gherkin/dialect'
359
386
 
360
- return indicate_invalid_language_and_exit(lang) unless ::Gherkin::DIALECTS.keys.include? lang
387
+ return indicate_invalid_language_and_exit(lang) unless ::Gherkin::DIALECTS.key?(lang)
361
388
  list_keywords_and_exit(lang)
362
389
  end
363
390
 
@@ -366,7 +393,7 @@ TEXT
366
393
  end
367
394
 
368
395
  def non_stdout_formats
369
- @options[:formats].select { |_, _, output| output != @out_stream }
396
+ @options[:formats].reject { |_, _, output| output == @out_stream }
370
397
  end
371
398
 
372
399
  def add_option(option, value)
@@ -374,8 +401,8 @@ TEXT
374
401
  end
375
402
 
376
403
  def add_tag(value)
377
- warn("Deprecated: Found tags option '#{value}'. Support for '~@tag' will be removed from the next release of Cucumber. Please use 'not @tag' instead.") if value.include?('~')
378
- warn("Deprecated: Found tags option '#{value}'. Support for '@tag1,@tag2' will be removed from the next release of Cucumber. Please use '@tag or @tag2' instead.") if value.include?(',')
404
+ raise("Found tags option '#{value}'. '~@tag' is no longer supported, use 'not @tag' instead.") if value.include?('~')
405
+ raise("Found tags option '#{value}'. '@tag1,@tag2' is no longer supported, use '@tag or @tag2' instead.") if value.include?(',')
379
406
  @options[:tag_expressions] << value.gsub(/(@\w+)(:\d+)?/, '\1')
380
407
  add_tag_limits(value)
381
408
  end
@@ -387,9 +414,7 @@ TEXT
387
414
  end
388
415
 
389
416
  def add_tag_limit(tag_limits, tag_name, limit)
390
- if tag_limits[tag_name] && tag_limits[tag_name] != limit
391
- raise "Inconsistent tag limits for #{tag_name}: #{tag_limits[tag_name]} and #{limit}"
392
- end
417
+ raise "Inconsistent tag limits for #{tag_name}: #{tag_limits[tag_name]} and #{limit}" if tag_limits[tag_name] && tag_limits[tag_name] != limit
393
418
  tag_limits[tag_name] = limit
394
419
  end
395
420
 
@@ -420,6 +445,7 @@ TEXT
420
445
  end
421
446
 
422
447
  def shut_up
448
+ @options[:publish_quiet] = true
423
449
  @options[:snippets] = false
424
450
  @options[:source] = false
425
451
  @options[:duration] = false
@@ -436,7 +462,7 @@ TEXT
436
462
  def extract_environment_variables
437
463
  @args.delete_if do |arg|
438
464
  if arg =~ /^(\w+)=(.*)$/
439
- @options[:env_vars][$1] = $2
465
+ @options[:env_vars][Regexp.last_match(1)] = Regexp.last_match(2)
440
466
  true
441
467
  end
442
468
  end
@@ -465,8 +491,8 @@ TEXT
465
491
  profile_args = profile_loader.args_from(profile)
466
492
  profile_options = Options.parse(
467
493
  profile_args, @out_stream, @error_stream,
468
- :skip_profile_information => true,
469
- :profile_loader => profile_loader
494
+ skip_profile_information: true,
495
+ profile_loader: profile_loader
470
496
  )
471
497
  reverse_merge(profile_options)
472
498
  end
@@ -474,14 +500,14 @@ TEXT
474
500
  def default_profile_should_be_used?
475
501
  @profiles.empty? &&
476
502
  profile_loader.cucumber_yml_defined? &&
477
- profile_loader.has_profile?(@default_profile)
503
+ profile_loader.profile?(@default_profile)
478
504
  end
479
505
 
480
506
  def profile_loader
481
507
  @profile_loader ||= ProfileLoader.new
482
508
  end
483
509
 
484
- def reverse_merge(other_options)
510
+ def reverse_merge(other_options) # rubocop:disable Metrics/AbcSize
485
511
  @options = other_options.options.merge(@options)
486
512
  @options[:require] += other_options[:require]
487
513
  @options[:excludes] += other_options[:excludes]
@@ -510,7 +536,7 @@ TEXT
510
536
  @options[:formats] = stdout_formats[0..0] + non_stdout_formats
511
537
  end
512
538
 
513
- @options[:retry] = other_options[:retry] if @options[:retry] == 0
539
+ @options[:retry] = other_options[:retry] if @options[:retry].zero?
514
540
 
515
541
  self
516
542
  end
@@ -546,7 +572,7 @@ TEXT
546
572
  ['but (code)', to_code_keywords_string(language.but_keywords)]
547
573
  ]
548
574
  )
549
- @out_stream.write(data.to_s({ color: false, prefixes: Hash.new('') }))
575
+ @out_stream.write(data.to_s(color: false, prefixes: Hash.new('')))
550
576
  Kernel.exit(0)
551
577
  end
552
578
 
@@ -557,7 +583,7 @@ TEXT
557
583
  [key, ::Gherkin::DIALECTS[key].fetch('name'), ::Gherkin::DIALECTS[key].fetch('native')]
558
584
  end
559
585
  )
560
- @out_stream.write(data.to_s({ color: false, prefixes: Hash.new('') }))
586
+ @out_stream.write(data.to_s(color: false, prefixes: Hash.new('')))
561
587
  Kernel.exit(0)
562
588
  end
563
589
 
@@ -571,20 +597,20 @@ TEXT
571
597
 
572
598
  def default_options
573
599
  {
574
- :strict => Cucumber::Core::Test::Result::StrictConfiguration.new,
575
- :require => [],
576
- :dry_run => false,
577
- :formats => [],
578
- :excludes => [],
579
- :tag_expressions => [],
580
- :tag_limits => {},
581
- :name_regexps => [],
582
- :env_vars => {},
583
- :diff_enabled => true,
584
- :snippets => true,
585
- :source => true,
586
- :duration => true,
587
- :retry => 0
600
+ strict: Cucumber::Core::Test::Result::StrictConfiguration.new,
601
+ require: [],
602
+ dry_run: false,
603
+ formats: [],
604
+ excludes: [],
605
+ tag_expressions: [],
606
+ tag_limits: {},
607
+ name_regexps: [],
608
+ env_vars: {},
609
+ diff_enabled: true,
610
+ snippets: true,
611
+ source: true,
612
+ duration: true,
613
+ retry: 0
588
614
  }
589
615
  end
590
616
  end
@@ -16,26 +16,19 @@ Could not find profile: '#{profile}'
16
16
 
17
17
  Defined profiles in cucumber.yml:
18
18
  * #{cucumber_yml.keys.sort.join("\n * ")}
19
- END_OF_ERROR
19
+ END_OF_ERROR
20
20
  end
21
21
 
22
22
  args_from_yml = cucumber_yml[profile] || ''
23
23
 
24
- require 'shellwords'
25
-
26
24
  case args_from_yml
27
25
  when String
28
- raise YmlLoadError, "The '#{profile}' profile in cucumber.yml was blank. Please define the command line arguments for the '#{profile}' profile in cucumber.yml.\n" if args_from_yml =~ /^\s*$/
29
- if Cucumber::WINDOWS
30
- # Shellwords treats backslash as an escape character so we have to mask it out temporarily
31
-
32
- placeholder = 'pseudo_unique_backslash_placeholder'
33
- sanitized_line = args_from_yml.gsub('\\', placeholder)
34
-
35
- args_from_yml = Shellwords.shellwords(sanitized_line).collect { |argument| argument.gsub(placeholder, '\\') }
36
- else
37
- args_from_yml = Shellwords.shellwords(args_from_yml)
26
+ if args_from_yml =~ /^\s*$/
27
+ raise YmlLoadError, "The '#{profile}' profile in cucumber.yml was blank." \
28
+ " Please define the command line arguments for the '#{profile}' profile in cucumber.yml.\n"
38
29
  end
30
+
31
+ args_from_yml = processed_shellwords(args_from_yml)
39
32
  when Array
40
33
  raise YmlLoadError, "The '#{profile}' profile in cucumber.yml was empty. Please define the command line arguments for the '#{profile}' profile in cucumber.yml.\n" if args_from_yml.empty?
41
34
  else
@@ -45,7 +38,7 @@ Defined profiles in cucumber.yml:
45
38
  args_from_yml
46
39
  end
47
40
 
48
- def has_profile?(profile)
41
+ def profile?(profile)
49
42
  cucumber_yml.key?(profile)
50
43
  end
51
44
 
@@ -58,29 +51,47 @@ Defined profiles in cucumber.yml:
58
51
  # Loads the profile, processing it through ERB and YAML, and returns it as a hash.
59
52
  def cucumber_yml
60
53
  return @cucumber_yml if @cucumber_yml
61
- unless cucumber_yml_defined?
62
- raise(ProfilesNotDefinedError, "cucumber.yml was not found. Current directory is #{Dir.pwd}. Please refer to cucumber's documentation on defining profiles in cucumber.yml. You must define a 'default' profile to use the cucumber command without any arguments.\nType 'cucumber --help' for usage.\n")
54
+
55
+ ensure_configuration_file_exists
56
+ process_configuration_file_with_erb
57
+ load_configuration
58
+
59
+ if @cucumber_yml.nil? || !@cucumber_yml.is_a?(Hash)
60
+ raise(YmlLoadError, 'cucumber.yml was found, but was blank or malformed. ' \
61
+ "Please refer to cucumber's documentation on correct profile usage.\n")
63
62
  end
64
63
 
64
+ @cucumber_yml
65
+ end
66
+
67
+ def ensure_configuration_file_exists
68
+ return if cucumber_yml_defined?
69
+
70
+ raise(ProfilesNotDefinedError, "cucumber.yml was not found. Current directory is #{Dir.pwd}." \
71
+ "Please refer to cucumber's documentation on defining profiles in cucumber.yml. You must define" \
72
+ "a 'default' profile to use the cucumber command without any arguments.\nType 'cucumber --help' for usage.\n")
73
+ end
74
+
75
+ def process_configuration_file_with_erb
65
76
  require 'erb'
66
- require 'yaml'
67
77
  begin
68
- @cucumber_erb = ERB.new(IO.read(cucumber_file), nil, '%').result(binding)
78
+ @cucumber_erb = if RUBY_VERSION >= '2.6'
79
+ ERB.new(IO.read(cucumber_file), trim_mode: '%').result(binding)
80
+ else
81
+ ERB.new(IO.read(cucumber_file), nil, '%').result(binding)
82
+ end
69
83
  rescue StandardError
70
- raise(YmlLoadError, "cucumber.yml was found, but could not be parsed with ERB. Please refer to cucumber's documentation on correct profile usage.\n#{$!.inspect}")
84
+ raise(YmlLoadError, "cucumber.yml was found, but could not be parsed with ERB. Please refer to cucumber's documentation on correct profile usage.\n#{$ERROR_INFO.inspect}")
71
85
  end
86
+ end
72
87
 
88
+ def load_configuration
89
+ require 'yaml'
73
90
  begin
74
- @cucumber_yml = YAML.load(@cucumber_erb)
91
+ @cucumber_yml = YAML.load(@cucumber_erb) # rubocop:disable Security/YAMLLoad
75
92
  rescue StandardError
76
93
  raise(YmlLoadError, "cucumber.yml was found, but could not be parsed. Please refer to cucumber's documentation on correct profile usage.\n")
77
94
  end
78
-
79
- if @cucumber_yml.nil? || !@cucumber_yml.is_a?(Hash)
80
- raise(YmlLoadError, "cucumber.yml was found, but was blank or malformed. Please refer to cucumber's documentation on correct profile usage.\n")
81
- end
82
-
83
- return @cucumber_yml
84
95
  end
85
96
 
86
97
  # Locates cucumber.yml file. The file can end in .yml or .yaml,
@@ -89,6 +100,18 @@ Defined profiles in cucumber.yml:
89
100
  def cucumber_file
90
101
  @cucumber_file ||= Dir.glob('{,.config/,config/}cucumber{.yml,.yaml}').first
91
102
  end
103
+
104
+ def processed_shellwords(args_from_yml)
105
+ require 'shellwords'
106
+
107
+ return Shellwords.shellwords(args_from_yml) unless Cucumber::WINDOWS
108
+
109
+ # Shellwords treats backslash as an escape character so we have to mask it out temporarily
110
+ placeholder = 'pseudo_unique_backslash_placeholder'
111
+ sanitized_line = args_from_yml.gsub('\\', placeholder)
112
+
113
+ Shellwords.shellwords(sanitized_line).collect { |argument| argument.gsub(placeholder, '\\') }
114
+ end
92
115
  end
93
116
  end
94
117
  end