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