pdk 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/pdk/report.rb CHANGED
@@ -12,7 +12,7 @@ module PDK
12
12
 
13
13
  # @return [Symbol] the method name of the default report format.
14
14
  def self.default_format
15
- :to_text
15
+ :write_text
16
16
  end
17
17
 
18
18
  # @return [#write] the default target to write the report to.
@@ -47,7 +47,13 @@ module PDK
47
47
  #
48
48
  # @param target [#write] an IO object that the report will be written to.
49
49
  # Defaults to PDK::Report.default_target.
50
- def to_junit(target = self.class.default_target)
50
+ def write_junit(target = self.class.default_target)
51
+ # Extra defaulting here, b/c the Class.send method will pass in nil
52
+ target ||= self.class.default_target
53
+
54
+ # Open a File Object for IO if target is a string containing a filename or path
55
+ target = File.open(target, 'w') if target.is_a? String
56
+
51
57
  document = REXML::Document.new
52
58
  document << REXML::XMLDecl.new
53
59
  testsuites = REXML::Element.new('testsuites')
@@ -59,6 +65,7 @@ module PDK
59
65
  testsuite.attributes['tests'] = testcases.length
60
66
  testsuite.attributes['errors'] = testcases.select(&:error?).length
61
67
  testsuite.attributes['failures'] = testcases.select(&:failure?).length
68
+ testsuite.attributes['skipped'] = testcases.select(&:skipped?).length
62
69
  testsuite.attributes['time'] = 0
63
70
  testsuite.attributes['timestamp'] = Time.now.strftime('%Y-%m-%dT%H:%M:%S')
64
71
  testsuite.attributes['hostname'] = Socket.gethostname
@@ -75,6 +82,8 @@ module PDK
75
82
 
76
83
  document.elements << testsuites
77
84
  document.write(target, 2)
85
+ ensure
86
+ target.close if target.is_a? File
78
87
  end
79
88
 
80
89
  # Renders the report as plain text.
@@ -84,12 +93,20 @@ module PDK
84
93
  #
85
94
  # @param target [#write] an IO object that the report will be written to.
86
95
  # Defaults to PDK::Report.default_target.
87
- def to_text(target = self.class.default_target)
96
+ def write_text(target = self.class.default_target)
97
+ # Extra defaulting here, b/c the Class.send method will pass in nil
98
+ target ||= self.class.default_target
99
+
100
+ # Open a File Object for IO if target is a string containing a filename or path
101
+ target = File.open(target, 'w') if target.is_a? String
102
+
88
103
  events.each do |_tool, tool_events|
89
104
  tool_events.each do |event|
90
105
  target.puts(event.to_text) unless event.pass?
91
106
  end
92
107
  end
108
+ ensure
109
+ target.close if target.is_a? File
93
110
  end
94
111
  end
95
112
  end
@@ -35,6 +35,9 @@ module PDK
35
35
  # :skipped.
36
36
  attr_reader :state
37
37
 
38
+ # @return [Array] Array of full stack trace lines associated with event
39
+ attr_reader :trace
40
+
38
41
  # Initailises a new PDK::Report::Event object.
39
42
  #
40
43
  # @param data [Hash{Symbol=>Object}
@@ -46,6 +49,7 @@ module PDK
46
49
  # @option data [String] :severity (see #severity)
47
50
  # @option data [String] :test (see #test)
48
51
  # @option data [Symbol] :state (see #state)
52
+ # @option data [Array] :trace (see #trace)
49
53
  #
50
54
  # @raise [ArgumentError] (see #sanitise_data)
51
55
  def initialize(data)
@@ -77,6 +81,7 @@ module PDK
77
81
  end
78
82
 
79
83
  # Checks if the event is the result of test that was not run.
84
+ # This includes pending tests (that are run but have an expected failure result).
80
85
  #
81
86
  # @return [Boolean] true if the test was skipped, otherwise false.
82
87
  def skipped?
