fastlane 2.13.0 → 2.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/credentials_manager/lib/credentials_manager.rb +1 -1
  3. data/fastlane/lib/fastlane/actions/ipa.rb +2 -1
  4. data/fastlane/lib/fastlane/actions/mailgun.rb +15 -2
  5. data/fastlane/lib/fastlane/actions/scan.rb +14 -0
  6. data/fastlane/lib/fastlane/documentation/docs_generator.rb +24 -1
  7. data/fastlane/lib/fastlane/environment_printer.rb +2 -1
  8. data/fastlane/lib/fastlane/fast_file.rb +4 -4
  9. data/fastlane/lib/fastlane/version.rb +1 -1
  10. data/fastlane_core/lib/fastlane_core.rb +1 -1
  11. data/fastlane_core/lib/fastlane_core/configuration/config_item.rb +44 -2
  12. data/fastlane_core/lib/fastlane_core/device_manager.rb +15 -0
  13. data/fastlane_core/lib/fastlane_core/helper.rb +1 -1
  14. data/fastlane_core/lib/fastlane_core/ui/disable_colors.rb +4 -4
  15. data/frameit/lib/frameit/config_parser.rb +8 -13
  16. data/frameit/lib/frameit/editor.rb +3 -2
  17. data/gym/lib/gym/options.rb +4 -2
  18. data/match/README.md +2 -2
  19. data/match/lib/match.rb +5 -19
  20. data/match/lib/match/generator.rb +8 -1
  21. data/match/lib/match/git_helper.rb +3 -1
  22. data/match/lib/match/nuke.rb +18 -14
  23. data/match/lib/match/options.rb +13 -2
  24. data/match/lib/match/runner.rb +20 -8
  25. data/match/lib/match/table_printer.rb +5 -4
  26. data/match/lib/match/utils.rb +12 -8
  27. data/scan/lib/scan/options.rb +24 -1
  28. data/scan/lib/scan/runner.rb +12 -11
  29. data/scan/lib/scan/test_command_generator.rb +10 -2
  30. data/sigh/lib/sigh/download_all.rb +1 -1
  31. data/sigh/lib/sigh/runner.rb +2 -2
  32. data/snapshot/lib/snapshot/options.rb +2 -1
  33. data/snapshot/lib/snapshot/runner.rb +12 -12
  34. data/spaceship/lib/spaceship.rb +1 -0
  35. data/spaceship/lib/spaceship/base.rb +27 -4
  36. data/spaceship/lib/spaceship/client.rb +3 -2
  37. data/spaceship/lib/spaceship/du/du_client.rb +26 -16
  38. data/spaceship/lib/spaceship/portal/app.rb +1 -1
  39. data/spaceship/lib/spaceship/portal/person.rb +53 -0
  40. data/spaceship/lib/spaceship/portal/persons.rb +49 -0
  41. data/spaceship/lib/spaceship/portal/portal.rb +2 -0
  42. data/spaceship/lib/spaceship/portal/portal_client.rb +69 -10
  43. data/spaceship/lib/spaceship/portal/provisioning_profile.rb +29 -1
  44. data/spaceship/lib/spaceship/tunes/application.rb +11 -1
  45. data/spaceship/lib/spaceship/tunes/iap.rb +113 -0
  46. data/spaceship/lib/spaceship/tunes/iap_detail.rb +185 -0
  47. data/spaceship/lib/spaceship/tunes/iap_families.rb +62 -0
  48. data/spaceship/lib/spaceship/tunes/iap_family_details.rb +59 -0
  49. data/spaceship/lib/spaceship/tunes/iap_family_list.rb +33 -0
  50. data/spaceship/lib/spaceship/tunes/iap_list.rb +72 -0
  51. data/spaceship/lib/spaceship/tunes/iap_status.rb +48 -0
  52. data/spaceship/lib/spaceship/tunes/iap_type.rb +45 -0
  53. data/spaceship/lib/spaceship/tunes/tunes.rb +1 -0
  54. data/spaceship/lib/spaceship/tunes/tunes_client.rb +163 -1
  55. metadata +21 -5
