run_loop 1.5.1 → 1.5.2

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.
@@ -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)