@@ -88,7 +93,9 @@ module PDK
88
93
  # @return [String] The rendered event.
89
94
  def to_text
90
95
  location = [file, line, column].compact.join(':')
96
+ location = nil if location.empty?
91
97
 
98
+ # TODO: maybe add trace
92
99
  [location, severity, message].compact.join(': ')
93
100
  end
94
101
 
@@ -271,6 +278,28 @@ module PDK
271
278
 
272
279
  value.to_i
273
280
  end
281
+
282
+ # Cleans up provided stack trace by removing entries that are inside gems
283
+ # or the rspec binstub.
284
+ #
285
+ # @param value [Array] Array of stack trace lines
286
+ #
287
+ # @return [Array] Array of stack trace lines with less relevant lines excluded
288
+ def sanitise_trace(value)
289
+ return nil if value.nil?
290
+
291
+ valid_types = [Array]
292
+
293
+ unless valid_types.include?(value.class)
294
+ raise ArgumentError, _('trace must be an Array of stack trace lines')
295
+ end
296
+
297
+ # Drop any stacktrace lines that include '/gems/' in the path or
298
+ # are the original rspec binstub lines
299
+ value.reject do |line|
300
+ (line =~ %r{/gems/}) || (line =~ %r{bin/rspec:})
301
+ end
302
+ end
274
303
  end
275
304
  end
276
305
  end
@@ -1,25 +1,130 @@
1
1
  require 'pdk'
2
2
  require 'pdk/cli/exec'
3
3
  require 'pdk/util/bundler'
4
+ require 'json'
4
5
 
5
6
  module PDK
6
7
  module Test
7
8
  class Unit
8
9
  def self.cmd(_tests)
9
- # TODO: actually run the tests
10
- # cmd = 'rake spec'
11
- # cmd += " #{tests}" if tests
12
- cmd = 'pwd'
13
- cmd
10
+ # TODO: test selection
11
+ [File.join(PDK::Util.module_root, 'bin', 'rake'), 'spec']
14
12
  end
15
13
 
16
- def self.invoke(tests, report = nil)
14
+ def self.invoke(report, options = {})
17
15
  PDK::Util::Bundler.ensure_bundle!
16
+ PDK::Util::Bundler.ensure_binstubs!('rake')
18
17
 
19
- puts _('Running unit tests: %{tests}') % { tests: tests }
18
+ tests = options.fetch(:tests)
20
19
 
