fastlane-plugin-xcresult_to_junit 0.3.2 → 0.4.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb3ca55a4312742fa6206f72196d9c82cb9044be5ec7eace2a119740291ac527
4
- data.tar.gz: a0d87f022674375715b8bd906369be0bb6e798531a5ead56902768a87c0dc65d
3
+ metadata.gz: a905df73bf4be24d949ec35d7e1b76f52b2264fe19279b8e5184ee30e277130b
4
+ data.tar.gz: b2fcd29e29935325fba9b246008a685849d51b1d81cf51f6c8797eab4f75d91e
5
5
  SHA512:
6
- metadata.gz: 8caaa365d18f8d36fed3c30159ac7393c9e95fe9a57b1dcfea89e878ab632a4bae16b4e28f1a414cc31adeb7de18455f64bbfde85f9aa090ede2650e928180d9
7
- data.tar.gz: f93a64d0ab5052064985d14b85e0e7751f020c93d31e3cddb1f21ea5083d4f11bd2e94b3209d6ba23ba938677875efd96a845478a330183d453e20b1c8aef9a2
6
+ metadata.gz: 9136fdc8f5d381444aee0ba6a314a3566acdd2a0cfa50364d4968e2cb19f576c4a9f25718027c7e5e297bb28d107ba17b6cd207b64f14df5a088a78b61a681a6
7
+ data.tar.gz: af066a12451e578d59f781bfe5fa43f9493a065ee0244e4b87a926e76318d3fee2b95202cccce8eee755efd2164f79e2dadb7084f91980c0fadb7adfd0aedcb4
data/README.md CHANGED
@@ -62,7 +62,7 @@ rake
62
62
  To automatically fix many of the styling issues, use
63
63
 
64
64
  ```bash
65
- rubocop -a
65
+ rubocop -A
66
66
  ```
67
67
 
68
68
  ## Issues and Feedback
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
4
  require 'fastlane/action'
3
5
  require_relative '../helper/xcresult_to_junit_helper'
@@ -6,120 +8,88 @@ module Fastlane
6
8
  module Actions
7
9
  class XcresultToJunitAction < Action
8
10
  def self.run(params)
9
- UI.message("The xcresult_to_junit plugin has started!")
10
- all_results = Helper::XcresultToJunitHelper.load_results(params[:xcresult_path])['actions']['_values']
11
- all_results.each do |test_run|
12
- next unless test_run['actionResult']['testsRef'] # Skip if section has no testRef data as this means its not a test run
13
- map = {}
14
- junit_folder = Helper::XcresultToJunitHelper.save_device_details_to_file(params[:output_path], test_run['runDestination'])
15
- test_run_id = test_run['actionResult']['testsRef']['id']['_value']
16
- all_tests = Helper::XcresultToJunitHelper.load_object(params[:xcresult_path], test_run_id)['summaries']['_values'][0]['testableSummaries']['_values']
17
- test_suites = []
18
- all_tests.each do |target|
19
- target_name = target['targetName']['_value']
20
- unless target['tests']
21
- failure_summary = target['failureSummaries']['_values'][0]
22
- test_suites << { name: target_name, error: failure_summary['message']['_value'] }
23
- next
24
- end
25
- test_classes = []
26
- if defined?(target['tests']['_values'][0]['subtests']['_values'][0]['subtests']['_values'])
27
- test_classes = target['tests']['_values'][0]['subtests']['_values'][0]['subtests']['_values']
28
- end
29
- test_classes.each do |test_class|
30
- suite_name = "#{target_name}.#{test_class['name']['_value']}"
31
- suite = { name: suite_name, cases: [] }
32
- if test_class['subtests']
33
- test_class['subtests']['_values'].each do |test|
34
- duration = 0
35
- duration = test['duration']['_value'] if test['duration']
36
- testcase_name = test['name']['_value'].tr('()', '')
37
- tags = testcase_name.split('_')[1..-1]
38
- testcase_name = testcase_name.split('_').first
39
- testcase = { name: testcase_name, time: duration }
40
- map["#{suite_name}.#{testcase_name}"] = { 'files' => [], 'tags' => tags }
11
+ UI.message('The xcresult_to_junit plugin has started!')
12
+ result = Helper::XcresultToJunitHelper.fetch_tests(params[:xcresult_path])
41
13
 
