pdk 0.2.0 → 0.3.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/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