run_loop 1.5.1 → 1.5.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -103,9 +103,9 @@ Please update your sources.))
103
103
  # @!visibility private
104
104
  def to_s
105
105
  if simulator?
106
- "#<Simulator: #{name} #{udid} #{instruction_set}>"
106
+ "#<Simulator: #{name} (#{version.to_s}) #{udid} #{instruction_set}>"
107
107
  else
108
- "#<Device: #{name} #{udid}>"
108
+ "#<Device: #{name} (#{version.to_s}) #{udid}>"
109
109
  end
110
110
  end
111
111
 
@@ -188,6 +188,28 @@ Please update your sources to pass an instance of RunLoop::Xcode))
188
188
  end
189
189
  end
190
190
 
191
+ # @!visibility private
192
+ # The device `state` is reported by the simctl tool.
193
+ #
194
+ # The expected values from simctl are:
195
+ #
196
+ # * Booted
197
+ # * Shutdown
198
+ # * Shutting Down
199
+ #
200
+ # To handle exceptional cases, there are these two additional states:
201
+ #
202
+ # * Unavailable # Should never occur
203
+ # * Unknown # A stub for future changes
204
+ def update_simulator_state
205
+ if physical_device?
206
+ raise RuntimeError, 'This method is available only for simulators'
207
+ end
208
+
209
+ @state = fetch_simulator_state
210
+ end
211
+
212
+ # @!visibility private
191
213
  def simulator_root_dir
192
214
  @simulator_root_dir ||= lambda {
193
215
  return nil if physical_device?
@@ -195,6 +217,7 @@ Please update your sources to pass an instance of RunLoop::Xcode))
195
217
  }.call
196
218
  end
197
219
 
220
+ # @!visibility private
198
221
  def simulator_accessibility_plist_path
199
222
  @simulator_accessibility_plist_path ||= lambda {
200
223
  return nil if physical_device?
@@ -202,6 +225,7 @@ Please update your sources to pass an instance of RunLoop::Xcode))
202
225
  }.call
203
226
  end
204
227
 
228
+ # @!visibility private
205
229
  def simulator_preferences_plist_path
206
230
  @simulator_preferences_plist_path ||= lambda {
207
231
  return nil if physical_device?
@@ -209,6 +233,7 @@ Please update your sources to pass an instance of RunLoop::Xcode))
209
233
  }.call
210
234
  end
211
235
 
236
+ # @!visibility private
212
237
  def simulator_log_file_path
213
238
  @simulator_log_file_path ||= lambda {
214
239
  return nil if physical_device?
@@ -216,9 +241,200 @@ Please update your sources to pass an instance of RunLoop::Xcode))
216
241
  }.call
217
242
  end
218
243
 
