cucumber 5.1.1 → 6.0.0

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