@@ -46,7 +46,9 @@ module Match
46
46
  [
47
47
  "[fastlane]",
48
48
  "Updated",
49
- params[:type].to_s
49
+ params[:type].to_s,
50
+ "and platform",
51
+ params[:platform]
50
52
  ].join(" ")
51
53
  end
52
54
 
@@ -13,7 +13,7 @@ module Match
13
13
 
14
14
  params[:workspace] = GitHelper.clone(params[:git_url], params[:shallow_clone], skip_docs: params[:skip_docs], branch: params[:git_branch])
15
15
 
16
- had_app_identifier = self.params[:app_identifier]
16
+ had_app_identifier = self.params.fetch(:app_identifier, ask: false)
17
17
  self.params[:app_identifier] = '' # we don't really need a value here
18
18
  FastlaneCore::PrintTable.print_values(config: params,
19
19
  hide_keys: [:app_identifier, :workspace],
@@ -51,12 +51,16 @@ module Match
51
51
  UI.message "Fetching certificates and profiles..."
52
52
  cert_type = Match.cert_type_sym(type)
53
53
 
54
- prov_types = [:development]
54
+ prov_types = []
55
+ prov_types = [:development] if cert_type == :development
55
56
  prov_types = [:appstore, :adhoc] if cert_type == :distribution
57
+ prov_types = [:enterprise] if cert_type == :enterprise
56
58
 
57
59
  Spaceship.login(params[:username])
58
60
  Spaceship.select_team
59
61
 
62
+ UI.user_error!("`fastlane match nuke` doesn't support enterprise accounts") if Spaceship.client.in_house?
63
+
60
64
  self.certs = certificate_type(cert_type).all
61
65
  self.profiles = []
62
66
  prov_types.each do |prov_type|
@@ -170,21 +174,21 @@ module Match
170
174
 
171
175
  # The kind of certificate we're interested in
172
176
  def certificate_type(type)
173
- cert_type = Spaceship.certificate.production
174
- cert_type = Spaceship.certificate.development if type == :development
175
- cert_type = Spaceship.certificate.in_house if Match.enterprise? && Spaceship.client.in_house?
176
-
177
- cert_type
177
+ {
178
+ distribution: Spaceship.certificate.production,
179
+ development: Spaceship.certificate.development,
180
+ enterprise: Spaceship.certificate.in_house
181
+ }[type] ||= raise "Unknown type '#{type}'"
178
182
  end
179
183
 
180
184
  # The kind of provisioning profile we're interested in
181
- def profile_type(type)
182
- profile_type = Spaceship.provisioning_profile.app_store
183
- profile_type = Spaceship.provisioning_profile.in_house if Match.enterprise? && Spaceship.client.in_house?
184
- profile_type = Spaceship.provisioning_profile.ad_hoc if type == :adhoc
185
- profile_type = Spaceship.provisioning_profile.development if type == :development
186
-
187
- profile_type
185
+ def profile_type(prov_type)
186
+ {
187
+ appstore: Spaceship.provisioning_profile.app_store,
188
+ development: Spaceship.provisioning_profile.development,
189
+ enterprise: Spaceship.provisioning_profile.in_house,
190
+ adhoc: Spaceship.provisioning_profile.ad_hoc
191
+ }[prov_type] ||= raise "Unknown provisioning type '#{prov_type}'"
188
192
  end
189
193
  end
190
194
  end
@@ -19,7 +19,7 @@ module Match
19
19
  default_value: 'master'),