244
+ # @!visibility private
245
+ def simulator_device_plist
246
+ @simulator_device_plist ||= lambda do
247
+ return nil if physical_device?
248
+ File.join(simulator_root_dir, 'device.plist')
249
+ end.call
250
+ end
251
+
252
+ # @!visibility private
253
+ # Is this the first launch of this Simulator?
254
+ #
255
+ # TODO Needs unit and integration tests.
256
+ def simulator_first_launch?
257
+ megabytes = simulator_data_dir_size
258
+
259
+ if version >= RunLoop::Version.new('9.0')
260
+ megabytes < 20
261
+ elsif version >= RunLoop::Version.new('8.0')
262
+ megabytes < 12
263
+ else
264
+ megabytes < 8
265
+ end
266
+ end
267
+
268
+ # @!visibility private
269
+ # The size of the simulator data/ directory.
270
+ #
271
+ # TODO needs unit tests.
272
+ def simulator_data_dir_size
273
+ path = File.join(simulator_root_dir, 'data')
274
+ args = ['du', '-m', '-d', '0', path]
275
+ hash = xcrun.exec(args)
276
+ hash[:out].split(' ').first.to_i
277
+ end
278
+
279
+ # @!visibility private
280
+ #
281
+ # Waits for three conditions:
282
+ #
283
+ # 1. The SHA sum of the simulator data/ directory to be stable.
284
+ # 2. No more log messages are begin generated
285
+ # 3. 1 and 2 must hold for 2 seconds.
286
+ #
287
+ # When the simulator version is >= iOS 9 _and_ it is the first launch of
288
+ # the simulator after a reset or a new simulator install, a fourth condition
289
+ # is added:
290
+ #
291
+ # 4. The first three conditions must be met a second time.
292
+ def simulator_wait_for_stable_state
293
+ require 'securerandom'
294
+
295
+ quiet_time = 2
296
+ delay = 0.5
297
+
298
+ first_launch = false
299
+
300
+ if version >= RunLoop::Version.new('9.0')
301
+ first_launch = simulator_data_dir_size < 20
302
+ end
303
+
304
+ now = Time.now
305
+ timeout = 30
306
+ poll_until = now + timeout
307
+ quiet = now + quiet_time
308
+
309
+ is_stable = false
310
+
311
+ path = File.join(simulator_root_dir, 'data')
312
+ current_sha = nil
313
+ sha_fn = lambda do |data_dir|
314
+ begin
315
+ # Directory.directory_digest has a blocking read. Typically, it
316
+ # returns in < 0.3 seconds.
317
+ Timeout.timeout(2, TimeoutError) do
318
+ RunLoop::Directory.directory_digest(data_dir)
319
+ end
320
+ rescue => e
321
+ RunLoop.log_error(e) if RunLoop::Environment.debug?
322
+ SecureRandom.uuid
323
+ end
324
+ end
325
+
326
+ current_line = nil
327
+
328
+ while Time.now < poll_until do
329
+ latest_sha = sha_fn.call(path)
330
+ latest_line = last_line_from_simulator_log_file
331
+
332
+ is_stable = current_sha == latest_sha && current_line == latest_line
333
+
334
+ if is_stable
335
+ if Time.now > quiet
336
+ if first_launch
337
+ RunLoop.log_debug('First launch detected - allowing additional time to stabilize')
338
+ first_launch = false
339
+ sleep 1.2
340
+ quiet = Time.now + quiet_time
341
+ else
342
+ break
343
+ end
344
+ else
345
+ quiet = Time.now + quiet_time
346
+ end
347
+ end
348
+
349
+ current_sha = latest_sha
350
+ current_line = latest_line
351
+ sleep delay
352
+ end
353
+
354
+ if is_stable
355
+ elapsed = Time.now - now
356
+ stabilized = elapsed - quiet_time
357
+ RunLoop.log_debug("Simulator stable after #{stabilized} seconds")
358
+ RunLoop.log_debug("Waited a total of #{elapsed} seconds for simulator to stabilize")
359
+ else
360
+ RunLoop.log_debug("Timed out: simulator not stable after #{timeout} seconds")
361
+ end
362
+ end
363
+
219
364
  private
220
365
 
366
+ # @!visibility private
367
+ # TODO write a unit test.
368
+ def last_line_from_simulator_log_file
369
+ file = simulator_log_file_path
370
+
371
+ return nil if !File.exist?(file)
372
+
373
+ debug = RunLoop::Environment.debug?
374
+
375
+ begin
376
+ io = File.open(file, 'r')
377
+ io.seek(-100, IO::SEEK_END)
378
+
379
+ line = io.readline
380
+ rescue StandardError => e
381
+ RunLoop.log_error("Caught #{e} while reading simulator log file") if debug
382
+ ensure
383
+ io.close if io && !io.closed?
384
+ end
385
+
386
+ line
387
+ end
388
+
389
+ # @!visibility private
390
+ def xcrun
391
+ RunLoop::Xcrun.new
392
+ end
393
+
394
+ # @!visibility private
395
+ def detect_state_from_line(line)
396
+
397
+ if line[/unavailable/, 0]
398
+ RunLoop.log_debug("Simulator state is unavailable: #{line}")
399
+ return 'Unavailable'
400
+ end
401
+
402
+ state = line[/(Booted|Shutdown|Shutting Down)/,0]
403
+
404
+ if state.nil?
405
+ RunLoop.log_debug("Simulator state is unknown: #{line}")
406
+ 'Unknown'
407
+ else
408
+ state
409
+ end
410
+ end
411
+
412
+ # @!visibility private
413
+ def fetch_simulator_state
414
+ if physical_device?
415
+ raise RuntimeError, 'This method is available only for simulators'
416
+ end
417
+
418
+ args = ['simctl', 'list', 'devices']
419
+ hash = xcrun.exec(args)
420
+ out = hash[:out]
421
+
422
+ matched_line = out.split("\n").find do |line|
423
+ line.include?(udid)
424
+ end
425
+
426
+ if matched_line.nil?
427
+ raise RuntimeError,
428
+ "Expected a simulator with udid '#{udid}', but found none"
429
+ end
430
+
431
+ detect_state_from_line(matched_line)
432
+ end
433
+
434
+ # @!visibility private
221
435
  CORE_SIMULATOR_DEVICE_DIR = File.expand_path('~/Library/Developer/CoreSimulator/Devices')
436
+
437
+ # @!visibility private
222
438
  CORE_SIMULATOR_LOGS_DIR = File.expand_path('~/Library/Logs/CoreSimulator')
223
439
 