21
- output = PDK::CLI::Exec.execute(cmd(tests))
22
- report.write(output) if report
20
+ cmd_argv = cmd(tests)
21
+ cmd_argv.unshift('ruby') if Gem.win_platform?
22
+
23
+ command = PDK::CLI::Exec::Command.new(*cmd_argv).tap do |c|
24
+ c.context = :module
25
+ c.add_spinner('Running unit tests')
26
+ c.environment['CI_SPEC_OPTIONS'] = '--format j'
27
+ end
28
+
29
+ PDK.logger.debug(_('Running %{cmd}') % { cmd: command.argv.join(' ') })
30
+
31
+ result = command.execute!
32
+
33
+ # TODO: cleanup rspec and/or beaker output
34
+ # Iterate through possible JSON documents until we find one that is valid.
35
+ json_result = nil
36
+
37
+ result[:stdout].scan(%r{\{(?:[^{}]|(?:\g<0>))*\}}x) do |str|
38
+ begin
39
+ json_result = JSON.parse(str)
40
+ break
41
+ rescue JSON::ParserError
42
+ next
43
+ end
44
+ end
45
+
46
+ raise PDK::CLI::FatalError, _('Unit test output did not contain a valid JSON result: %{output}') % { output: result[:stdout] } unless json_result
47
+
48
+ parse_output(report, json_result)
49
+
50
+ result[:exit_code]
51
+ end
52
+
53
+ def self.parse_output(report, json_data)
54
+ # Output messages to stderr.
55
+ json_data['messages'] && json_data['messages'].each { |msg| $stderr.puts msg }
56
+
57
+ example_results = {
58
+ # Only possibilities are passed, failed, pending:
59
+ # https://github.com/rspec/rspec-core/blob/master/lib/rspec/core/example.rb#L548
60
+ 'passed' => [],
61
+ 'failed' => [],
62
+ 'pending' => [],
63
+ }
64
+
65
+ json_data['examples'] && json_data['examples'].each do |ex|
66
+ example_results[ex['status']] << ex if example_results.key?(ex['status'])
67
+ end
68
+
69
+ example_results.each do |result, examples|
70
+ # Translate rspec example results to JUnit XML testcase results
71
+ state = case result
72
+ when 'passed' then :passed
73
+ when 'failed' then :failure
74
+ when 'pending' then :skipped
75
+ end
76
+
77
+ examples.each do |ex|
78
+ report.add_event(
79
+ source: 'rspec',
80
+ state: state,
81
+ file: ex['file_path'],
82
+ line: ex['line_number'],
83
+ test: ex['full_description'],
84
+ severity: ex['status'],
85
+ message: ex['pending_message'] || (ex['exception'] && ex['exception']['message']) || nil,
86
+ trace: (ex['exception'] && ex['exception']['backtrace']) || nil,
87
+ )
88
+ end
89
+ end
90
+
91
+ return unless json_data['summary']
92
+
93
+ # TODO: standardize summary output
94
+ $stderr.puts ' ' << _('Evaluated %{total} tests in %{duration} seconds: %{failures} failures, %{pending} pending') % {
95
+ total: json_data['summary']['example_count'],
96
+ duration: json_data['summary']['duration'],
97
+ failures: json_data['summary']['failure_count'],
98
+ pending: json_data['summary']['pending_count'],
99
+ }
100
+ end
101
+
102
+ # @return array of { :id, :full_description }
103
+ def self.list
104
+ PDK::Util::Bundler.ensure_bundle!
105
+ PDK::Util::Bundler.ensure_binstubs!('rspec-core')
106
+
107
+ command_argv = [File.join(PDK::Util.module_root, 'bin', 'rspec'), '--dry-run', '--format', 'json']
108
+ command_argv.unshift('ruby') if Gem.win_platform?
109
+ list_command = PDK::CLI::Exec::Command.new(*command_argv)
110
+ list_command.context = :module
111
+ output = list_command.execute!
112
+
113
+ rspec_json_output = JSON.parse(output[:stdout])
114
+ if rspec_json_output['examples'].empty?
115
+ rspec_message = rspec_json_output['messages'][0]
116
+ return [] if rspec_message == 'No examples found.'
117
+
118
+ raise PDK::CLI::FatalError, _('Unable to enumerate examples. rspec reported: %{message}' % { message: rspec_message })
119
+ else
120
+ examples = []
121
+ rspec_json_output['examples'].each do |example|
122
+ examples << { id: example['id'], full_description: example['full_description'] }
123
+ end
124
+ examples
125
+ end
126
+ rescue JSON::ParserError => e
127
+ raise PDK::CLI::FatalError, _('Failed to parse output from rspec: %{message}' % { message: e.message })
23
128
  end
24
129
  end
25
130
  end
@@ -62,24 +62,28 @@ module PDK
62
62
  end
63
63
 
64
64
  def installed?
65
- output_start(_('Checking for missing Gemfile dependencies'))
65
+ command = bundle_command('check', "--gemfile=#{gemfile}", "--path=#{bundle_cachedir}").tap do |c|
66
+ c.add_spinner(_('Checking for missing Gemfile dependencies'))
67
+ end
66
68
 
67
- result = invoke('check', "--gemfile=#{gemfile}", "--path=#{bundle_cachedir}")
69
+ result = command.execute!
68
70
 