42
- if defined?(test['summaryRef']['id']['_value'])
43
- summary_ref = test['summaryRef']['id']['_value']
44
- ref = Helper::XcresultToJunitHelper.load_object(params[:xcresult_path], summary_ref)
45
- if defined?(ref['activitySummaries']['_values'])
46
- ref['activitySummaries']['_values'].each do |summary|
47
- next unless summary['attachments']
48
- summary['attachments']['_values'].each do |attachment|
49
- name = attachment['name']['_value']
50
- next unless name != 'kXCTAttachmentLegacyDiagnosticReportData'
51
- timestamp = DateTime.parse(attachment['timestamp']['_value']).to_time.to_i
52
- filename = attachment['filename']['_value']
53
- folder_name = "#{suite_name}.#{testcase_name}"
54
- id = attachment.dig('payloadRef', 'id', '_value')
55
- next if id.nil?
56
- Helper::XcresultToJunitHelper.fetch_screenshot(params[:xcresult_path], "#{junit_folder}/attachments/#{folder_name}", filename.to_s, id)
57
- map[folder_name]['files'].push({ 'description' => name, 'mime-type' => 'image/png', 'path' => "#{folder_name}/#{filename}", 'timestamp' => timestamp })
58
- end
59
- end
60
- end
14
+ devices = result['devices']
15
+ test_plans = result['testNodes']
16
+ map = {}
17
+ junit_folder = Helper::XcresultToJunitHelper.save_device_details_to_file(params[:output_path], devices)
18
+ test_suites = []
61
19
 
62
- if defined?(ref['performanceMetrics']['_value'])
63
- performancemetrics = ""
64
- ref['performanceMetrics']['_values'].each do |metric|
65
- metricname = metric['displayName']['_value']
66
- if defined?(metric['baselineAverage']['_value'])
67
- metricbaseline = metric['baselineAverage']['_value']
68
- else
69
- metricbaseline = 0
70
- end
71
- metricmaxdev = metric['maxPercentRelativeStandardDeviation']['_value']
72
- metricunit = metric['unitOfMeasurement']['_value']
73
- metricave = 0
74
- measurecount = 0
75
- metric['measurements']['_values'].each do |measure|
76
- metricave += measure['_value'].to_f
77
- measurecount += 1
78
- end
79
- metricave = (metricave / measurecount).round(2)
80
- if metricbaseline != 0
81
- metricresult = (((metricbaseline.to_f - metricave) / metricbaseline.to_f) * 100).round(2)
82
- else
83
- metricresult = 0
84
- end
85
- performancemetric = "\nMetric: #{metricname}\nResult: #{metricresult}%\nAverage: #{metricave}#{metricunit}\nBaseline: #{metricbaseline}#{metricunit}\nMax Deviation: #{metricmaxdev}%\n\n"
86
- performancemetrics << performancemetric
20
+ test_plans.each do |test_plan|
21
+ test_plan['children'].each do |test_bundle|
22
+ test_bundle['children'].each do |test_suite|
23
+ suite_name = test_suite['name']
24
+ count = 0
25
+ passed = 0
26
+ failed = 0
27
+ test_cases = []
28
+ test_suite['children'].each do |test_case|
29
+ duration = 0.0
30
+ duration = test_case['duration'].sub('s', '').to_f if test_case['duration']
31
+ full_testcase_name = test_case['name'].sub('()', '')
32
+ tags = full_testcase_name.split('_')[1..]
33
+ testcase = { name: full_testcase_name, time: duration }
34
+ count += 1
35
+ if test_case['result'] == 'Passed'
36
+ passed += 1
37
+ elsif test_case['result'] == 'Failed'
38
+ failed += 1
39
+ testcase[:failure] ||= []
40
+ testcase[:failure_location] ||= []
41
+ test_case['children'].each do |failure|
42
+ if failure['nodeType'] == 'Repetition'
43
+ failure['children'].each do |retry_failure|
44
+ testcase[:failure] << retry_failure['name']
45
+ testcase[:failure_location] << failure['name']
87
46
  end
88
- testcase[:performance] = performancemetrics
89
- end
90
- end
91
- if test['testStatus']['_value'] == 'Failure'
92
- failure = Helper::XcresultToJunitHelper.load_object(params[:xcresult_path], test['summaryRef']['id']['_value'])['failureSummaries']['_values'][0]
93
- filename = failure.dig('fileName', '_value')
94
- message = failure['message']['_value']
95
- if filename == '<unknown>' || filename.nil?
96
- testcase[:error] = message
97
- else
98
- testcase[:failure] = message
99
- testcase[:failure_location] = "#{filename}:#{failure['lineNumber']['_value']}"
47
+ elsif failure['nodeType'] == 'Failure Message'
48
+ testcase[:failure] << failure['name']
49
+ testcase[:failure_location] << failure['name'].split(': ')[0]
100
50
  end
101
51
  end
102
- suite[:cases] << testcase
103
52
  end
53
+ test_cases << testcase
54
+ map["#{suite_name}.#{full_testcase_name}"] = { 'files' => [], 'tags' => tags }
104
55
  end
105
- suite[:count] = suite[:cases].size
106
- suite[:failures] = suite[:cases].count { |testcase| testcase[:failure] }
107
- suite[:errors] = suite[:cases].count { |testcase| testcase[:error] }
56
+ suite = { name: suite_name.to_s, count: count, failures: failed, errors: 0, cases: test_cases }
108
57
  test_suites << suite