224
440
  # TODO Is this a good idea? It speeds up rspec tests by a factor of ~2x...
@@ -1,10 +1,10 @@
1
- require 'digest'
2
- require 'openssl'
3
-
4
1
  module RunLoop
5
2
 
6
3
  # Class for performing operations on directories.
7
4
  class Directory
5
+ require 'digest'
6
+ require 'openssl'
7
+ require 'pathname'
8
8
 
9
9
  # Dir.glob ignores files that start with '.', but we often need to find
10
10
  # dotted files and directories.
@@ -39,10 +39,27 @@ module RunLoop
39
39
  raise ArgumentError, "Expected a non-empty dir at '#{path}' found '#{entries}'"
40
40
  end
41
41
 
42
+ debug = RunLoop::Environment.debug?
43
+
42
44
  sha = OpenSSL::Digest::SHA256.new
43
45
  self.recursive_glob_for_entries(path).each do |file|
44
- unless File.directory?(file)
45
- sha << File.read(file)
46
+ if File.directory?(file)
47
+ # skip directories
48
+ elsif !Pathname.new(file).exist?
49
+ # skip broken symlinks
50
+ else
51
+ case File.ftype(file)
52
+ when 'fifo'
53
+ RunLoop.log_warn("SHA1 SKIPPING FIFO #{file}") if debug
54
+ when 'socket'
55
+ RunLoop.log_warn("SHA1 SKIPPING SOCKET #{file}") if debug
56
+ when 'characterSpecial'
57
+ RunLoop.log_warn("SHA1 SKIPPING CHAR SPECIAL #{file}") if debug
58
+ when 'blockSpecial'
59
+ RunLoop.log_warn("SHA1 SKIPPING BLOCK SPECIAL #{file}") if debug
60
+ else
61
+ sha << File.read(file)
62
+ end
46
63
  end
47
64
  end
48
65
  sha.hexdigest
@@ -95,5 +95,15 @@ module RunLoop
95
95
  float
96
96
  end
97
97
  end
98
+
99
+ def self.with_debugging(&block)
100
+ original_value = ENV['DEBUG']
101
+ ENV['DEBUG'] = '1'
102
+ begin
103
+ block.call
104
+ ensure
105
+ ENV['DEBUG'] = original_value
106
+ end
107
+ end
98
108
  end
99
109
  end
@@ -13,6 +13,10 @@ module RunLoop
13
13
  @xcode ||= RunLoop::Xcode.new
14
14
  end
15
15
 
16
+ def xcrun
17
+ RunLoop::Xcrun.new
18
+ end
19
+
16
20
  # @!visibility private
17
21
  def to_s
18
22
  "#<Instruments #{version.to_s}>"
@@ -106,10 +110,10 @@ Please update your sources to pass an instance of RunLoop::Xcode))
106
110
  # @return [RunLoop::Version] A version object.
107
111
  def version
108
112
  @instruments_version ||= lambda do
109
- execute_command([]) do |_, stderr, _|
110
- version_str = stderr.read[VERSION_REGEX, 0]
111
- RunLoop::Version.new(version_str)
112
- end
113
+ args = ['instruments']
114
+ hash = xcrun.exec(args, log_cmd: true)
115
+ version_str = hash[:err][VERSION_REGEX, 0]
116
+ RunLoop::Version.new(version_str)
113
117
  end.call
114
118
  end
115
119
 
@@ -129,54 +133,41 @@ Please update your sources to pass an instance of RunLoop::Xcode))
129
133
  # @return [Array<String>] Instruments.app templates.
130
134
  def templates
131
135
  @instruments_templates ||= lambda do
136
+ args = ['instruments', '-s', 'templates']
137
+ hash = xcrun.exec(args, log_cmd: true)
132
138
  if xcode.version_gte_6?
133
- execute_command(['-s', 'templates']) do |stdout, stderr, _|
134
- filter_stderr_spam(stderr)
135
- stdout.read.chomp.split("\n").map do |elm|
136
- stripped = elm.strip.tr('"', '')
137
- if stripped == '' || stripped == 'Known Templates:'
138
- nil
139
- else
140
- stripped
141
- end
142
- end.compact
143
- end
144
- elsif xcode.version_gte_51?
145
- execute_command(['-s', 'templates']) do |stdout, stderr, _|
146
- err = stderr.read
147
- if !err.nil? || err != ''
148
- $stderr.puts stderr.read
139
+ hash[:out].chomp.split("\n").map do |elm|
140
+ stripped = elm.strip.tr('"', '')
141
+ if stripped == '' || stripped == 'Known Templates:'
142
+ nil
143
+ else
144
+ stripped
149
145
  end