20
20
  FastlaneCore::ConfigItem.new(key: :type,
21
21
  env_name: "MATCH_TYPE",
22
- description: "Create a development certificate instead of a distribution one",
22
+ description: "Define the profile type, can be #{Match.environments.join(', ')}",
23
23
  is_string: true,
24
24
  short_option: "-y",
25
25
  default_value: 'development',
@@ -117,7 +117,18 @@ module Match
117
117
  env_name: "MATCH_SKIP_DOCS",
118
118
  description: "Skip generation of a README.md for the created git repository",
119
119
  is_string: false,
120
- default_value: false)
120
+ default_value: false),
121
+ FastlaneCore::ConfigItem.new(key: :platform,
122
+ short_option: '-o',
123
+ env_name: "MATCH_PLATFORM",
124
+ description: "Set the provisioning profile's platform to work with (i.e. ios, tvos)",
125
+ is_string: false,
126
+ default_value: "ios",
127
+ verify_block: proc do |value|
128
+ value = value.to_s
129
+ pt = %w(tvos ios)
130
+ UI.user_error!("Unsupported platform, must be: #{pt}") unless pt.include?(value)
131
+ end)
121
132
  ]
122
133
  end
123
134
  end
@@ -8,10 +8,14 @@ module Match
8
8
  hide_keys: [:workspace],
9
9
  title: "Summary for match #{Fastlane::VERSION}")
10
10
 
11
- UI.error("Enterprise profiles are currently not officially supported in _match_, you might run into issues") if Match.enterprise?
12
-
13
11
  params[:workspace] = GitHelper.clone(params[:git_url], params[:shallow_clone], skip_docs: params[:skip_docs], branch: params[:git_branch])
14
- self.spaceship = SpaceshipEnsure.new(params[:username]) unless params[:readonly]
12
+
13
+ unless params[:readonly]
14
+ self.spaceship = SpaceshipEnsure.new(params[:username])
15
+ if params[:type] == "enterprise" && !Spaceship.client.in_house?
16
+ UI.user_error!("You defined the profile type 'enterprise', but your Apple account doesn't support In-House profiles")
17
+ end
18
+ end
15
19
 
16
20
  if params[:app_identifier].kind_of?(Array)
17
21
  app_identifiers = params[:app_identifier]
@@ -47,7 +51,7 @@ module Match
47
51
 
48
52
  # Print a summary table for each app_identifier
49
53
  app_identifiers.each do |app_identifier|
50
- TablePrinter.print_summary(app_identifier: app_identifier, type: params[:type])
54
+ TablePrinter.print_summary(app_identifier: app_identifier, type: params[:type], platform: params[:platform])
51
55
  end
52
56
 
53
57
  UI.success "All required keys, certificates and provisioning profiles are installed 🙌".green
@@ -98,7 +102,11 @@ module Match
98
102
  def fetch_provisioning_profile(params: nil, certificate_id: nil, app_identifier: nil)
99
103
  prov_type = Match.profile_type_sym(params[:type])
100
104
 
101
- profile_name = [Match::Generator.profile_type_name(prov_type), app_identifier].join("_").gsub("*", '\*') # this is important, as it shouldn't be a wildcard
105
+ names = [Match::Generator.profile_type_name(prov_type), app_identifier]
106
+ if params[:platform].to_s != :ios.to_s
107
+ names.push(params[:platform])
108
+ end
109
+ profile_name = names.join("_").gsub("*", '\*') # this is important, as it shouldn't be a wildcard
102
110
  base_dir = File.join(params[:workspace], "profiles", prov_type.to_s)
103
111
  profiles = Dir[File.join(base_dir, "#{profile_name}.mobileprovision")]
104
112
 
@@ -139,16 +147,20 @@ module Match
139
147
  end
140
148
 
141
149
  Utils.fill_environment(Utils.environment_variable_name(app_identifier: app_identifier,
142
- type: prov_type),
150
+ type: prov_type,
151
+ platform: params[:platform]),
152
+
143
153
  uuid)
144
154
 
145
155
  # TeamIdentifier is returned as an array, but we're not sure why there could be more than one
146
156
  Utils.fill_environment(Utils.environment_variable_name_team_id(app_identifier: app_identifier,
147
- type: prov_type),
157
+ type: prov_type,
158
+ platform: params[:platform]),
148
159
  parsed["TeamIdentifier"].first)
