]> git.saurik.com Git - apple/configd.git/blob - sctest/SCTestInterfaceNamer.m
configd-1109.40.9.tar.gz
[apple/configd.git] / sctest / SCTestInterfaceNamer.m
1 /*
2 * Copyright (c) 2020 Apple Inc. All rights reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
11 * file.
12 *
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #import "SCTest.h"
25 #import "SCTestUtils.h"
26
27 #include <net/ethernet.h>
28 #include <IOKit/IOKitLib.h>
29 #include <IOKit/IOBSD.h>
30 #include <IOKit/network/IONetworkController.h>
31 #include <IOKit/network/IOUserEthernetController.h>
32 #include <IOKit/storage/IOStorageDeviceCharacteristics.h>
33 #include <IOKit/usb/USB.h>
34 #include "plugin_shared.h"
35
36 #define INTERFACES_KEY @"State:/Network/Interface"
37 #define PLUGIN_INTERFACE_NAMER_KEY @"Plugin:InterfaceNamer"
38
39 #define WAIT_TIME (30 * NSEC_PER_SEC)
40
41
42 @interface SCTestInterfaceNamer : SCTest
43 @property NSArray *interfaces_all;
44 @property NSArray *interfaces_preconfigured;
45 @property dispatch_queue_t queue;
46 @property dispatch_semaphore_t sem_interfaces_all;
47 @property dispatch_semaphore_t sem_interfaces_preconfigured;
48 @property SCDynamicStoreRef store;
49 @end
50
51 @implementation SCTestInterfaceNamer
52
53 + (NSString *)command
54 {
55 return @"InterfaceNamer";
56 }
57
58 + (NSString *)commandDescription
59 {
60 return @"Tests the InterfaceNamer.bundle code paths";
61 }
62
63 static void
64 storeCallback(SCDynamicStoreRef store, CFArrayRef changedKeys, void *info)
65 {
66 @autoreleasepool {
67 NSDictionary *dict;
68 NSArray *interfaces;
69 SCTestInterfaceNamer *test = (__bridge SCTestInterfaceNamer *)info;
70
71 if ([(__bridge NSArray *)changedKeys containsObject:INTERFACES_KEY]) {
72 // copy list of interfaces
73 dict = (__bridge_transfer NSDictionary *)SCDynamicStoreCopyValue(store, (CFStringRef)INTERFACES_KEY);
74 interfaces = [dict objectForKey:(__bridge NSString *)kSCPropNetInterfaces];
75 if (!_SC_CFEqual((__bridge CFArrayRef)interfaces, (__bridge CFArrayRef)test.interfaces_all)) {
76 test.interfaces_all = interfaces;
77 dispatch_semaphore_signal(test.sem_interfaces_all);
78 }
79 }
80
81 if ([(__bridge NSArray *)changedKeys containsObject:PLUGIN_INTERFACE_NAMER_KEY]) {
82 // copy list of [pre-configured] interfaces
83 dict = (__bridge_transfer NSDictionary *)SCDynamicStoreCopyValue(store, (CFStringRef)PLUGIN_INTERFACE_NAMER_KEY);
84 interfaces = [dict objectForKey:@"_PreConfigured_"];
85 if (!_SC_CFEqual((__bridge CFArrayRef)interfaces, (__bridge CFArrayRef)test.interfaces_preconfigured)) {
86 test.interfaces_preconfigured = interfaces;
87 dispatch_semaphore_signal(test.sem_interfaces_preconfigured);
88 }
89 }
90 }
91
92 return;
93 }
94
95 - (instancetype)initWithOptions:(NSDictionary *)options
96 {
97 self = [super initWithOptions:options];
98 // if (self) {
99 // }
100 return self;
101 }
102
103 - (void)dealloc
104 {
105 [self watchForChanges:FALSE];
106 }
107
108 - (void)start
109 {
110 [self cleanupAndExitWithErrorCode:0];
111 }
112
113 - (void)cleanupAndExitWithErrorCode:(int)error
114 {
115 [super cleanupAndExitWithErrorCode:error];
116 }
117
118 - (BOOL)setup
119 {
120 return YES;
121 }
122
123 - (BOOL)unitTest
124 {
125 if(![self setup]) {
126 return NO;
127 }
128
129 BOOL allUnitTestsPassed = YES;
130 allUnitTestsPassed &= [self unitTestInsertRemoveOneInterface];
131 allUnitTestsPassed &= [self unitTestInsertRemoveMultipleInterfaces];
132 allUnitTestsPassed &= [self unitTestCheckIOKitQuiet];
133 allUnitTestsPassed &= [self unitTestCheckEN0];
134
135 if(![self tearDown]) {
136 return NO;
137 }
138
139 return allUnitTestsPassed;
140 }
141
142 - (BOOL)tearDown
143 {
144 return YES;
145 }
146
147 - (void)watchForChanges:(BOOL)enable
148 {
149 if (enable) {
150 SCDynamicStoreContext context = {0, NULL, CFRetain, CFRelease, NULL};
151 Boolean ok;
152
153 self.queue = dispatch_queue_create("SCTestInterfaceNamer callback queue", NULL);
154
155 self.sem_interfaces_all = dispatch_semaphore_create(0);
156
157 self.sem_interfaces_preconfigured = dispatch_semaphore_create(0);
158
159 context.info = (__bridge void * _Nullable)self;
160 self.store = SCDynamicStoreCreate(kCFAllocatorDefault, CFSTR("SCTest"), storeCallback, &context);
161
162 ok = SCDynamicStoreSetNotificationKeys(self.store,
163 (__bridge CFArrayRef)@[INTERFACES_KEY,
164 PLUGIN_INTERFACE_NAMER_KEY],
165 NULL);
166 assert(ok);
167
168 ok = SCDynamicStoreSetDispatchQueue(self.store, self.queue);
169 assert(ok);
170 } else {
171 if (self.store != NULL) {
172 (void)SCDynamicStoreSetDispatchQueue(self.store, NULL);
173 CFRelease(self.store);
174 self.store = NULL;
175 }
176
177 self.queue = NULL;
178 self.sem_interfaces_all = NULL;
179 self.sem_interfaces_preconfigured = NULL;
180 }
181 }
182
183 - (BOOL)isPreconfiguredInterface:(NSString *)bsdName
184 {
185 return [self.interfaces_preconfigured containsObject:bsdName];;
186 }
187
188 #pragma mark -
189 #pragma mark IOEthernetController support
190
191 static CFStringRef
192 copy_interface_name(IOEthernetControllerRef controller)
193 {
194 CFStringRef bsdName;
195 io_object_t interface;
196
197 interface = IOEthernetControllerGetIONetworkInterfaceObject(controller);
198 if (interface == MACH_PORT_NULL) {
199 SCTestLog("*** could not get interface for controller");
200 return NULL;
201 }
202
203 bsdName = IORegistryEntryCreateCFProperty(interface, CFSTR(kIOBSDNameKey), NULL, kNilOptions);
204 if (bsdName == NULL) {
205 SCTestLog("*** IOEthernetController with no BSD interface name");
206 return NULL;
207 }
208
209 return bsdName;
210 }
211
212 static CFDataRef
213 copy_interface_mac(IOEthernetControllerRef controller)
214 {
215 CFDataRef macAddress;
216 io_object_t interface;
217
218 interface = IOEthernetControllerGetIONetworkInterfaceObject(controller);
219 if (interface == MACH_PORT_NULL) {
220 SCTestLog("*** could not get interface for controller");
221 return NULL;
222 }
223
224 macAddress = IORegistryEntrySearchCFProperty(interface,
225 kIOServicePlane,
226 CFSTR(kIOMACAddress),
227 NULL,
228 kIORegistryIterateRecursively | kIORegistryIterateParents);
229 if (macAddress == NULL) {
230 SCTestLog("*** IOEthernetController with no BSD interface name");
231 return NULL;
232 }
233
234 return macAddress;
235 }
236
237 static IOEthernetControllerRef
238 create_hidden_interface(u_char ea_unique)
239 {
240 IOEthernetControllerRef controller;
241 CFDataRef data;
242 struct ether_addr ea = { .octet = { 0x02, 'F', 'A', 'K', 'E', ea_unique } };
243 CFMutableDictionaryRef merge;
244 CFNumberRef num;
245 CFMutableDictionaryRef props;
246 const int usb_vid_apple = kIOUSBAppleVendorID;
247
248 props = CFDictionaryCreateMutable(NULL, 0,
249 &kCFTypeDictionaryKeyCallBacks,
250 &kCFTypeDictionaryValueCallBacks);
251
252 data = CFDataCreate(NULL, ea.octet, ETHER_ADDR_LEN);
253 CFDictionarySetValue(props, kIOEthernetHardwareAddress, data);
254 CFRelease(data);
255
256 merge = CFDictionaryCreateMutable(NULL, 0,
257 &kCFTypeDictionaryKeyCallBacks,
258 &kCFTypeDictionaryValueCallBacks);
259 CFDictionarySetValue(merge, CFSTR(kIOPropertyProductNameKey), CFSTR("Hidden Ethernet"));
260 CFDictionarySetValue(merge, kIOUserEthernetInterfaceRole, CFSTR("hidden-ethernet"));
261 CFDictionarySetValue(merge, kSCNetworkInterfaceHiddenConfigurationKey, kCFBooleanTrue);
262 num = CFNumberCreate(NULL, kCFNumberIntType, &usb_vid_apple);
263 CFDictionarySetValue(merge, CFSTR(kUSBVendorID), num);
264 CFRelease(num);
265 CFDictionarySetValue(props, kIOUserEthernetInterfaceMergeProperties, merge);
266 CFRelease(merge);
267
268 controller = IOEthernetControllerCreate(NULL, props);
269 CFRelease(props);
270 if (controller == NULL) {
271 SCTestLog("*** could not create ethernet controller for \"%s\"", ether_ntoa(&ea));
272 return NULL;
273 }
274
275 return controller;
276 }
277
278 - (BOOL)interfaceAdd:(u_char)ea_unique controller:(IOEthernetControllerRef *)newController
279 {
280 BOOL ok = FALSE;
281
282 do {
283 NSString *bsdName;
284 NSData *macAddress;
285 long status;
286
287 // add an interface
288 *newController = create_hidden_interface(ea_unique);
289 if (*newController == NULL) {
290 SCTestLog("*** could not create controller");
291 break;
292 }
293
294 // wait for the [BSD] interface to show up
295 status = dispatch_semaphore_wait(self.sem_interfaces_all, dispatch_time(DISPATCH_TIME_NOW, WAIT_TIME));
296 if (status != 0) {
297 // if timeout
298 SCTestLog("*** no KernelEventMonitor change posted, interface not? created");
299 break;
300 }
301
302 bsdName = (__bridge_transfer NSString *)copy_interface_name(*newController);
303 if (bsdName == NULL) {
304 break;
305 }
306
307 macAddress = (__bridge_transfer NSData *)copy_interface_mac(*newController);
308 if (macAddress == NULL) {
309 break;
310 }
311
312 SCTestLog(" Interface \"%@\" added, mac=%s",
313 bsdName,
314 ether_ntoa((struct ether_addr *)macAddress.bytes));
315
316 // check if pre-configured
317 status = dispatch_semaphore_wait(self.sem_interfaces_preconfigured, dispatch_time(DISPATCH_TIME_NOW, WAIT_TIME));
318 if (status != 0) {
319 // if timeout
320 SCTestLog("*** no InterfaceNamer change posted, not? preconfigured");
321 break;
322 }
323
324 if (![self isPreconfiguredInterface:bsdName]) {
325 SCTestLog("*** Interface \"%@\" is not pre-configured", bsdName);
326 break;
327 }
328
329 SCTestLog(" Interface \"%@\" is pre-configured", bsdName);
330
331 ok = TRUE;
332 } while (0);
333
334 if (!ok) {
335 if (*newController != NULL) {
336 CFRelease(*newController);
337 *newController = NULL;
338 }
339 }
340
341 return ok;
342 }
343
344 - (BOOL)interfaceRemove:(IOEthernetControllerRef)controller
345 {
346 BOOL ok = FALSE;
347
348 do {
349 NSString *bsdName = (__bridge_transfer NSString *)copy_interface_name(controller);
350 long status;
351
352 // remove the interface
353 CFRelease(controller);
354
355 // wait for the [BSD] interface to go away
356 status = dispatch_semaphore_wait(self.sem_interfaces_all, dispatch_time(DISPATCH_TIME_NOW, WAIT_TIME));
357 if (status != 0) {
358 // if timeout
359 SCTestLog("*** no KernelEventMonitor change posted, interface not? removed");
360 break;
361 }
362
363 SCTestLog(" Interface \"%@\" removed", bsdName);
364
365 // check if [still] pre-configured
366 status = dispatch_semaphore_wait(self.sem_interfaces_preconfigured, dispatch_time(DISPATCH_TIME_NOW, WAIT_TIME));
367 if (status != 0) {
368 // if timeout
369 SCTestLog("*** no InterfaceNamer change posted, still? preconfigured");
370 break;
371 }
372
373 if ([self isPreconfiguredInterface:bsdName]) {
374 SCTestLog("*** interface \"%@\" is still pre-configured", bsdName);
375 break;
376 }
377
378 ok = TRUE;
379 } while (0);
380
381 return ok;
382 }
383
384 #pragma mark -
385 #pragma mark unitTestInsertRemoveOneInterface
386
387 #define N_INTERFACES 5
388
389 - (BOOL)unitTestInsertRemoveOneInterface
390 {
391 NSString *bsdName1 = nil;
392 BOOL ok;
393 SCTestInterfaceNamer *test;
394
395 test = [[SCTestInterfaceNamer alloc] initWithOptions:self.options];
396 [test watchForChanges:TRUE];
397
398 for (size_t i = 0; i < N_INTERFACES; i++) {
399 NSString *bsdName;
400 IOEthernetControllerRef controller;
401
402 SCTestLog("Interface #%zd", i + 1);
403
404 // add an interface
405 ok = [test interfaceAdd:i controller:&controller];
406 if (!ok) {
407 break;
408 }
409
410 // check the assigned interface name
411 bsdName = (__bridge_transfer NSString *)copy_interface_name(controller);
412 if (i == 0) {
413 bsdName1 = bsdName;
414 } else if ([bsdName isNotEqualTo:bsdName1]) {
415 SCTestLog("*** interface name not re-assigned, expected \"%@\", assigned \"%@\"", bsdName1, bsdName);
416 break;
417 }
418
419 // remove the interface
420 ok = [test interfaceRemove:controller];
421 if (!ok) {
422 break;
423 }
424 };
425
426 if (ok) {
427 SCTestLog("Successfully completed unitTestInsertRemoveOneInterface unit test");
428 }
429
430 return ok;
431 }
432
433 #pragma mark -
434 #pragma mark unitTestInsertRemoveMultipleInterfaces
435
436 #define N_INTERFACES_SLOT_X (N_INTERFACES / 2)
437
438 - (BOOL)unitTestInsertRemoveMultipleInterfaces
439 {
440 NSString *bsdName;
441 IOEthernetControllerRef controller;
442 struct {
443 NSString *bsdName;
444 IOEthernetControllerRef controller;
445 } interfaces[N_INTERFACES] = { { } };
446 BOOL ok;
447 SCTestInterfaceNamer *test;
448
449 test = [[SCTestInterfaceNamer alloc] initWithOptions:self.options];
450 [test watchForChanges:TRUE];
451
452 SCTestLog("Adding %d interfaces", N_INTERFACES);
453
454 for (size_t i = 0; i < N_INTERFACES; i++) {
455 // add an interface
456 ok = [test interfaceAdd:i controller:&controller];
457 if (!ok) {
458 break;
459 }
460
461 interfaces[i].controller = controller;
462 interfaces[i].bsdName = (__bridge_transfer NSString *)copy_interface_name(controller);
463 }
464
465 if (ok) {
466 SCTestLog("Removing interfaces");
467
468 for (size_t i = 0; i < N_INTERFACES; i++) {
469 // remove the interface
470 ok = [test interfaceRemove:interfaces[i].controller];
471 interfaces[i].controller = NULL;
472 if (!ok) {
473 break;
474 }
475 }
476 }
477
478 if (ok) {
479 SCTestLog("Re-adding %d interfaces", N_INTERFACES);
480
481 for (size_t i = 0; i < N_INTERFACES; i++) {
482 // add an interface
483 ok = [test interfaceAdd:i controller:&controller];
484 if (!ok) {
485 break;
486 }
487
488 interfaces[i].controller = controller;
489
490 bsdName = (__bridge_transfer NSString *)copy_interface_name(controller);
491 ok = [bsdName isEqualTo:interfaces[i].bsdName];
492 if (!ok) {
493 SCTestLog("*** interface %zd not assigned the same name, expected \"%@\", assigned \"%@\"",
494 i,
495 interfaces[i].bsdName,
496 bsdName);
497 break;
498 }
499
500 interfaces[i].bsdName = bsdName;
501 }
502 }
503
504 if (ok) {
505 SCTestLog("Removing one interface");
506
507 ok = [test interfaceRemove:interfaces[N_INTERFACES_SLOT_X].controller];
508 interfaces[N_INTERFACES_SLOT_X].controller = NULL;
509 }
510
511 if (ok) {
512 SCTestLog("Adding a new interface");
513
514 do {
515 // add a new interface (should re-use the first available name)
516 ok = [test interfaceAdd:N_INTERFACES controller:&controller];
517 if (!ok) {
518 break;
519 }
520
521 interfaces[N_INTERFACES_SLOT_X].controller = controller;
522
523 bsdName = (__bridge_transfer NSString *)copy_interface_name(controller);
524 ok = [bsdName isEqualTo:interfaces[N_INTERFACES_SLOT_X].bsdName];
525 if (!ok) {
526 SCTestLog("*** interface %d not assigned an old name, expected \"%@\", assigned \"%@\"",
527 N_INTERFACES,
528 interfaces[N_INTERFACES_SLOT_X].bsdName,
529 bsdName);
530 break;
531 }
532
533 interfaces[N_INTERFACES_SLOT_X].bsdName = bsdName;
534 } while (0);
535 }
536
537 if (ok) {
538 SCTestLog("Removing interfaces (again)");
539
540 for (size_t i = 0; i < N_INTERFACES; i++) {
541 // remove the interface
542 ok = [test interfaceRemove:interfaces[i].controller];
543 interfaces[i].controller = NULL;
544 if (!ok) {
545 break;
546 }
547 }
548 }
549
550 // cleanup
551 for (size_t i = 0; i < N_INTERFACES; i++) {
552 if (interfaces[i].controller != NULL) {
553 CFRelease(interfaces[i].controller);
554 }
555 }
556
557 return ok;
558 }
559
560 #pragma mark -
561 #pragma mark unitTestCheckIOKitQuiet
562
563 - (BOOL)unitTestCheckIOKitQuiet
564 {
565 CFDictionaryRef dict;
566 const char *err = NULL;
567 Boolean hasNamer = FALSE;
568 Boolean hasQuiet = FALSE;
569 Boolean hasTimeout = FALSE;
570 CFArrayRef interfaces;
571 CFStringRef key;
572 BOOL ok = FALSE;
573
574 SCTestLog("Checking IOKit quiet");
575
576 // first off, we need to wait for *QUIET* or *TIMEOUT*
577 interfaces = SCNetworkInterfaceCopyAll();
578 if (interfaces != NULL) {
579 CFRelease(interfaces);
580 }
581
582 // now, we check the configd/InterfaceNamer status
583 key = SCDynamicStoreKeyCreate(NULL, CFSTR("%@" "InterfaceNamer"), kSCDynamicStoreDomainPlugin);
584 dict = SCDynamicStoreCopyValue(NULL, key);
585 CFRelease(key);
586 if (dict != NULL) {
587 hasNamer = TRUE;
588 hasQuiet = CFDictionaryContainsKey(dict, kInterfaceNamerKey_Quiet);
589 hasTimeout = CFDictionaryContainsKey(dict, kInterfaceNamerKey_Timeout);
590 CFRelease(dict);
591 } else {
592 SCTestLog("*** configd/InterfaceNamer status not available");
593 }
594
595 if (hasQuiet) {
596 if (hasTimeout) {
597 err = "*** configd/InterfaceNamer quiet after timeout";
598 } else {
599 // quiet
600 ok = TRUE;
601 }
602 } else {
603 if (hasTimeout) {
604 err = "*** configd/InterfaceNamer timeout, not quiet";
605 } else {
606 kern_return_t ret;
607 mach_timespec_t waitTime = { 60, 0 };
608
609 /*
610 * Here, we're in limbo.
611 * 1. InterfaceNamer has not reported that the IORegistry
612 * to be quiet
613 * 2. InterfaceNamer was happy yet quiet, but has not
614 * reported the timeout
615 *
616 * This likely means that we detected the previousl named
617 * interfaces and released any waiting processes. But, we
618 * don't know if the IORegistry actually quiesced.
619 *
620 * So, let's just check/wait.
621 */
622 ret = IOKitWaitQuiet(kIOMasterPortDefault, &waitTime);
623 if (ret == kIOReturnSuccess) {
624 if (hasNamer) {
625 SCTestLog("*** configd/InterfaceNamer released before quiet");
626 ok = TRUE;
627 } else {
628 err = "*** configd/InterfaceNamer did not report quiet status";
629 }
630 } else {
631 err = "*** IOKit not quiet";
632 }
633 }
634 }
635
636 if (ok) {
637 SCTestLog("IOKit quiesced");
638 } else if (err != NULL) {
639 SCTestLog("%s", err);
640 }
641
642 return ok;
643 }
644
645 #pragma mark -
646 #pragma mark unitTestCheckEN0
647
648 - (BOOL)unitTestCheckEN0
649 {
650 CFStringRef en0 = CFSTR("en0");
651 Boolean en0Found = FALSE;
652 char *if_name;
653 CFArrayRef interfaces;
654 BOOL ok = FALSE;
655
656 SCTestLog("Checking interfaces");
657
658 // for debugging, provide a way to use an alternate interface name
659 if_name = getenv("EN0");
660 if (if_name != NULL) {
661 en0 = CFStringCreateWithCString(NULL, if_name, kCFStringEncodingUTF8);
662 } else {
663 CFRetain(en0);
664 }
665
666 interfaces = SCNetworkInterfaceCopyAll();
667 if (interfaces != NULL) {
668 CFIndex n;
669
670 n = CFArrayGetCount(interfaces);
671 for (CFIndex i = 0; i < n; i++) {
672 CFStringRef bsdName;
673 SCNetworkInterfaceRef interface;
674
675 interface = CFArrayGetValueAtIndex(interfaces, i);
676 bsdName = SCNetworkInterfaceGetBSDName(interface);
677 if (_SC_CFEqual(bsdName, en0)) {
678 CFStringRef interfaceType;
679
680 en0Found = TRUE;
681
682 if (!_SCNetworkInterfaceIsBuiltin(interface)) {
683 SCTestLog("*** Network interface \"%@\" not built-in", en0);
684 break;
685 }
686
687 interfaceType = SCNetworkInterfaceGetInterfaceType(interface);
688 if (CFEqual(interfaceType, kSCNetworkInterfaceTypeEthernet)) {
689 if (!_SCNetworkInterfaceIsThunderbolt(interface) &&
690 !_SCNetworkInterfaceIsApplePreconfigured(interface)) {
691 // if Ethernet (and not Thunderbolt, Bridge, ...)
692 ok = TRUE;
693 break;
694 }
695 } else if (CFEqual(interfaceType, kSCNetworkInterfaceTypeIEEE80211)) {
696 // if Wi-Fi
697 ok = TRUE;
698 break;
699 }
700
701 SCTestLog("*** Network interface \"%@\" not Ethernet or Wi-Fi", en0);
702 break;
703 }
704 }
705
706 CFRelease(interfaces);
707 }
708
709 if (!en0Found) {
710 SCTestLog("*** Network interface \"%@\" not found", en0);
711 }
712
713 if (ok) {
714 SCTestLog("Verified \"%@\"", en0);
715 }
716
717 return ok;
718 }
719
720 @end