cucumber 5.1.1 → 6.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.
data/CONTRIBUTING.md CHANGED
@@ -21,17 +21,29 @@ You can chat with the core team on https://gitter.im/cucumber/contributors. We t
21
21
 
22
22
  ## Installing your own gems
23
23
 
24
- A `Gemfile.local`-file can be used to have your own gems installed to support
25
- your normal development workflow.
24
+ A `Gemfile.local`-file can be used to have your own gems installed to support your normal development workflow.
25
+ Execute `bundle config set --local gemfile Gemfile.local` to use it per default.
26
26
 
27
27
  Example:
28
28
 
29
29
  ~~~ruby
30
- gem 'pry'
31
- gem 'pry-byebug'
32
- gem 'byebug'
30
+ # Include the regular Gemfile
31
+ eval File.read('Gemfile')
32
+
33
+ group :development do
34
+ gem 'byebug'
35
+ gem 'debase', require: false
36
+ gem 'ruby-debug-ide', require: false
37
+ gem 'pry'
38
+ gem 'pry-byebug'
39
+ end
33
40
  ~~~
34
41
 
42
+ ## Using Visual Studio Code?
43
+
44
+ Sample for launch.json configuration is available in
45
+ [docs/vscode-example-launch-configuration.md](https://github.com/cucumber/cucumber-ruby/blob/master/docs/vscode-example-launch-configuration.md)
46
+
35
47
  ## Note on Patches/Pull Requests
36
48
 
37
49
  * Fork the project. Make a branch for your change.
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  [![OpenCollective](https://opencollective.com/cucumber/backers/badge.svg)](https://opencollective.com/cucumber)
2
2
  [![OpenCollective](https://opencollective.com/cucumber/sponsors/badge.svg)](https://opencollective.com/cucumber)
3
-
3
+ [![pull requests](https://oselvar.com/api/badge?label=pull%20requests&csvUrl=https%3A%2F%2Fraw.githubusercontent.com%2Fcucumber%2Foselvar-github-metrics%2Fmain%2Fdata%2Fcucumber%2Fcucumber-ruby%2FpullRequests.csv)](https://oselvar.com/github/cucumber/oselvar-github-metrics/main/cucumber/cucumber-ruby)
4
+ [![issues](https://oselvar.com/api/badge?label=issues&csvUrl=https%3A%2F%2Fraw.githubusercontent.com%2Fcucumber%2Foselvar-github-metrics%2Fmain%2Fdata%2Fcucumber%2Fcucumber-ruby%2Fissues.csv)](https://oselvar.com/github/cucumber/oselvar-github-metrics/main/cucumber/cucumber-ruby)
4
5
  [![CircleCI](https://circleci.com/gh/cucumber/cucumber-ruby.svg?style=svg)](https://circleci.com/gh/cucumber/cucumber-ruby)
5
-
6
6
  [![Code Climate](https://codeclimate.com/github/cucumber/cucumber-ruby.svg)](https://codeclimate.com/github/cucumber/cucumber-ruby)
7
7
  [![Coverage Status](https://coveralls.io/repos/cucumber/cucumber-ruby/badge.svg?branch=master)](https://coveralls.io/r/cucumber/cucumber-ruby?branch=master)
8
8
 
@@ -24,6 +24,8 @@ Where to get more info:
24
24
  See [CONTRIBUTING.md](CONTRIBUTING.md) for info on contributing to Cucumber.
25
25
 
26
26
  ## Supported platforms
27
+ * Ruby 3.0
28
+ * Ruby 2.7
27
29
  * Ruby 2.6
28
30
  * Ruby 2.5
29
31
  * Ruby 2.4
@@ -126,13 +126,35 @@ 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
135
  @options.check_formatter_stream_conflicts
135
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?
157
+ end
136
158
  end
137
159
  end
138
160
  end
@@ -22,7 +22,8 @@ module Cucumber
22
22
  "#{INDENT}filename instead."],
23
23
  'stepdefs' => ['Cucumber::Formatter::Stepdefs', "Prints All step definitions with their locations. Same as\n" \
24
24
  "#{INDENT}the usage formatter, except that steps are not printed."],
25
- 'junit' => ['Cucumber::Formatter::Junit', 'Generates a report similar to Ant+JUnit.'],
25
+ 'junit' => ['Cucumber::Formatter::Junit', "Generates a report similar to Ant+JUnit. Use\n" \
26
+ "#{INDENT}junit,fileattribute=true to include a file attribute."],
26
27
  'json' => ['Cucumber::Formatter::Json', '[DEPRECATED] Prints the feature as JSON'],
27
28
  'message' => ['Cucumber::Formatter::Message', 'Outputs protobuf messages'],
28
29
  'html' => ['Cucumber::Formatter::HTML', 'Outputs HTML report'],
@@ -335,7 +336,7 @@ Specify SEED to reproduce the shuffling from a previous run.
335
336
  'option is specified; all loading becomes explicit.',
336
337
  'Files in directories named "support" are still always',
337
338
  'loaded first when their parent directories are',
338
- 'required or if the "support" directoires themselves are',
339
+ 'required or if the "support" directories themselves are',
339
340
  'explicitly required.',
340
341
  'This option can be specified multiple times.'
341
342
  ]
@@ -15,6 +15,7 @@ module Cucumber
15
15
  test/unit
16
16
  .gem/ruby
17
17
  bin/bundle
18
+ rdebug-ide
18
19
  ]
19
20
 
20
21
  @backtrace_filters << RbConfig::CONFIG['rubyarchdir'] if RbConfig::CONFIG['rubyarchdir']
@@ -8,7 +8,7 @@ module Cucumber
8
8
  include Io
9
9
 
10
10
  def initialize(config)
11
- @io = ensure_io(config.out_stream)
11
+ @io = ensure_io(config.out_stream, config.error_stream)
12
12
  @html_formatter = Cucumber::HTMLFormatter::Formatter.new(@io)
13
13
  @html_formatter.write_pre_message
14
14
 
@@ -1,5 +1,6 @@
1
1
  require 'net/http'
2
2
  require 'tempfile'
3
+ require 'shellwords'
3
4
 
4
5
  module Cucumber
5
6
  module Formatter
@@ -18,48 +19,43 @@ module Cucumber
18
19
 
19
20
  class CurlOptionParser
20
21
  def self.parse(options)
21
- chunks = options.split(/\s/).compact
22
- http_method = 'PUT'
23
- url = chunks[0]
24
- headers = ''
25
-
26
- last_flag = nil
27
- chunks.each do |chunk|
28
- if ['-X', '--request'].include?(chunk)
29
- last_flag = '-X'
30
- next
31
- end
32
-
33
- if chunk == '-H'
34
- last_flag = '-H'
35
- next
36
- end
22
+ args = Shellwords.split(options)
37
23
 
38
- if last_flag == '-X'
39
- http_method = chunk
40
- last_flag = nil
24
+ url = nil
25
+ http_method = 'PUT'
26
+ headers = {}
27
+
28
+ until args.empty?
29
+ arg = args.shift
30
+ case arg
31
+ when '-X', '--request'
32
+ http_method = remove_arg_for(args, arg)
33
+ when '-H'
34
+ header_arg = remove_arg_for(args, arg)
35
+ headers = headers.merge(parse_header(header_arg))
36
+ else
37
+ raise StandardError, "#{options} was not a valid curl command. Can't set url to #{arg} it is already set to #{url}" if url
38
+ url = arg
41
39
  end
42
-
43
- headers += chunk if last_flag == '-H'
44
40
  end
41
+ raise StandardError, "#{options} was not a valid curl command" unless url
45
42
 
46
43
  [
47
44
  url,
48
45
  http_method,
49
- make_headers(headers)
46
+ headers
50
47
  ]
51
48
  end
52
49
 
53
- def self.make_headers(headers)
54
- hash_headers = {}
55
- str_scanner = /("(?<key>[^":]+)\s*:\s*(?<value>[^":]+)")|('(?<key1>[^':]+)\s*:\s*(?<value1>[^':]+)')/
56
-
57
- headers.scan(str_scanner) do |header|
58
- header = header.compact!
59
- hash_headers[header[0]] = header[1]&.strip
60
- end
50
+ def self.remove_arg_for(args, arg)
51
+ return args.shift unless args.empty?
52
+ raise StandardError, "Missing argument for #{arg}"
53
+ end
61
54
 
62
- hash_headers
55
+ def self.parse_header(header_arg)
56
+ parts = header_arg.split(':', 2)
57
+ raise StandardError, "#{header_arg} was not a valid header" unless parts.length == 2
58
+ { parts[0].strip => parts[1].strip }
63
59
  end
64
60
  end
65
61
 
@@ -76,10 +72,11 @@ module Cucumber
76
72
  end
77
73
 
78
74
  def close
79
- resource_uri = send_content(@uri, @method, @headers)
80
-
81
- @reporter.report(resource_uri)
75
+ response = send_content(@uri, @method, @headers)
76
+ @reporter.report(response.body)
82
77
  @write_io.close
78
+ return if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPRedirection)
79
+ raise StandardError, "request to #{uri} failed with status #{response.code}"
83
80
  end
84
81
 
85
82
  def write(data)
@@ -121,16 +118,11 @@ module Cucumber
121
118
 
122
119
  case response
123
120
  when Net::HTTPAccepted
124
- return uri unless response['Location']
125
-
126
- send_content(URI(response['Location']), 'PUT', headers, attempt - 1)
127
- when Net::HTTPSuccess
128
- uri
121
+ send_content(URI(response['Location']), 'PUT', {}, attempt - 1) if response['Location']
129
122
  when Net::HTTPRedirection
130
123
  send_content(URI(response['Location']), method, headers, attempt - 1)
131
- else
132
- raise StandardError, "request to #{uri} failed with status #{response.code}"
133
124
  end
125
+ response
134
126
  end
135
127
 
136
128
  def build_request(uri, method, headers)
@@ -9,13 +9,13 @@ module Cucumber
9
9
  module Io
10
10
  module_function
11
11
 
12
- def ensure_io(path_or_url_or_io)
12
+ def ensure_io(path_or_url_or_io, error_stream)
13
13
  return nil if path_or_url_or_io.nil?
14
14
  return path_or_url_or_io if io?(path_or_url_or_io)
15
15
 
16
16
  io = if url?(path_or_url_or_io)
17
17
  url = path_or_url_or_io
18
- reporter = url.start_with?(Cucumber::Cli::Options::CUCUMBER_PUBLISH_URL) ? URLReporter.new($stderr) : NoReporter.new
18
+ reporter = url.start_with?(Cucumber::Cli::Options::CUCUMBER_PUBLISH_URL) ? URLReporter.new(error_stream) : NoReporter.new
19
19
  HTTPIO.open(url, nil, reporter)
20
20
  else
21
21
  File.open(path_or_url_or_io, Cucumber.file_mode('w'))
@@ -64,7 +64,7 @@ module Cucumber
64
64
  raise "You *must* specify --out FILE for the #{name} formatter" unless String == path.class
65
65
  raise "I can't write #{name} to a directory - it has to be a file" if File.directory?(path)
66
66
  raise "I can't write #{name} to a file in the non-existing directory #{File.dirname(path)}" unless File.directory?(File.dirname(path))
67
- ensure_io(path)
67
+ ensure_io(path, nil)
68
68
  end
69
69
 
70
70
  def ensure_dir(path, name)
@@ -22,7 +22,7 @@ module Cucumber
22
22
  '6.0.0'
23
23
  )
24
24
 
25
- @io = ensure_io(config.out_stream)
25
+ @io = ensure_io(config.out_stream, config.error_stream)
26
26
  @ast_lookup = AstLookup.new(config)
27
27
  @feature_hashes = []
28
28
  @step_or_hook_hash = {}
@@ -94,10 +94,7 @@ module Cucumber
94
94
  test_step_output << src
95
95
  return
96
96
  end
97
- if File.file?(src)
98
- content = File.open(src, 'rb', &:read)
99
- data = encode64(content)
100
- elsif mime_type =~ /;base64$/
97
+ if mime_type =~ /;base64$/
101
98
  mime_type = mime_type[0..-8]
102
99
  data = src
103
100
  else
@@ -132,9 +132,12 @@ module Cucumber
132
132
  duration = ResultBuilder.new(result).test_case_duration
133
133
  @current_feature_data[:time] += duration
134
134
  classname = @current_feature_data[:feature].name
135
+ filename = @current_feature_data[:uri]
135
136
  name = scenario_designation
136
137
 
137
- @current_feature_data[:builder].testcase(classname: classname, name: name, time: format('%<duration>.6f', duration: duration)) do
138
+ testcase_attributes = get_testcase_attributes(classname, name, duration, filename)
139
+
140
+ @current_feature_data[:builder].testcase(testcase_attributes) do
138
141
  if !result.passed? && result.ok?(@config.strict)
139
142
  @current_feature_data[:builder].skipped
140
143
  @current_feature_data[:skipped] += 1
@@ -157,6 +160,20 @@ module Cucumber
157
160
  @current_feature_data[:tests] += 1
158
161
  end
159
162
 
163
+ def get_testcase_attributes(classname, name, duration, filename)
164
+ { classname: classname, name: name, time: format('%<duration>.6f', duration: duration) }.tap do |attributes|
165
+ attributes[:file] = filename if add_fileattribute?
166
+ end
167
+ end
168
+
169
+ def add_fileattribute?
170
+ return false if @config.formats.nil? || @config.formats.empty?
171
+
172
+ !!@config.formats.find do |format|
173
+ format.first == 'junit' && format.dig(1, 'fileattribute') == 'true'
174
+ end
175
+ end
176
+
160
177
  def get_backtrace_object(result)
161
178
  if result.failed?
162
179
  result.exception
@@ -10,7 +10,7 @@ module Cucumber
10
10
  include Io
11
11
 
12
12
  def initialize(config)
13
- @io = ensure_io(config.out_stream)
13
+ @io = ensure_io(config.out_stream, config.error_stream)
14
14
  super(config)
15
15
  end
16
16
 
@@ -32,7 +32,7 @@ module Cucumber
32
32
  private :in_scenario_outline, :print_background_steps
33
33
 
34
34
  def initialize(config)
35
- @io = ensure_io(config.out_stream)
35
+ @io = ensure_io(config.out_stream, config.error_stream)
36
36
  @config = config
37
37
  @options = config.to_hash
38
38
  @snippets_input = []
@@ -19,7 +19,7 @@ module Cucumber
19
19
 
20
20
  def initialize(config)
21
21
  @config = config
22
- @io = ensure_io(config.out_stream)
22
+ @io = ensure_io(config.out_stream, config.error_stream)
23
23
  @snippets_input = []
24
24
  @undefined_parameter_types = []
25
25
  @total_duration = 0
@@ -30,7 +30,9 @@ module Cucumber
30
30
  ],
31
31
  [
32
32
  'Environment variable: ',
33
- highlight('CUCUMBER_PUBLISH_ENABLED=true')
33
+ highlight('CUCUMBER_PUBLISH_ENABLED'),
34
+ '=',
35
+ highlight('true')
34
36
  ],
35
37
  [
36
38
  'cucumber.yml: ',
@@ -8,7 +8,7 @@ module Cucumber
8
8
  include Formatter::Io
9
9
 
10
10
  def initialize(config)
11
- @io = ensure_io(config.out_stream)
11
+ @io = ensure_io(config.out_stream, config.error_stream)
12
12
  @config = config
13
13
  @failures = {}
14
14
  config.on_event :test_case_finished do |event|
@@ -5,7 +5,7 @@ module Cucumber
5
5
  # The formatter used for <tt>--format steps</tt>
6
6
  class Steps
7
7
  def initialize(runtime, path_or_io, options)
8
- @io = ensure_io(path_or_io)
8
+ @io = ensure_io(path_or_io, nil)
9
9
  @options = options
10
10
  @step_definition_files = collect_steps(runtime)
11
11
  end
@@ -16,7 +16,7 @@ module Cucumber
16
16
 
17
17
  def initialize(config)
18
18
  @config = config
19
- @io = ensure_io(config.out_stream)
19
+ @io = ensure_io(config.out_stream, config.error_stream)
20
20
  @ast_lookup = AstLookup.new(config)
21
21
  @counts = ConsoleCounts.new(@config)
22
22
  @issues = ConsoleIssues.new(@config, @ast_lookup)
@@ -1,30 +1,17 @@
1
- require 'cucumber/term/banner'
2
-
3
1
  module Cucumber
4
2
  module Formatter
5
3
  class URLReporter
6
- include Term::Banner
7
-
8
4
  def initialize(io)
9
5
  @io = io
10
6
  end
11
7
 
12
- def report(url)
13
- uri = URI(url)
14
- display_banner(
15
- [
16
- 'View your Cucumber Report at:',
17
- [["https://reports.cucumber.io#{uri.path}", :cyan, :bold, :underline]],
18
- '',
19
- [['This report will self-destruct in 24h unless it is claimed or deleted.', :green, :bold]]
20
- ],
21
- @io
22
- )
8
+ def report(banner)
9
+ @io.puts(banner)
23
10
  end
24
11
  end
25
12
 
26
13
  class NoReporter
27
- def report(url); end
14
+ def report(banner); end
28
15
  end
29
16
  end
30
17
  end
@@ -137,4 +137,4 @@ module Cucumber
137
137
  end
138
138
 
139
139
  # TODO: can we avoid adding methods to the global namespace (Kernel)
140
- extend(Cucumber::Glue::Dsl) # rubocop:disable Style/MixinUsage
140
+ extend(Cucumber::Glue::Dsl)
@@ -3,6 +3,7 @@
3
3
  require 'cucumber/gherkin/formatter/ansi_escapes'
4
4
  require 'cucumber/core/test/data_table'
5
5
  require 'cucumber/deprecate'
6
+ require 'mime/types'
6
7
 
7
8
  module Cucumber
8
9
  module Glue
@@ -72,47 +73,28 @@ module Cucumber
72
73
  MultilineArgument::DataTable.from(text_or_table)
73
74
  end
74
75
 
75
- # Print a message to the output.
76
- #
77
- # @note Cucumber might surprise you with the behaviour of this method. Instead
78
- # of sending the output directly to STDOUT, Cucumber will intercept and cache
79
- # the message until the current step has finished, and then display it.
80
- #
81
- # If you'd prefer to see the message immediately, call {Kernel.puts} instead.
82
- def puts(*messages)
83
- Cucumber.deprecate(
84
- 'Messages emitted with "puts" will no longer be caught by Cucumber ' \
85
- 'and sent to the formatter. If you want message to be in the formatted output, ' \
86
- "please use log(message) instead.\n" \
87
- 'If you simply want it in the console, '\
88
- 'keep using "puts" (or Kernel.puts to avoid this message)',
89
- 'puts(message)',
90
- '6.0.0'
91
- )
92
- messages.each { |message| log(message.to_s) }
93
- end
94
-
95
76
  # Pause the tests and ask the operator for input
96
77
  def ask(question, timeout_seconds = 60)
97
78
  super
98
79
  end
99
80
 
100
- # Embed an image in the output
101
- def embed(file, mime_type, _label = 'Screenshot')
102
- Cucumber.deprecate(
103
- 'Please use attach(file, media_type) instead',
104
- 'embed(file, mime_type, label)',
105
- '6.0.0'
106
- )
107
- attach(file, mime_type)
108
- end
109
-
110
81
  def log(*messages)
111
82
  messages.each { |message| attach(message.to_s.dup, 'text/x.cucumber.log+plain') }
112
83
  end
113
84
 
114
- def attach(file, media_type)
115
- super
85
+ # Attach a file to the output
86
+ # @param file [string|io] the file to attach.
87
+ # It can be a string containing the file content itself,
88
+ # the file path, or an IO ready to be read.
89
+ # @param media_type [string] the media type. If file is a valid path,
90
+ # media_type can be ommitted, it will then be inferred from the file name.
91
+ def attach(file, media_type = nil)
92
+ return super unless File.file?(file)
93
+
94
+ content = File.read(file, mode: 'rb')
95
+ media_type = MIME::Types.type_for(file).first if media_type.nil?
96
+
97
+ super(content, media_type.to_s)
116
98
  end
117
99
 
118
100
  # Mark the matched step as pending.