149
160
 
150
161
  Utils.fill_environment(Utils.environment_variable_name_profile_name(app_identifier: app_identifier,
151
- type: prov_type),
162
+ type: prov_type,
163
+ platform: params[:platform]),
152
164
  parsed["Name"])
153
165
 
154
166
  return uuid
@@ -14,18 +14,19 @@ module Match
14
14
  UI.error(ex)
15
15
  end
16
16
 
17
- def self.print_summary(app_identifier: nil, type: nil)
17
+ def self.print_summary(app_identifier: nil, type: nil, platform: :ios)
18
18
  rows = []
19
19
 
20
20
  type = type.to_sym
21
21
 
22
22
  rows << ["App Identifier", "", app_identifier]
23
23
  rows << ["Type", "", type]
24
+ rows << ["Platform", "", platform.to_s]
24
25
 
25
26
  {
26
- Utils.environment_variable_name(app_identifier: app_identifier, type: type) => "Profile UUID",
27
- Utils.environment_variable_name_profile_name(app_identifier: app_identifier, type: type) => "Profile Name",
28
- Utils.environment_variable_name_team_id(app_identifier: app_identifier, type: type) => "Development Team ID"
27
+ Utils.environment_variable_name(app_identifier: app_identifier, type: type, platform: platform) => "Profile UUID",
28
+ Utils.environment_variable_name_profile_name(app_identifier: app_identifier, type: type, platform: platform) => "Profile Name",
29
+ Utils.environment_variable_name_team_id(app_identifier: app_identifier, type: type, platform: platform) => "Development Team ID"
29
30
  }.each do |env_key, name|
30
31
  rows << [name, env_key, ENV[env_key]]
31
32
  end
@@ -11,16 +11,16 @@ module Match
11
11
  ENV[key] = value
12
12
  end
13
13
 
14
- def self.environment_variable_name(app_identifier: nil, type: nil)
15
- base_environment_variable_name(app_identifier: app_identifier, type: type).join("_")
14
+ def self.environment_variable_name(app_identifier: nil, type: nil, platform: :ios)
15
+ base_environment_variable_name(app_identifier: app_identifier, type: type, platform: platform).join("_")
16
16
  end
17
17
 
18
- def self.environment_variable_name_team_id(app_identifier: nil, type: nil)
19
- (base_environment_variable_name(app_identifier: app_identifier, type: type) + ["team-id"]).join("_")
18
+ def self.environment_variable_name_team_id(app_identifier: nil, type: nil, platform: :ios)
19
+ (base_environment_variable_name(app_identifier: app_identifier, type: type, platform: platform) + ["team-id"]).join("_")
20
20
  end
21
21
 
22
- def self.environment_variable_name_profile_name(app_identifier: nil, type: nil)
23
- (base_environment_variable_name(app_identifier: app_identifier, type: type) + ["profile-name"]).join("_")
22
+ def self.environment_variable_name_profile_name(app_identifier: nil, type: nil, platform: :ios)
23
+ (base_environment_variable_name(app_identifier: app_identifier, type: type, platform: platform) + ["profile-name"]).join("_")
24
24
  end
25
25
 
26
26
  def self.get_cert_info(cer_certificate_path)
@@ -52,8 +52,12 @@ module Match
52
52
  return {}
53
53
  end
54
54
 
55
- def self.base_environment_variable_name(app_identifier: nil, type: nil)
56
- ["sigh", app_identifier, type]
55
+ def self.base_environment_variable_name(app_identifier: nil, type: nil, platform: :ios)
56
+ if platform.to_s == :ios.to_s
57
+ ["sigh", app_identifier, type] # We keep the ios profiles without the platform for backwards compatibility
58
+ else
59
+ ["sigh", app_identifier, type, platform.to_s]
60
+ end
57
61
  end
58
62
  end
59
63
  end