150
-
151
- stdout.read.strip.split("\n").delete_if do |path|
152
- not path =~ /tracetemplate/
153
- end.map { |elm| elm.strip }
154
- end
146
+ end.compact
155
147
  else
156
- raise "Xcode version '#{xcode.version}' is not supported."
148
+ hash[:out].strip.split("\n").delete_if do |path|
149
+ not path =~ /tracetemplate/
150
+ end.map { |elm| elm.strip }
157
151
  end
158
152
  end.call
159
153
  end
160
154
 
161
- # Returns an array the available physical devices.
155
+ # Returns an array of the available physical devices.
162
156
  #
163
157
  # @return [Array<RunLoop::Device>] All the devices will be physical
164
158
  # devices.
165
159
  def physical_devices
166
160
  @instruments_physical_devices ||= lambda do
167
- execute_command(['-s', 'devices']) do |stdout, stderr, _|
168
- filter_stderr_spam(stderr)
169
- all = stdout.read.chomp.split("\n")
170
- valid = all.select do |device|
171
- device =~ DEVICE_UDID_REGEX
172
- end
173
- valid.map do |device|
174
- udid = device[DEVICE_UDID_REGEX, 0]
175
- version = device[VERSION_REGEX, 0]
176
- name = device.split('(').first.strip
161
+ fetch_devices[:out].chomp.split("\n").map do |line|
162
+ udid = line[DEVICE_UDID_REGEX, 0]
163
+ if udid
164
+ version = line[VERSION_REGEX, 0]
165
+ name = line.split('(').first.strip
177
166
  RunLoop::Device.new(name, version, udid)
167
+ else
168
+ nil
178
169
  end
179
- end
170
+ end.compact
180
171
  end.call
181
172
  end
182
173
 
@@ -195,35 +186,39 @@ Please update your sources to pass an instance of RunLoop::Xcode))
195
186
  # @return [Array<RunLoop::Device>] All the devices will be simulators.
196
187
  def simulators
197
188
  @instruments_simulators ||= lambda do
198
- execute_command(['-s', 'devices']) do |stdout, stderr, _|
199
- filter_stderr_spam(stderr)
200
- lines = stdout.read.chomp.split("\n")
201
- lines.map do |line|
202
- stripped = line.strip
203
- if line_is_simulator?(stripped) &&
204
- !line_is_simulator_paired_with_watch?(stripped)
205
-
206
- version = stripped[VERSION_REGEX, 0]
207
-
208
- if line_is_xcode5_simulator?(stripped)
209
- name = line
210
- udid = line
211
- else
212
- name = stripped.split('(').first.strip
213
- udid = line[CORE_SIMULATOR_UDID_REGEX, 0]
214
- end
215
-
216
- RunLoop::Device.new(name, version, udid)
189
+ fetch_devices[:out].chomp.split("\n").map do |line|
190
+ stripped = line.strip
191
+ if line_is_simulator?(stripped) &&
192
+ !line_is_simulator_paired_with_watch?(stripped)
193
+
194
+ version = stripped[VERSION_REGEX, 0]
195
+
196
+ if line_is_xcode5_simulator?(stripped)
197
+ name = line
198
+ udid = line
217
199
  else
218
- nil
200
+ name = stripped.split('(').first.strip
201
+ udid = line[CORE_SIMULATOR_UDID_REGEX, 0]
219
202
  end
220
- end.compact
221
- end
203
+
204
+ RunLoop::Device.new(name, version, udid)
205
+ else
206
+ nil
207
+ end
208
+ end.compact
222
209
  end.call
223
210
  end
224
211
 
225
212
  private
226
213
 
214
+ # @!visibility private
215
+ def fetch_devices
216
+ @device_hash ||= lambda do
217
+ args = ['instruments', '-s', 'devices']
218
+ xcrun.exec(args, log_cmd: true)
219
+ end.call
220
+ end
221
+
227
222
  # @!visibility private
228
223
  #
229
224
  # ```
@@ -354,18 +349,6 @@ Please update your sources to pass an instance of RunLoop::Xcode))
354
349
  end
355
350
  end
356
351
 
357
- # @!visibility private
358
- #
359
- # Filters `instruments` spam.
360
- def filter_stderr_spam(stderr)
361
- # Xcode 6 GM is spamming "WebKit Threading Violations"
362
- stderr.read.strip.split("\n").each do |line|
363
- unless line[/WebKit Threading Violation/, 0]
364
- $stderr.puts line
365
- end
366
- end
367
- end
368
-
369
352
  # @!visibility private
370
353
  def line_is_simulator?(line)
371
354
  line_is_core_simulator?(line) || line_is_xcode5_simulator?(line)