109
58
  end
110
59
  end
111
- Helper::XcresultToJunitHelper.generate_junit(junit_folder, test_suites)
112
- Helper::XcresultToJunitHelper.save_screenshot_mapping(map, "#{junit_folder}/attachments/")
113
60
  end
114
- UI.message("The xcresult_to_junit plugin has finished!")
61
+
62
+ Helper::XcresultToJunitHelper.generate_junit(junit_folder, test_suites)
63
+ attachments_folder = "#{junit_folder}/attachments"
64
+ Helper::XcresultToJunitHelper.save_attachments(params[:xcresult_path], attachments_folder)
65
+
66
+ test_attachments = Helper::XcresultToJunitHelper.fetch_attachment_manifest(attachments_folder)
67
+
68
+ test_attachments.each do |test_attachment|
69
+ test_identifier = test_attachment['testIdentifier']
70
+ folder_name = test_identifier.sub('()', '').sub('/', '.')
71
+
72
+ test_attachment['attachments'].reverse().each do |attachment|
73
+ name = attachment['suggestedHumanReadableName']
74
+ filename = attachment['exportedFileName']
75
+ mime_type = 'image/png'
76
+ mime_type = 'text/plain' if filename.end_with?('.txt')
77
+ timestamp = attachment['timestamp']
78
+ map[folder_name]['files'].push({ 'description' => name, 'mime-type' => mime_type, 'path' => filename,
79
+ 'timestamp' => timestamp })
80
+ end
81
+ end
82
+
83
+ Helper::XcresultToJunitHelper.save_screenshot_mapping(map, attachments_folder)
84
+ UI.message('The xcresult_to_junit plugin has finished!')
115
85
  end
116
86
 
117
87
  def self.description
118
- "Produces junit xml files from Xcode 11+ xcresult files"
88
+ 'Produces junit xml files from Xcode 11+ xcresult files'
119
89
  end
120
90
 
121
91
  def self.authors
122
- ["Shane Birdsall"]
92
+ ['Shane Birdsall']
123
93
  end
124
94
 
125
95
  def self.return_value
@@ -127,26 +97,26 @@ module Fastlane
127
97
  end
128
98
 
129
99
  def self.details
130
- "By using the xcresulttool this plugin parses xcresult files and generates junit reports to be used with other tools to display iOS test results"
100
+ 'By using the xcresulttool this plugin parses xcresult files and generates junit reports to be used with other tools to display iOS test results'
131
101
  end
132
102
 
133
103
  def self.available_options
134
104
  [
135
105
  FastlaneCore::ConfigItem.new(key: :xcresult_path,
136
- env_name: "XCRESULT_TO_JUNIT_XCRESULT_PATH",
137
- description: "The path to the xcresult file",
138
- optional: false,
139
- type: String),
106
+ env_name: 'XCRESULT_TO_JUNIT_XCRESULT_PATH',
107
+ description: 'The path to the xcresult file',
108
+ optional: false,
109
+ type: String),
140
110
  FastlaneCore::ConfigItem.new(key: :output_path,
141
- env_name: "XCRESULT_TO_JUNIT_OUTPUT_PATH",
142
- description: "The path where the output will be placed",
143
- optional: false,
144
- type: String)
111
+ env_name: 'XCRESULT_TO_JUNIT_OUTPUT_PATH',
112
+ description: 'The path where the output will be placed',
113
+ optional: false,
114
+ type: String)
145
115
  ]
146
116
  end
147
117
 
148
118
  def self.is_supported?(platform)
149
- [:ios, :mac].include?(platform)
119
+ %i[ios mac].include?(platform)
150
120
  end
151
121
  end
152
122
  end
@@ -1,25 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'fastlane_core/ui/ui'
2
4
  require 'json'
3
5
  require 'fileutils'
4
6
 
5
7
  module Fastlane
6
- UI = FastlaneCore::UI unless Fastlane.const_defined?("UI")
8
+ UI = FastlaneCore::UI unless Fastlane.const_defined?('UI')
7
9
 
8
10
  module Helper
9
11
  class XcresultToJunitHelper
10
- def self.load_object(xcresult_path, id)
11
- JSON.parse(FastlaneCore::CommandExecutor.execute(command: "xcrun xcresulttool get --legacy --format json --path #{xcresult_path} --id #{id}"))
12
+ def self.fetch_tests(xcresult_path)
13
+ JSON.parse(FastlaneCore::CommandExecutor.execute(command: "xcrun xcresulttool get test-results tests --path #{xcresult_path}"))
12
14
  end
13
15
 
