-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Export screenshots #5
Draft
ainame
wants to merge
24
commits into
fastlane-community:master
Choose a base branch
from
ainame:export-screenshots
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
1e1614d
Update Gemfile.lock
ainame 49f71a1
Add XCResult::ModelGenerator
ainame 62ecc34
Add a script to generete models
ainame 9d287c0
Add nil check and Double type support
ainame ff218c2
Use TSort to define classes that depend on another class
ainame 5f870ff
Fix bugs
ainame 3eae67c
Migrate existing models
ainame 33bdcc0
Add missing super for subclasses
ainame 8f4ae56
Update models
ainame 0c34d5e
Rename source -> format
ainame 1e7457f
Fix conversion of camel case to snake_case
ainame 6bec119
Fix rubocop issues
ainame 2eb2c70
Extract Models.load_class
ainame 893edf8
Move XCResult::Models.load_class to models.rb
ainame f065120
Support JSON foramt parsing
ainame 4da709c
Update models
ainame 65e6605
Fix a bug where primitive values aren't converted
ainame 910c1a7
add Reference#load_object to simplify loading
ainame 6b560df
Add Parser#export_screenshots
ainame eceb87b
Add handy standard script; bin/console, bin/setup
ainame 540384e
Update README.md
ainame 5c49bf7
add XCResult::ExportOptions
ainame 3808edd
Update README.md
ainame a4f5dcb
Add more options
ainame File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
#!/usr/bin/env ruby | ||
# frozen_string_literal: true | ||
|
||
require "bundler/setup" | ||
require "xcresult" | ||
|
||
# You can add fixtures and/or initialization code here to make experimenting | ||
# with your gem easier. You can also use a different console, if you like. | ||
|
||
# (If you use this, don't forget to add pry to your Gemfile!) | ||
# require "pry" | ||
# Pry.start | ||
|
||
require "irb" | ||
IRB.start(__FILE__) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
#! /usr/bin/env ruby | ||
|
||
require_relative '../lib/xcresult/model_generator' | ||
|
||
XCResult::ModelGenerator.generate(File.expand_path(File.join(File.dirname(__FILE__), '../lib/xcresult/models.gen.rb'))) | ||
puts 'Done! Check out lib/xcresult/models.gen.rb' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
#!/usr/bin/env bash | ||
set -euo pipefail | ||
IFS=$'\n\t' | ||
set -vx | ||
|
||
bundle install | ||
|
||
# Do any other automated setup that you need to do here |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
module XCResult | ||
class ExportOptions | ||
AVAILABLE_OPTIONS = %i[destination by_device by_locale by_region by_language by_os_version by_test_name].freeze | ||
|
||
# A initializer of ExportOptions class. The order of options that are prefixed with `by_` is important. | ||
# `XCResult::Parser#export_screenshots` will create nested directories by the order you give. | ||
# | ||
# @option [String] destination The base directory path of exported items | ||
# @option [Boolean] by_device If true, a nested directory under destination will be made based on device model name | ||
# @option [Boolean] by_locale If true, a nested directory under destination will be made based on locale; i.e. "en_US" | ||
# @option [Boolean] by_region If true, a nested directory under destination will be made based on region | ||
# @option [Boolean] by_language If true, a nested directory under destination will be made based on language | ||
# @option [Boolean] by_os_version If true, a nested directory under destination will be made based on OS version | ||
# @option [Boolean] by_test_name If true, a nested directory under destination will be made based on test name in TestPlan | ||
def initialize(**options) | ||
raise ':destination option is required' unless options[:destination] | ||
|
||
options.each do |key, value| | ||
raise "Found unknown option #{key} - #{value}." unless AVAILABLE_OPTIONS.include?(key) | ||
end | ||
|
||
@options = options.dup | ||
@destination = options.delete(:destination) | ||
end | ||
|
||
def output_directory(target_device_record:, action_test_plan_summary:, action_testable_summary:) | ||
output_directory = @destination | ||
|
||
# keep the order of given options so that you can customize output directory path based on your needs | ||
@options.select { |key, value| key.to_s.start_with?('by_') && value == true }.each do |key, _| | ||
case key | ||
when :by_device | ||
output_directory = File.join(output_directory, target_device_record.model_name) | ||
when :by_language | ||
output_directory = File.join(output_directory, action_testable_summary.test_language) | ||
when :by_region | ||
output_directory = File.join(output_directory, action_testable_summary.test_region) | ||
when :by_locale | ||
locale = [action_testable_summary.test_language, action_testable_summary.test_region].compact.join('_') | ||
locale = 'UNKOWN' if locale.empty? | ||
output_directory = File.join(output_directory, locale) | ||
when :by_os_version | ||
output_directory = File.join(output_directory, target_device_record.operating_system_version) | ||
when :by_test_name | ||
output_directory = File.join(output_directory, action_test_plan_summary.name) | ||
end | ||
end | ||
|
||
output_directory | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'json' | ||
require 'erb' | ||
require 'tsort' | ||
|
||
module XCResult | ||
class ModelGenerator | ||
def self.format_description | ||
`xcrun xcresulttool formatDescription get --format json` | ||
end | ||
|
||
def self.generate(destination) | ||
parser = Parser.new(format: format_description) | ||
parser.parse | ||
generator = Generator.new(parser.format) | ||
generator.write(destination) | ||
end | ||
|
||
class Generator | ||
def initialize(format) | ||
@format = format | ||
@type_to_kind = format.types.map { |t| [t.name, t.kind] }.to_h | ||
end | ||
|
||
def write(destination) | ||
file = File.open(destination, 'w+') | ||
file.puts(<<~"HEADER") | ||
# This is a generated file. Don't modify this directly! | ||
# Last generated at: #{Time.now.utc} | ||
# | ||
# Name: #{@format.name} | ||
# Version: #{@format.version} | ||
# Signature: #{@format.signature} | ||
require 'time' | ||
|
||
HEADER | ||
|
||
file.puts(<<~OPEN_MODULE) | ||
module XCResult | ||
module Models | ||
|
||
OPEN_MODULE | ||
|
||
sorted_types.each do |type| | ||
# We use Ruby native classes for values, like Date, String, Int, Double, etc.. | ||
next if type.kind == 'value' | ||
|
||
type_text = compose_type(type, 2 * 2) | ||
file.puts(type_text) | ||
file.puts('') | ||
end | ||
|
||
file.puts(<<~CLOSE_MODULE) | ||
end | ||
end | ||
CLOSE_MODULE | ||
ensure | ||
file.close | ||
end | ||
|
||
# Use topological sort to correctly defines classes that may depend on another as its super class | ||
def sorted_types | ||
h = @format.types.map {|x| [x, [@format.types.find {|y| y.name == x.supertype}].compact] }.to_h | ||
each_node = ->(&b) { h.each_key(&b) } | ||
each_child = ->(n, &b) { h[n].each(&b) } | ||
TSort.tsort(each_node, each_child) | ||
end | ||
|
||
def compose_type(type, indentation) | ||
type_def = <<~"TYPE" | ||
<% if type.supertype %> | ||
class <%= type.name %> < <%= type.supertype %> | ||
<% else %> | ||
class <%= type.name %> | ||
<% end%> | ||
<% type.properties.each do |property| %> | ||
<%= property.rdoc_comment %> | ||
attr_reader :<%= property.name_in_snake_case %> | ||
<% end %> | ||
|
||
def initialize(data) | ||
<% type.properties.each do |property| %> | ||
@<%= property.name_in_snake_case %> = <%= property.mapping(type_to_kind[property.wrapped_type || property.type], 'data') %> | ||
<% end %> | ||
<% if type.supertype %> | ||
super | ||
<% end %> | ||
end | ||
end | ||
TYPE | ||
type_def = ERB.new(type_def, trim_mode: '<>').result_with_hash(type: type, type_to_kind: @type_to_kind) | ||
type_def.each_line.map { |line| "#{' ' * indentation}#{line}" }.join | ||
end | ||
end | ||
|
||
class Parser | ||
attr_reader :format | ||
|
||
def initialize(format:) | ||
@raw_format = format | ||
@json = JSON.parse(format) | ||
@format = Format.new | ||
end | ||
|
||
def parse | ||
@format.name = @json['name'] | ||
@format.version = @json['version'].values.join('.') | ||
@format.signature = @json['signature'] | ||
@format.types = @json['types'].map do |type| | ||
Type.new( | ||
name: type.dig('type', 'name'), | ||
supertype: type.dig('type', 'supertype'), | ||
kind: type['kind'], | ||
raw_text: JSON.pretty_generate(type), | ||
properties: type.fetch('properties', []).map do |prop| | ||
Property.new( | ||
name: prop['name'], | ||
type: prop['type'], | ||
wrapped_type: prop['wrappedType'], | ||
is_optional: prop['isOptional'], | ||
is_internal: prop['isInternal'] | ||
) | ||
end | ||
) | ||
end | ||
end | ||
end | ||
|
||
Format = Struct.new(:name, :version, :signature, :types, keyword_init: true) | ||
Type = Struct.new(:name, :supertype, :kind, :properties, :raw_text, keyword_init: true) | ||
Property = Struct.new(:name, :type, :wrapped_type, :is_optional, :is_internal, keyword_init: true) do | ||
def name_in_snake_case | ||
name | ||
.gsub(/(CPU|ID|MHz|UTI|SDK)/) { Regexp.last_match[1].downcase.capitalize } # normalize acronyms | ||
.gsub(/([a-z])(?=[A-Z])/) { (Regexp.last_match[1] || Regexp.last_match[2]) << '_' } | ||
.downcase | ||
end | ||
|
||
def mapping(kind, variable_name) | ||
return "(#{variable_name}.dig('#{name}', '_values') || []).map {|d| #{_mapping(kind, 'd')} }" if type == 'Array' | ||
_mapping(kind, variable_name) + (is_optional ? " if #{variable_name}['#{name}']" : '') | ||
end | ||
|
||
def _mapping(kind, variable_name) | ||
if kind == 'object' | ||
type_access_key = type == 'Array' ? "'_type', '_name'" : "'#{name}', '_type', '_name'" | ||
value_access_key = type == 'Array' ? variable_name : "#{variable_name}.dig('#{name}')" | ||
"Models.load_class(#{variable_name}.dig(#{type_access_key})).new(#{value_access_key})" | ||
elsif kind == 'value' && [type, wrapped_type].include?('Date') | ||
"Time.parse(#{variable_name}.dig('#{name}', '_value'))" | ||
elsif kind == 'value' && [type, wrapped_type].include?('Int') | ||
"#{variable_name}.dig('#{name}', '_value').to_i" | ||
elsif kind == 'value' && [type, wrapped_type].include?('Double') | ||
"#{variable_name}.dig('#{name}', '_value').to_f" | ||
else | ||
"#{variable_name}.dig('#{name}', '_value')" | ||
end | ||
end | ||
|
||
def rdoc_comment | ||
if type == 'Array' | ||
"# @return [Array<#{wrapped_type}>] #{name_in_snake_case}" | ||
elsif is_optional | ||
"# @return [#{wrapped_type}, nil] #{name_in_snake_case}" | ||
else | ||
"# @return [#{type}] #{name_in_snake_case}" | ||
end | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
todo add document comments