xcode-result-bundle-processor 1.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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +23 -0
- data/.csslintrc +34 -0
- data/.eslintrc +253 -0
- data/.gitignore +6 -0
- data/.rspec +2 -0
- data/.rubocop.yml +1171 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +83 -0
- data/LICENSE +36 -0
- data/README.md +72 -0
- data/Rakefile +50 -0
- data/bin/xcode-result-bundle-processor +37 -0
- data/lib/xcoderesultbundleprocessor.rb +28 -0
- data/lib/xcoderesultbundleprocessor/activity_log_formatter.rb +20 -0
- data/lib/xcoderesultbundleprocessor/element_snapshot.rb +81 -0
- data/lib/xcoderesultbundleprocessor/indented_string_buffer.rb +33 -0
- data/lib/xcoderesultbundleprocessor/keyword_struct.rb +10 -0
- data/lib/xcoderesultbundleprocessor/log_deserializer.rb +26 -0
- data/lib/xcoderesultbundleprocessor/results_bundle.rb +45 -0
- data/lib/xcoderesultbundleprocessor/slf0/class_name_resolver.rb +26 -0
- data/lib/xcoderesultbundleprocessor/slf0/deserializer.rb +45 -0
- data/lib/xcoderesultbundleprocessor/slf0/model/dvtdocumentlocation.rb +16 -0
- data/lib/xcoderesultbundleprocessor/slf0/model/dvttextdocumentlocation.rb +23 -0
- data/lib/xcoderesultbundleprocessor/slf0/model/ideactivitylogmessage.rb +26 -0
- data/lib/xcoderesultbundleprocessor/slf0/model/ideactivitylogsection.rb +29 -0
- data/lib/xcoderesultbundleprocessor/slf0/model/ideactivitylogunittestsection.rb +24 -0
- data/lib/xcoderesultbundleprocessor/slf0/model/ideconsoleitem.rb +16 -0
- data/lib/xcoderesultbundleprocessor/slf0/tokenizer.rb +82 -0
- data/lib/xcoderesultbundleprocessor/snapshot_summary.rb +47 -0
- data/lib/xcoderesultbundleprocessor/test_summaries/html_report.rb +156 -0
- data/lib/xcoderesultbundleprocessor/test_summaries/test_summaries.rb +84 -0
- data/lib/xcoderesultbundleprocessor/test_summaries/text_report.rb +40 -0
- data/lib/xcoderesultbundleprocessor/version.rb +3 -0
- data/static/report.css +30 -0
- data/static/report.js +14 -0
- data/xcode-result-bundle-processor.gemspec +30 -0
- metadata +225 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
module XcodeResultBundleProcessor
|
2
|
+
module LogDeserializer
|
3
|
+
include Methadone::CLILogging
|
4
|
+
|
5
|
+
def self.deserialize_action_logs(results_bundle)
|
6
|
+
|
7
|
+
plist = results_bundle.read_plist('Info.plist')
|
8
|
+
|
9
|
+
action = plist['Actions'].first
|
10
|
+
log_pathname = action['ActionResult']['LogPath']
|
11
|
+
results_bundle.open_file(log_pathname) do |activity_log_io|
|
12
|
+
io = Zlib::GzipReader.new(activity_log_io)
|
13
|
+
tokens = SLF0::Tokenizer.read_token_stream(io)
|
14
|
+
tokens = SLF0::ClassNameResolver.resolve_class_names(tokens).to_a
|
15
|
+
|
16
|
+
# SLF0 files have a random int at the beginning; don't know its significance
|
17
|
+
tokens.shift
|
18
|
+
|
19
|
+
section = SLF0::Deserializer.deserialize(tokens)
|
20
|
+
debug section.ai
|
21
|
+
|
22
|
+
ActivityLogFormatter.format(section.subsections.first)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module XcodeResultBundleProcessor
|
2
|
+
class DirectoryResultsBundle
|
3
|
+
def initialize(path)
|
4
|
+
@path = Pathname.new(path)
|
5
|
+
end
|
6
|
+
|
7
|
+
def read_plist(path)
|
8
|
+
CFPropertyList.native_types(CFPropertyList::List.new(file: @path.join(path)).value)
|
9
|
+
end
|
10
|
+
|
11
|
+
def open_file(path, &block)
|
12
|
+
File.open(@path.join(path), &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def copy_file(source, destination)
|
16
|
+
FileUtils.copy(@path.join(source), destination)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class TarballResultsBundle
|
21
|
+
def initialize(path)
|
22
|
+
file = File.new(path)
|
23
|
+
zip = Zlib::GzipReader.new(file)
|
24
|
+
@tar = Gem::Package::TarReader.new(zip)
|
25
|
+
end
|
26
|
+
|
27
|
+
def read_plist(path)
|
28
|
+
@tar.seek("./#{path}") do |plist_entry|
|
29
|
+
CFPropertyList.native_types(CFPropertyList::List.new(data: plist_entry.read).value)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def open_file(path, &block)
|
34
|
+
@tar.seek("./#{path}", &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def copy_file(source, destination)
|
38
|
+
@tar.seek("./#{source}") do |source_entry|
|
39
|
+
File.open(destination, 'w') do |destination_file|
|
40
|
+
destination_file.write(source_entry.read)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module XcodeResultBundleProcessor
|
2
|
+
module SLF0
|
3
|
+
module ClassNameResolver
|
4
|
+
|
5
|
+
ResolvedClassName = Struct.new(:class_name)
|
6
|
+
|
7
|
+
def self.resolve_class_names(tokens)
|
8
|
+
class_names = []
|
9
|
+
|
10
|
+
Enumerator.new do |enumerator|
|
11
|
+
tokens.each do |token|
|
12
|
+
if token.is_a?(Tokenizer::ClassName)
|
13
|
+
class_names << token.class_name
|
14
|
+
elsif token.is_a?(Tokenizer::ClassNameRef)
|
15
|
+
raise "Invalid ClassNameRef to class index #{token.class_name_id}" if token.class_name_id > class_names.length
|
16
|
+
class_name = class_names[token.class_name_id - 1]
|
17
|
+
enumerator.yield(ResolvedClassName.new(class_name))
|
18
|
+
else
|
19
|
+
enumerator.yield(token)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module XcodeResultBundleProcessor
|
2
|
+
module SLF0
|
3
|
+
module Deserializer
|
4
|
+
include Methadone::CLILogging
|
5
|
+
|
6
|
+
def self.deserialize(tokens)
|
7
|
+
debug "Deserializing #{tokens.ai}"
|
8
|
+
|
9
|
+
if tokens.first.nil?
|
10
|
+
tokens.shift
|
11
|
+
return nil
|
12
|
+
end
|
13
|
+
|
14
|
+
self._assert_first_token_type(ClassNameResolver::ResolvedClassName, tokens)
|
15
|
+
|
16
|
+
resolved_class = tokens.shift
|
17
|
+
return nil if resolved_class.nil?
|
18
|
+
|
19
|
+
class_name = resolved_class.class_name
|
20
|
+
raise "Unsupported class #{class_name}" unless Model.const_defined?(class_name)
|
21
|
+
|
22
|
+
Model.const_get(class_name).deserialize(tokens)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.deserialize_list(tokens)
|
26
|
+
if tokens.first.nil?
|
27
|
+
tokens.shift
|
28
|
+
return []
|
29
|
+
end
|
30
|
+
|
31
|
+
self._assert_first_token_type(Tokenizer::ObjectList, tokens)
|
32
|
+
|
33
|
+
object_list_info = tokens.shift
|
34
|
+
|
35
|
+
object_list_info.mystery_number.times.map { Deserializer.deserialize(tokens) }
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def self._assert_first_token_type(expected_type, tokens)
|
41
|
+
raise "First token should be #{expected_type} but was a <#{tokens.first}>" unless tokens.first.is_a?(expected_type)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module XcodeResultBundleProcessor
|
2
|
+
module SLF0
|
3
|
+
module Model
|
4
|
+
class DVTDocumentLocation < KeywordStruct.new(:document_url_string,
|
5
|
+
:timestamp)
|
6
|
+
|
7
|
+
def self.deserialize(tokens)
|
8
|
+
self.new(
|
9
|
+
document_url_string: tokens.shift,
|
10
|
+
timestamp: tokens.shift,
|
11
|
+
)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module XcodeResultBundleProcessor
|
2
|
+
module SLF0
|
3
|
+
module Model
|
4
|
+
class DVTTextDocumentLocation < KeywordStruct.new(*(DVTDocumentLocation.members + [:starting_line_number, :starting_column_number, :ending_line_number, :ending_column_number, :character_range_1, :character_range_2, :location_encoding]))
|
5
|
+
|
6
|
+
def self.deserialize(tokens)
|
7
|
+
parent = DVTDocumentLocation.deserialize(tokens)
|
8
|
+
parent_values = DVTDocumentLocation.members.map { |member| parent[member] }
|
9
|
+
|
10
|
+
self.new(*(parent_values + [
|
11
|
+
tokens.shift,
|
12
|
+
tokens.shift,
|
13
|
+
tokens.shift,
|
14
|
+
tokens.shift,
|
15
|
+
tokens.shift,
|
16
|
+
tokens.shift,
|
17
|
+
tokens.shift,
|
18
|
+
]))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module XcodeResultBundleProcessor
|
2
|
+
module SLF0
|
3
|
+
module Model
|
4
|
+
class IDEActivityLogMessage < KeywordStruct.new(
|
5
|
+
:title, :short_title, :time_emitted, :range_in_section_text_1, :range_in_section_text_2, :submessages, :severity, :type, :location, :category_ident, :secondary_locations, :additional_description)
|
6
|
+
|
7
|
+
def self.deserialize(tokens)
|
8
|
+
self.new(
|
9
|
+
title: tokens.shift,
|
10
|
+
short_title: tokens.shift,
|
11
|
+
time_emitted: tokens.shift,
|
12
|
+
range_in_section_text_1: tokens.shift,
|
13
|
+
range_in_section_text_2: tokens.shift,
|
14
|
+
submessages: Deserializer.deserialize_list(tokens),
|
15
|
+
severity: tokens.shift,
|
16
|
+
type: tokens.shift,
|
17
|
+
location: Deserializer.deserialize(tokens),
|
18
|
+
category_ident: tokens.shift,
|
19
|
+
secondary_locations: Deserializer.deserialize_list(tokens),
|
20
|
+
additional_description: tokens.shift
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module XcodeResultBundleProcessor
|
2
|
+
module SLF0
|
3
|
+
module Model
|
4
|
+
class IDEActivityLogSection < KeywordStruct.new(:section_type, :domain_type, :title, :signature, :time_started_recording, :time_stopped_recording, :subsections, :text, :messages, :cancelled?, :quiet?, :fetched_from_cache?, :subtitle, :location, :command_detail_desc, :uuid, :localized_result_string)
|
5
|
+
def self.deserialize(tokens)
|
6
|
+
self.new(
|
7
|
+
section_type: tokens.shift,
|
8
|
+
domain_type: tokens.shift,
|
9
|
+
title: tokens.shift,
|
10
|
+
signature: tokens.shift,
|
11
|
+
time_started_recording: tokens.shift,
|
12
|
+
time_stopped_recording: tokens.shift,
|
13
|
+
subsections: Deserializer.deserialize_list(tokens),
|
14
|
+
text: tokens.shift,
|
15
|
+
messages: Deserializer.deserialize_list(tokens),
|
16
|
+
cancelled?: tokens.shift == 1,
|
17
|
+
quiet?: tokens.shift == 1,
|
18
|
+
fetched_from_cache?: tokens.shift == 1,
|
19
|
+
subtitle: tokens.shift,
|
20
|
+
location: tokens.shift,
|
21
|
+
command_detail_desc: tokens.shift,
|
22
|
+
uuid: tokens.shift,
|
23
|
+
localized_result_string: tokens.shift
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module XcodeResultBundleProcessor
|
2
|
+
module SLF0
|
3
|
+
module Model
|
4
|
+
class IDEActivityLogUnitTestSection < KeywordStruct.new(*(IDEActivityLogSection.members + [:tests_passed_string, :duration_string, :summary_string,
|
5
|
+
:suite_name, :test_name, :performance_test_output_string]))
|
6
|
+
|
7
|
+
def self.deserialize(tokens)
|
8
|
+
parent = IDEActivityLogSection.deserialize(tokens)
|
9
|
+
parent_values = IDEActivityLogSection.members.map { |member| parent[member] }
|
10
|
+
|
11
|
+
self.new(
|
12
|
+
*(parent_values +
|
13
|
+
[tokens.shift,
|
14
|
+
tokens.shift,
|
15
|
+
tokens.shift,
|
16
|
+
tokens.shift,
|
17
|
+
tokens.shift,
|
18
|
+
tokens.shift])
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module XcodeResultBundleProcessor
|
2
|
+
module SLF0
|
3
|
+
module Model
|
4
|
+
class IDEConsoleItem < KeywordStruct.new(:adaptor_type, :content, :kind, :timestamp)
|
5
|
+
def self.deserialize(tokens)
|
6
|
+
self.new(
|
7
|
+
adaptor_type: tokens.shift,
|
8
|
+
content: tokens.shift,
|
9
|
+
kind: tokens.shift,
|
10
|
+
timestamp: tokens.shift,
|
11
|
+
)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module XcodeResultBundleProcessor
|
2
|
+
module SLF0
|
3
|
+
module Tokenizer
|
4
|
+
Token = Struct.new(:int, :identifier)
|
5
|
+
ClassName = Struct.new(:class_name)
|
6
|
+
ClassNameRef = Struct.new(:class_name_id)
|
7
|
+
ObjectList = Struct.new(:mystery_number)
|
8
|
+
TOKEN_INT = '#'
|
9
|
+
TOKEN_CLASS_NAME = '%'
|
10
|
+
TOKEN_CLASS_NAME_REF = '@'
|
11
|
+
TOKEN_STRING = '"'
|
12
|
+
TOKEN_DOUBLE = '^'
|
13
|
+
OBJECT_LIST_NIL = '-'
|
14
|
+
OBJECT_LIST = '('
|
15
|
+
|
16
|
+
def self.read_token_stream(io)
|
17
|
+
raise 'Not an SLF0 file' unless self.valid_slf0_io?(io)
|
18
|
+
|
19
|
+
Enumerator.new do |enumerator|
|
20
|
+
until io.eof?
|
21
|
+
token = self.read_length_and_token_type(io)
|
22
|
+
|
23
|
+
case token.identifier
|
24
|
+
when TOKEN_INT
|
25
|
+
enumerator.yield(token.int)
|
26
|
+
when TOKEN_DOUBLE
|
27
|
+
enumerator.yield(token.int)
|
28
|
+
when TOKEN_STRING
|
29
|
+
enumerator.yield(io.read(token.int).gsub("\r", "\n"))
|
30
|
+
when TOKEN_CLASS_NAME
|
31
|
+
enumerator.yield(ClassName.new(io.read(token.int)))
|
32
|
+
when TOKEN_CLASS_NAME_REF
|
33
|
+
enumerator.yield(ClassNameRef.new(token.int))
|
34
|
+
when OBJECT_LIST
|
35
|
+
enumerator.yield(ObjectList.new(token.int))
|
36
|
+
when OBJECT_LIST_NIL
|
37
|
+
enumerator.yield(nil)
|
38
|
+
else
|
39
|
+
enumerator.yield(token)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.valid_slf0_io?(io)
|
47
|
+
return false if io.nil?
|
48
|
+
io.read(4) == 'SLF0'
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.read_length_and_token_type(io)
|
52
|
+
# The length appears as the start of the token as 0 or more ASCII decimal or hex digits until the token type marker
|
53
|
+
length_string = ''
|
54
|
+
until [TOKEN_INT, TOKEN_CLASS_NAME, TOKEN_CLASS_NAME_REF, TOKEN_STRING, TOKEN_DOUBLE, OBJECT_LIST_NIL, OBJECT_LIST].include?((c = io.readchar)) do
|
55
|
+
length_string << c
|
56
|
+
end
|
57
|
+
|
58
|
+
token = c
|
59
|
+
|
60
|
+
# TODO: Doubles are stored as hex strings. If it turns out the doubles contain useful information, implement
|
61
|
+
# logic to convert the hex string to doubles
|
62
|
+
if token == TOKEN_DOUBLE
|
63
|
+
length_string = ''
|
64
|
+
end
|
65
|
+
|
66
|
+
return Token.new(length_string.to_i, c)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def self._read_length_for_token(io, token_identifier)
|
72
|
+
# The length is an ASCII decimal string possibly preceded by dashes
|
73
|
+
length_string = ''
|
74
|
+
while (c = io.readchar) != token_identifier do
|
75
|
+
length_string << c unless c == '-' # Some strings lengths are preceded by 1 or more dashes, which we want to skip
|
76
|
+
end
|
77
|
+
|
78
|
+
length_string.to_i
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module XcodeResultBundleProcessor
|
2
|
+
class SnapshotSummary < KeywordStruct.new(:element_class, :children, :identifier, :title, :label, :value, :frame, :placeholder, :focused?, :keyboardFocused?, :selected?, :enabled?)
|
3
|
+
|
4
|
+
def self.parse(element_snapshot)
|
5
|
+
element_class = nil
|
6
|
+
if element_snapshot.key?(:additionalAttributes) and element_snapshot[:additionalAttributes].key?(5004.to_s.to_sym)
|
7
|
+
element_class = element_snapshot[:additionalAttributes][5004.to_s.to_sym]
|
8
|
+
end
|
9
|
+
|
10
|
+
SnapshotSummary.new(
|
11
|
+
element_class: element_class,
|
12
|
+
children: Array(element_snapshot[:children]).map { |child| SnapshotSummary.parse(child) },
|
13
|
+
identifier: element_snapshot[:identifier],
|
14
|
+
title: element_snapshot[:title],
|
15
|
+
label: element_snapshot[:label],
|
16
|
+
value: element_snapshot[:value],
|
17
|
+
frame: element_snapshot[:frame],
|
18
|
+
placeholder: element_snapshot[:placeholder],
|
19
|
+
focused?: !!element_snapshot[:hasFocus],
|
20
|
+
keyboardFocused?: !!element_snapshot[:hasKeyboardFocus],
|
21
|
+
selected?: !!element_snapshot[:selected],
|
22
|
+
enabled?: element_snapshot[:enabled] != false,
|
23
|
+
)
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
def summary
|
28
|
+
summary_components = [
|
29
|
+
self.element_class || 'UnknownElement',
|
30
|
+
self.frame
|
31
|
+
]
|
32
|
+
|
33
|
+
summary_components += [:identifier, :title, :label, :value, :placeholder].map do |attribute|
|
34
|
+
unless self[attribute].nil? or self[attribute].empty?
|
35
|
+
"#{attribute}=#{self[attribute]}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
summary_components << 'focused' if self.focused?
|
40
|
+
summary_components << 'keyboardFocused' if self.keyboardFocused?
|
41
|
+
summary_components << 'selected' if self.selected?
|
42
|
+
summary_components << 'disabled' unless self.enabled?
|
43
|
+
|
44
|
+
summary_components.compact.join(' ')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
module XcodeResultBundleProcessor
|
2
|
+
module TestSummaries
|
3
|
+
class HTMLReport
|
4
|
+
include Methadone::CLILogging
|
5
|
+
|
6
|
+
def initialize(results_bundle)
|
7
|
+
@results_bundle = results_bundle
|
8
|
+
@stylesheet_path = File.join(File.dirname(__FILE__), '..', '..', '..', 'static', 'report.css')
|
9
|
+
@js_path = File.join(File.dirname(__FILE__), '..', '..', '..', 'static', 'report.js')
|
10
|
+
end
|
11
|
+
|
12
|
+
def save(destination_dir)
|
13
|
+
info "Saving HTML report to #{destination_dir}"
|
14
|
+
|
15
|
+
raise "Destination directory #{destination_dir} already exists" if Dir.exist?(destination_dir)
|
16
|
+
|
17
|
+
FileUtils.mkdir_p(File.join(destination_dir, 'screenshots'))
|
18
|
+
|
19
|
+
info 'Deserializing logs'
|
20
|
+
action_logs = LogDeserializer.deserialize_action_logs(@results_bundle)
|
21
|
+
debug action_logs
|
22
|
+
|
23
|
+
|
24
|
+
tests = TestSummaries.new(@results_bundle.read_plist('TestSummaries.plist'))
|
25
|
+
|
26
|
+
debug "Formatting test summaries <#{tests.ai}>"
|
27
|
+
|
28
|
+
Markaby::Builder.set_html5_options!
|
29
|
+
Markaby::Builder.set(:indent, 2)
|
30
|
+
mab = Markaby::Builder.new({}, self)
|
31
|
+
report = mab.html5 do
|
32
|
+
head do
|
33
|
+
link rel: 'stylesheet', href: 'report.css'
|
34
|
+
script src: 'report.js' do
|
35
|
+
''
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
body do
|
40
|
+
unless tests.failed_tests.empty?
|
41
|
+
h1 'Failed Tests :('
|
42
|
+
ul do
|
43
|
+
tests.failed_tests.each do |failed_test|
|
44
|
+
li do
|
45
|
+
a href: "##{failed_test.identifier}" do
|
46
|
+
failed_test.identifier
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
h1 'Test Timelines'
|
54
|
+
tests.tests.each do |test|
|
55
|
+
_format_test(test, mab, destination_dir)
|
56
|
+
end
|
57
|
+
|
58
|
+
hr
|
59
|
+
|
60
|
+
h1 'Action Logs'
|
61
|
+
pre action_logs
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
FileUtils.copy(@stylesheet_path, File.join(destination_dir, 'report.css'))
|
67
|
+
FileUtils.copy(@js_path, File.join(destination_dir, 'report.js'))
|
68
|
+
File.open(File.join(destination_dir, 'index.html'), 'w').write(report)
|
69
|
+
end
|
70
|
+
|
71
|
+
def _format_test(test, mab, destination_dir)
|
72
|
+
mab.a name: test.identifier do
|
73
|
+
if test.passed?
|
74
|
+
mab.h2 test.summary
|
75
|
+
else
|
76
|
+
mab.h2.testFailed test.summary
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
mab.ul do
|
81
|
+
test.failure_summaries.each do |failure_summary|
|
82
|
+
li do
|
83
|
+
em "Failure at #{failure_summary.location}"
|
84
|
+
pre failure_summary.message
|
85
|
+
end
|
86
|
+
end
|
87
|
+
''
|
88
|
+
end
|
89
|
+
|
90
|
+
mab.div.timelineContainer do
|
91
|
+
table do
|
92
|
+
tr do
|
93
|
+
test.activities.each do |activity_summary|
|
94
|
+
td do
|
95
|
+
h4 activity_summary.title
|
96
|
+
|
97
|
+
table do
|
98
|
+
tr do
|
99
|
+
activity_summary.subactivities.each do |subactivity|
|
100
|
+
td do
|
101
|
+
h5 subactivity.title
|
102
|
+
|
103
|
+
unless subactivity.screenshot_path.nil?
|
104
|
+
basename = File.basename(subactivity.screenshot_path)
|
105
|
+
|
106
|
+
output_image_path = File.join(destination_dir, 'screenshots', basename)
|
107
|
+
@results_bundle.copy_file(File.join('Attachments', subactivity.screenshot_path), output_image_path)
|
108
|
+
|
109
|
+
img src: File.join('screenshots', basename)
|
110
|
+
end
|
111
|
+
|
112
|
+
unless subactivity.snapshot_path.nil?
|
113
|
+
a.viewSnapshot href: '#' do
|
114
|
+
'View Snapshot'
|
115
|
+
end
|
116
|
+
|
117
|
+
pre.snapshot do
|
118
|
+
@results_bundle.open_file(File.join('Attachments', subactivity.snapshot_path)) do |file|
|
119
|
+
snapshot_plist = CFPropertyList::List.new(data: file.read)
|
120
|
+
element_snapshot = ElementSnapshot.new(snapshot_plist)
|
121
|
+
|
122
|
+
snapshot_summary = SnapshotSummary.parse(element_snapshot.to_h)
|
123
|
+
|
124
|
+
buffer = IndentedStringBuffer.new
|
125
|
+
_format_element_summary(buffer, snapshot_summary)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
''
|
131
|
+
end
|
132
|
+
end
|
133
|
+
''
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
''
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
''
|
143
|
+
end
|
144
|
+
|
145
|
+
def _format_element_summary(buffer, element_summary)
|
146
|
+
buffer << element_summary.summary
|
147
|
+
|
148
|
+
element_summary.children.each do |child|
|
149
|
+
_format_element_summary(buffer.indent, child)
|
150
|
+
end
|
151
|
+
|
152
|
+
buffer
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|