@@ -125,6 +125,28 @@ module Scan
125
125
  env_name: "SCAN_FORMATTER",
126
126
  description: "A custom xcpretty formatter to use",
127
127
  optional: true),
128
+
129
+ FastlaneCore::ConfigItem.new(key: :test_without_building,
130
+ short_option: "-T",
131
+ env_name: "SCAN_TEST_WITHOUT_BUILDING",
132
+ description: "Test without building, requires a derrived data path",
133
+ is_string: false,
134
+ conflicting_options: [:build_for_testing],
135
+ optional: true),
136
+ FastlaneCore::ConfigItem.new(key: :build_for_testing,
137
+ short_option: "-B",
138
+ env_name: "SCAN_BUILD_FOR_TESTING",
139
+ description: "Build for testing only, does not run tests",
140
+ conflicting_options: [:test_without_building],
141
+ is_string: false,
142
+ optional: true),
143
+ FastlaneCore::ConfigItem.new(key: :xctestrun,
144
+ short_option: "-X",
145
+ env_name: "SCAN_XCTESTRUN",
146
+ description: "Run tests using the provided .xctestrun file",
147
+ conflicting_options: [:test_without_building, :build_for_testing],
148
+ is_string: true,
149
+ optional: true),
128
150
  FastlaneCore::ConfigItem.new(key: :derived_data_path,
129
151
  short_option: "-j",
130
152
  env_name: "SCAN_DERIVED_DATA_PATH",
@@ -162,7 +184,8 @@ module Scan
162
184
  short_option: "-x",
163
185
  env_name: "SCAN_XCARGS",
164
186
  description: "Pass additional arguments to xcodebuild. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS=\"-ObjC -lstdc++\"",
165
- optional: true),
187
+ optional: true,
188
+ type: :shell_string),
166
189
  FastlaneCore::ConfigItem.new(key: :xcconfig,
167
190
  short_option: "-y",
168
191
  env_name: "SCAN_XCCONFIG",
@@ -16,7 +16,6 @@ module Scan
16
16
  # This way it's okay to just call it for the first simulator we're using for
17
17
  # the first test run
18
18
  open_simulator_for_device(Scan.devices.first) if Scan.devices
19
-
20
19
  command = TestCommandGenerator.generate
21
20
  prefix_hash = [
22
21
  {
@@ -80,17 +79,9 @@ module Scan
80
79
  })
81
80
  puts ""
82
81
 
83
- report_collector.parse_raw_file(TestCommandGenerator.xcodebuild_log_path)
82
+ copy_simulator_logs
84
83
 
85
- if Scan.config[:include_simulator_logs]
86
- Scan.devices.each do |device|
87
- sim_device_logfilepath_source = File.expand_path("~/Library/Logs/CoreSimulator/#{device.udid}/system.log")
88
- if File.exist?(sim_device_logfilepath_source)
89
- sim_device_logfilepath_dest = File.join(Scan.config[:output_directory], "#{device.name}_#{device.os_type}_#{device.os_version}_system.log")
90
- FileUtils.cp(sim_device_logfilepath_source, sim_device_logfilepath_dest)
91
- end
92
- end
93
- end
84
+ report_collector.parse_raw_file(TestCommandGenerator.xcodebuild_log_path)
94
85
 
95
86
  unless tests_exit_status == 0
96
87
  UI.user_error!("Test execution failed. Exit status: #{tests_exit_status}")
@@ -101,6 +92,16 @@ module Scan
101
92
  end
102
93
  end
103
94
 
95
+ def copy_simulator_logs
96
+ return unless Scan.config[:include_simulator_logs]
97
+
98
+ UI.header("Collecting system logs")
99
+ Scan.devices.each do |device|
100
+ logarchive_dest = File.join(Scan.config[:output_directory], "system_logs-#{device.name}_#{device.os_type}_#{device.os_version}.logarchive")
101
+ FastlaneCore::Simulator.copy_logarchive(device, logarchive_dest)
102
+ end
103
+ end
104
+
104
105
  def open_simulator_for_device(device)
105
106
  return unless FastlaneCore::Env.truthy?('FASTLANE_EXPLICIT_OPEN_SIMULATOR')
106
107
 
@@ -39,6 +39,7 @@ module Scan
39
39
  options << "-enableAddressSanitizer #{config[:address_sanitizer] ? 'YES' : 'NO'}" unless config[:address_sanitizer].nil?
40
40
  options << "-enableThreadSanitizer #{config[:thread_sanitizer] ? 'YES' : 'NO'}" unless config[:thread_sanitizer].nil?
41
41
  options << "-xcconfig '#{config[:xcconfig]}'" if config[:xcconfig]
42
+ options << "-xctestrun '#{config[:xctestrun]}'" if config[:xctestrun]
42
43
  options << config[:xcargs] if config[:xcargs]
43
44
 
44
45
  # detect_values will ensure that these values are present as Arrays if
@@ -54,8 +55,15 @@ module Scan
54
55
 
55
56
  actions = []
56
57
  actions << :clean if config[:clean]
57
- actions << :build unless config[:skip_build]
58
- actions << :test
58
+
59
+ if config[:build_for_testing]
60
+ actions << "build-for-testing"
61
+ elsif config[:test_without_building] || config[:xctestrun]
62
+ actions << "test-without-building"
63
+ else
64
+ actions << :build unless config[:skip_build]
65
+ actions << :test
66
+ end
59
67
 
60
68
  actions
61
69
  end
@@ -23,7 +23,7 @@ module Sigh
23
23
  type_name = profile.class.pretty_type
24
24
  type_name = "AdHoc" if profile.is_adhoc?
25
25
 
26
- profile_name = "#{type_name}_#{profile.app.bundle_id}.mobileprovision" # default name
26
+ profile_name = "#{type_name}_#{profile.uuid}_#{profile.app.bundle_id}.mobileprovision" # default name
27
27
 
28
28
  output_path = File.join(Sigh.config[:output_path], profile_name)
29
29
  File.open(output_path, "wb") do |f|
@@ -132,13 +132,13 @@ module Sigh
132
132
  bundle_id: bundle_id,
133
133
  certificate: cert,
134
134
  mac: Sigh.config[:platform].to_s == 'macos',
135
- sub_platform: Sigh.config[:platform].to_s == 'tvos' ? 'tvos' : nil)
135
+ sub_platform: Sigh.config[:platform].to_s == 'tvos' ? 'tvOS' : nil)
136
136
  profile