14
- def self.load_results(xcresult_path)
15
- JSON.parse(FastlaneCore::CommandExecutor.execute(command: "xcrun xcresulttool get --legacy --format json --path #{xcresult_path}"))
16
+ def self.save_attachments(xcresult_path, output_path)
17
+ FileUtils.mkdir(output_path) unless File.directory?(output_path)
18
+ FastlaneCore::CommandExecutor.execute(command: "xcrun xcresulttool export attachments --path #{xcresult_path} --output-path #{output_path}")
16
19
  end
17
20
 
18
- def self.fetch_screenshot(xcresult_path, output_path, file_name, id)
19
- unless File.directory?(output_path)
20
- FileUtils.mkdir(output_path)
21
- end
22
- FastlaneCore::CommandExecutor.execute(command: "xcrun xcresulttool export --legacy --path #{xcresult_path} --output-path \"#{output_path}/#{file_name}\" --id #{id} --type file")
21
+ def self.fetch_attachment_manifest(attachments_folder)
22
+ JSON.parse(FastlaneCore::CommandExecutor.execute(command: "cat #{attachments_folder}/manifest.json"))
23
23
  end
24
24
 
25
25
  def self.save_screenshot_mapping(map_hash, output_path)
@@ -28,21 +28,24 @@ module Fastlane
28
28
  end
29
29
  end
30
30
 
31
- def self.save_device_details_to_file(output_path, device_destination)
32
- device_udid = device_destination['targetDeviceRecord']['identifier']['_value']
31
+ def self.save_device_details_to_file(output_path, devices)
32
+ device = devices[0]
33
33
  device_details = {
34
- 'udid' => device_udid,
35
- 'name' => device_destination['targetDeviceRecord']['modelName']['_value'],
36
- 'os' => device_destination['targetDeviceRecord']['operatingSystemVersion']['_value']
34
+ 'architecture' => device['architecture'],
35
+ 'udid' => device['deviceId'],
36
+ 'name' => device['deviceName'],
37
+ 'model' => device['modelName'],
38
+ 'os' => device['osVersion'],
39
+ 'platform' => device['platform']
37
40
  }.to_json
38
41
 
39
- junit_folder = "#{output_path}/ios-#{device_udid}.junit"
42
+ junit_folder = "#{output_path}/ios-#{device['deviceId']}.junit"
40
43
  FileUtils.rm_rf(junit_folder)
41
44
  FileUtils.mkdir_p("#{junit_folder}/attachments")
42
45
  File.open("#{junit_folder}/device.json", 'w') do |f|
43
46
  f << device_details
44
47
  end
45
- return junit_folder
48
+ junit_folder
46
49
  end
47
50
 
48
51
  def self.junit_file_start
@@ -76,7 +79,14 @@ module Fastlane
76
79
  end
77
80
 
78
81
  def self.junit_testcase_failure(testcase)
79
- puts("<failure message=#{testcase[:failure].encode(xml: :attr)}>#{testcase[:failure_location].encode(xml: :text)}</failure>")
82
+ if testcase[:failure].length != testcase[:failure_location].length
83
+ raise "Mismatch in lengths: testcase[:failure] and testcase[:failure_location] must have the same length."
84
+ end
85
+ for index in 0...testcase[:failure].length
86
+ failure = testcase[:failure][index]
87
+ failure_location = testcase[:failure_location][index]
88
+ puts("<failure message=#{failure.encode(xml: :attr)}>#{failure_location.encode(xml: :text)}</failure>")
89
+ end
80
90
  end
81
91
 
82
92
  def self.junit_testcase_error(testcase)
@@ -104,9 +114,7 @@ module Fastlane
104
114
  elsif testcase[:error]
105
115
  Helper::XcresultToJunitHelper.junit_testcase_error(testcase)
106
116
  end
107
- if testcase[:performance]
108
- Helper::XcresultToJunitHelper.junit_testcase_performance(testcase)
109
- end
117
+ Helper::XcresultToJunitHelper.junit_testcase_performance(testcase) if testcase[:performance]
110
118
  Helper::XcresultToJunitHelper.junit_testcase_end
111
119
  end
112
120
  end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fastlane
2
4
  module XcresultToJunit
3
- VERSION = "0.3.2"
5
+ VERSION = '0.4.2'
4
6
  end
5
7
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'fastlane/plugin/xcresult_to_junit/version'
2
4
 
3
5
  module Fastlane
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-xcresult_to_junit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shane Birdsall
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-17 00:00:00.000000000 Z
11
+ date: 2024-12-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: pry
14
+ name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
@@ -25,7 +25,7 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: bundler
28
+ name: pry
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
@@ -39,7 +39,7 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: rspec
42
+ name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
@@ -53,7 +53,7 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: rspec_junit_formatter
56
+ name: rspec
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
@@ -67,7 +67,7 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: rake
70
+ name: rspec_junit_formatter
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="