69
- output_end(:success)
71
+ unless result[:exit_code].zero?
72
+ $stderr.puts result[:stdout]
73
+ $stderr.puts result[:stderr]
74
+ end
70
75
 
71
76
  result[:exit_code].zero?
72
77
  end
73
78
 
74
79
  def lock!
75
- output_start(_('Resolving Gemfile dependencies'))
80
+ command = bundle_command('lock').tap do |c|
81
+ c.add_spinner(_('Resolving Gemfile dependencies'))
82
+ end
76
83
 
77
- result = invoke('lock')
84
+ result = command.execute!
78
85
 
79
- if result[:exit_code].zero?
80
- output_end(:success)
81
- else
82
- output_end(:failure)
86
+ unless result[:exit_code].zero?
83
87
  $stderr.puts result[:stdout]
84
88
  $stderr.puts result[:stderr]
85
89
  end
@@ -88,14 +92,13 @@ module PDK
88
92
  end
89
93
 
90
94
  def install!
91
- output_start(_('Installing missing Gemfile dependencies'))
95
+ command = bundle_command('install', "--gemfile=#{gemfile}", "--path=#{bundle_cachedir}").tap do |c|
96
+ c.add_spinner(_('Installing missing Gemfile dependencies'))
97
+ end
92
98
 
93
- result = invoke('install', "--gemfile=#{gemfile}", "--path=#{bundle_cachedir}")
99
+ result = command.execute!
94
100
 
95
- if result[:exit_code].zero?
96
- output_end(:success)
97
- else
98
- output_end(:failure)
101
+ unless result[:exit_code].zero?
99
102
  $stderr.puts result[:stdout]
100
103
  $stderr.puts result[:stderr]
101
104
  end
@@ -104,10 +107,12 @@ module PDK
104
107
  end
105
108
 
106
109
  def binstubs!(gems)
107
- # FIXME: wrap in progress indicator
108
- result = invoke('binstubs', gems.join(' '), '--force')
110
+ command = bundle_command('binstubs', gems.join(' '), '--force')
111
+
112
+ result = command.execute!
109
113
 
110
114
  unless result[:exit_code].zero?
115
+ PDK.logger.error(_('Failed to generate binstubs for %{gems}') % { gems: gems.join(' ') })
111
116
  $stderr.puts result[:stdout]
112
117
  $stderr.puts result[:stderr]
113
118
  end
@@ -121,13 +126,10 @@ module PDK
121
126
 
122
127
  private
123
128
 
124
- def invoke(*args)
125
- bundle_bin = PDK::CLI::Exec.bundle_bin
126
- command = PDK::CLI::Exec::Command.new(bundle_bin, *args).tap do |c|
129
+ def bundle_command(*args)
130
+ PDK::CLI::Exec::Command.new(PDK::CLI::Exec.bundle_bin, *args).tap do |c|
127
131
  c.context = :module
128
132
  end
129
-
130
- command.execute!
131
133
  end
132
134
 
133
135
  def gemfile_lock
@@ -137,30 +139,6 @@ module PDK
137
139
  def bundle_cachedir
138
140
  @bundle_cachedir ||= File.join(PDK::Util.cachedir, 'bundler')
139
141
  end
140
-
141
- # These two output_* methods are just a way to not try to do the spinner stuff on Windows for now.
142
- def output_start(message)
143
- if Gem.win_platform?
144
- $stderr.print "#{message}... "
145
- else
146
- @spinner = TTY::Spinner.new("[:spinner] #{message}")
147
- @spinner.auto_spin
148
- end
149
- end
150
-
151
- def output_end(state)
152
- if Gem.win_platform?
153
- $stderr.print((state == :success) ? _("done.\n") : _("FAILURE!\n"))
154
- else
155
- if state == :success
156
- @spinner.success
157
- else
158
- @spinner.error
159
- end
160
-
161
- remove_instance_variable(:@spinner)
162
- end
163
- end
164
142
  end
165
143
  end
166
144
  end