137
137
  end
138
138
 
139
139
  def certificates_for_profile_and_platform
140
140
  case Sigh.config[:platform].to_s
141
- when 'ios'
141
+ when 'ios', 'tvos'
142
142
  if profile_type == Spaceship.provisioning_profile.Development
143
143
  certificates = Spaceship.certificate.development.all
144
144
  elsif profile_type == Spaceship.provisioning_profile.InHouse
@@ -33,7 +33,8 @@ module Snapshot
33
33
  short_option: "-X",
34
34
  env_name: "SNAPSHOT_XCARGS",
35
35
  description: "Pass additional arguments to xcodebuild for the test phase. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS=\"-ObjC -lstdc++\"",
36
- optional: true),
36
+ optional: true,
37
+ type: :shell_string),
37
38
  FastlaneCore::ConfigItem.new(key: :devices,
38
39
  description: "A list of devices you want to take the screenshots from",
39
40
  short_option: "-d",
@@ -51,6 +51,8 @@ module Snapshot
51
51
  UI.message("snapshot run #{current_run} of #{number_of_runs}")
52
52
 
53
53
  results[device][language] = run_for_device_and_language(language, locale, device, launch_arguments)
54
+
55
+ copy_simulator_logs(device, language, locale, launch_arguments)
54
56
  end
55
57
  end
56
58
  end
@@ -62,10 +64,6 @@ module Snapshot
62
64
  # Generate HTML report
63
65
  ReportsGenerator.new.generate
64
66
 
65
- if Snapshot.config[:output_simulator_logs]
66
- output_simulator_logs
67
- end
68
-
69
67
  # Clear the Derived Data
70
68
  unless Snapshot.config[:derived_data_path]
71
69
  FileUtils.rm_rf(TestCommandGenerator.derived_data_path)
@@ -100,15 +98,17 @@ module Snapshot
100
98
  end
101
99
  end
102
100
 
103
- def output_simulator_logs
104
- Snapshot.config[:devices].each do |device_name|
105
- device = TestCommandGenerator.find_device(device_name)
106
- sim_device_logfilepath_source = File.expand_path("~/Library/Logs/CoreSimulator/#{device.udid}/system.log")
107
- next unless File.exist?(sim_device_logfilepath_source)
101
+ def copy_simulator_logs(device_name, language, locale, launch_arguments)
102
+ return unless Snapshot.config[:output_simulator_logs]
108
103
 
109
- sim_device_logfilepath_dest = File.join(Snapshot.config[:output_directory], "#{device.name}_#{device.os_type}_#{device.os_version}_system.log")
110
- FileUtils.cp(sim_device_logfilepath_source, sim_device_logfilepath_dest)
111
- end
104
+ detected_language = locale || language
105
+ language_folder = File.join(Snapshot.config[:output_directory], detected_language)
106
+ device = TestCommandGenerator.find_device(device_name)
107
+ components = [launch_arguments].delete_if { |a| a.to_s.length == 0 }
108
+
109
+ UI.header("Collecting system logs #{device_name} - #{language}")
110
+ logarchive_dest = File.join(language_folder, "system_logs-" + Digest::MD5.hexdigest(components.join("-")) + ".logarchive")
111
+ FastlaneCore::Simulator.copy_logarchive(device, logarchive_dest)
112
112
  end
113
113
 
114
114
  def print_results(results)
@@ -27,6 +27,7 @@ module Spaceship
27
27
  AppSubmission = Spaceship::Tunes::AppSubmission
28
28
  Application = Spaceship::Tunes::Application
29
29
  Members = Spaceship::Tunes::Members
30
+ Persons = Spaceship::Portal::Persons
30
31
 
31
32
  DESCRIPTION = "Ruby library to access the Apple Dev Center and iTunes Connect".freeze
32
33
  end
@@ -217,20 +217,43 @@ module Spaceship
217
217
  #####################################################
218
218
 
219
219
  def inspect
220
- inspectables = self.attributes
220
+ # To avoid circular references, we keep track of the references
221
+ # of all objects already inspected from the first call to inspect
222
+ # in this call stack
223
+ # We use a Thread local storage for multi-thread friendliness
224
+ thread = Thread.current
225
+ tree_root = thread[:inspected_objects].nil?
226
+ thread[:inspected_objects] = Set.new if tree_root
221
227
 
222
- value = inspectables.map do |k|
228
+ if thread[:inspected_objects].include? self
229
+ # already inspected objects have a default value,
230
+ # let's follow Ruby's convention for circular references
231
+ value = "#<Object ...>"
232
+ else
233
+ thread[:inspected_objects].add self
234
+ begin
235
+ value = inspect_value
236
+ ensure
237
+ thread[:inspected_objects] = nil if tree_root
238
+ end
239
+ end
240
+
241
+ "<#{self.class.name} \n#{value}>"
242
+ end
243
+
244
+ def inspect_value
245
+ self.attributes.map do |k|
223
246
  v = self.send(k).inspect
224
247
  v.gsub!("\n", "\n\t") # to align nested elements
225
248
 
226
249
  "\t#{k}=#{v}"
227
250
  end.join(", \n")
228
-
229
- "<#{self.class.name} \n#{value}>"
230
251
  end
231
252
 
232
253
  def to_s
233
254
  self.inspect
234
255
  end
256
+
257
+ private :inspect_value
235
258
  end
236
259
  end