@@ -20,8 +20,7 @@ module PDK
20
20
  targets.map { |target|
21
21
  if respond_to?(:pattern)
22
22
  if File.directory?(target)
23
- files_glob = Array[pattern].flatten.map { |p| Dir.glob(File.join(target, p)) }
24
- files_glob.flatten.empty? ? target : files_glob
23
+ Array[pattern].flatten.map { |p| Dir.glob(File.join(target, p)) }
25
24
  else
26
25
  target
27
26
  end
@@ -40,13 +39,13 @@ module PDK
40
39
  end
41
40
 
42
41
  def self.invoke(report, options = {})
43
- PDK::Util::Bundler.ensure_binstubs!(cmd)
44
-
45
42
  targets = parse_targets(options)
46
- cmd_argv = parse_options(options, targets).unshift(cmd_path)
47
- cmd_argv.unshift('ruby') if Gem.win_platform?
48
43
 
49
- PDK.logger.debug(_('Running %{cmd}') % { cmd: cmd_argv.join(' ') })
44
+ return 0 if targets.empty?
45
+
46
+ PDK::Util::Bundler.ensure_binstubs!(cmd)
47
+ cmd_argv = parse_options(options, targets).unshift(cmd_path)
48
+ cmd_argv.unshift('ruby', '-W0') if Gem.win_platform?
50
49
 
51
50
  command = PDK::CLI::Exec::Command.new(*cmd_argv).tap do |c|
52
51
  c.context = :module
@@ -55,13 +54,7 @@ module PDK
55
54
 
56
55
  result = command.execute!
57
56
 
58
- begin
59
- json_data = JSON.parse(result[:stdout])
60
- rescue JSON::ParserError
61
- json_data = []
62
- end
63
-
64
- parse_output(report, json_data)
57
+ parse_output(report, result, targets)
65
58
 
66
59
  result[:exit_code]
67
60
  end
@@ -14,6 +14,10 @@ module PDK
14
14
  'metadata-json-lint'
15
15
  end
16
16
 
17
+ def self.spinner_text
18
+ _('Checking metadata.json')
19
+ end
20
+
17
21
  def self.parse_targets(_options)
18
22
  [File.join(PDK::Util.module_root, 'metadata.json')]
19
23
  end
@@ -24,20 +28,38 @@ module PDK
24
28
  cmd_options.concat(targets)
25
29
  end
26
30
 
27
- def self.parse_output(report, json_data)
28
- return if json_data.empty?
29
-
30
- json_data.delete('result')
31
- json_data.keys.each do |type|
32
- json_data[type].each do |offense|
33
- report.add_event(
34
- file: 'metadata.json',
35
- source: cmd,
36
- message: offense['msg'],
37
- test: offense['check'],
38
- severity: type,
39
- state: :failure,
40
- )
31
+ def self.parse_output(report, result, _targets)
32
+ begin
33
+ json_data = JSON.parse(result[:stdout])
34
+ rescue JSON::ParserError
35
+ json_data = []
36
+ end
37
+
38
+ if json_data.empty?
39
+ report.add_event(
40
+ file: 'metadata.json',
41
+ source: cmd,
42
+ state: :passed,
43
+ severity: :ok,
44
+ )
45
+ else
46
+ json_data.delete('result')
47
+ json_data.keys.each do |type|
48
+ json_data[type].each do |offense|
49
+ # metadata-json-lint groups the offenses by type, so the type ends
50
+ # up being `warnings` or `errors`. We want to convert that to the
51
+ # singular noun for the event.
52
+ event_type = type[%r{\A(.+?)s?\Z}, 1]
53
+
54
+ report.add_event(
55
+ file: 'metadata.json',
56
+ source: cmd,
57
+ message: offense['msg'],
58
+ test: offense['check'],
59
+ severity: event_type,
60
+ state: :failure,
61
+ )
62
+ end
41
63
  end
42
64
  end
43
65
  end