-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathCoverStoryDocument.m
1436 lines (1299 loc) · 50.8 KB
/
CoverStoryDocument.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//
// CoverStoryDocument.m
// CoverStory
//
// Created by dmaclach on 12/20/06.
// Copyright 2006-2007 Google Inc.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy
// of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
//
#import "CoverStoryDocument.h"
#import "CoverStoryCoverageData.h"
#import "CoverStoryDocumentTypes.h"
#import "CoverStoryPreferenceKeys.h"
#import "CoverStoryCodeViewTableView.h"
#import "CoverStoryArrayController.h"
#import "GTMScriptRunner.h"
#import "GTMNSFileManager+Path.h"
#import "GTMNSEnumerator+Filter.h"
#import "GTMNSString+HTML.h"
#import "GTMLocalizedString.h"
#import "CoverStoryValueTransformers.h"
#import "GCovVersionManager.h"
const NSInteger kCoverStorySDKToolbarTag = 1026;
const NSInteger kCoverStoryUnittestToolbarTag = 1027;
const NSInteger kCoverStoryCommonPrefixToolbarTag = 1028;
@interface NSWindow (CoverStoryExportToHTML)
// Script command that we want NSWindow to handle
- (id)cs_handleExportHTMLScriptCommand:(NSScriptCommand *)command;
@end
@interface NSOperationQueue (CoverStorySharedOpQueue)
+ (NSOperationQueue*)cs_sharedOperationQueue;
@end
@interface NSFileManager (CoverStoryThreading)
+ (NSFileManager *)threadSafeManager;
@end
typedef enum {
kCSMessageTypeError,
kCSMessageTypeWarning,
kCSMessageTypeInfo
} CSMessageType;
@interface CoverStoryDocument ()
- (void)openFolderInThread:(NSString*)path;
- (void)openFileInThread:(NSString*)path;
- (void)backgroundWorkDone:(id)sender;
- (void)setOpenThreadState:(BOOL)threadRunning;
- (BOOL)processCoverageForFolder:(NSString *)path;
- (void)cleanupTempDir:(NSString *)tempDir;
- (void)loadCoveragePath:(NSString *)fullPath;
- (BOOL)processCoverageForFiles:(NSArray *)filenames
inFolder:(NSString *)folderPath;
- (BOOL)addFileData:(CoverStoryCoverageFileData *)fileData;
- (void)addMessageFromThread:(NSString *)message
path:(NSString*)path
messageType:(CSMessageType)msgType;
- (void)addMessageFromThread:(NSString *)message
messageType:(CSMessageType)msgType;
- (void)addMessage:(NSDictionary *)msgInfo;
- (BOOL)isClosed;
- (void)moveSelection:(NSUInteger)offset;
- (void)finishedLoadingFileDatas:(id)ignored;
@end
@implementation CoverStoryDocument
+ (void)registerDefaults {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSDictionary *documentDefaults =
[NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:kCoverStoryFilterStringTypeWildcardPattern],
kCoverStoryFilterStringTypeKey,
[NSNumber numberWithBool:YES],
kCoverStoryRemoveCommonSourcePrefixKey,
nil];
[defaults registerDefaults:documentDefaults];
}
- (id)init {
if ((self = [super init])) {
dataSet_ = [[CoverStoryCoverageSet alloc] init];
NSString *path;
NSFileWrapper *wrapper;
NSBundle *mainBundle = [NSBundle mainBundle];
path = [mainBundle pathForResource:@"error" ofType:@"png"];
wrapper = [[[NSFileWrapper alloc] initWithPath:path] autorelease];
errorIcon_ = [[NSTextAttachment alloc] initWithFileWrapper:wrapper];
path = [mainBundle pathForResource:@"warning" ofType:@"png"];
wrapper = [[[NSFileWrapper alloc] initWithPath:path] autorelease];
warningIcon_ = [[NSTextAttachment alloc] initWithFileWrapper:wrapper];
path = [mainBundle pathForResource:@"info" ofType:@"png"];
wrapper = [[[NSFileWrapper alloc] initWithPath:path] autorelease];
infoIcon_ = [[NSTextAttachment alloc] initWithFileWrapper:wrapper];
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
hideSDKSources_ = [ud boolForKey:kCoverStoryHideSystemSourcesKey];
hideUnittestSources_ = [ud boolForKey:kCoverStoryHideUnittestSourcesKey];
removeCommonSourcePrefix_ =
[ud boolForKey:kCoverStoryRemoveCommonSourcePrefixKey];
}
return self;
}
- (void)dealloc {
[dataSet_ release];
[filterString_ release];
[currentAnimation_ release];
[commonPathPrefix_ release];
[doneOperation_ release];
#if DEBUG
[startDate_ release];
#endif
[super dealloc];
}
- (void)awakeFromNib {
// expand the search field to start out (odds are we've already started to
// load something, but the annimation is done by a selector invoke on the main
// thread so it will happen after we've been called.)
NSRect searchFieldFrame = [searchField_ frame];
animationWidth_ = searchFieldFrame.origin.x - [spinner_ frame].origin.x;
searchFieldFrame.origin.x -= animationWidth_;
searchFieldFrame.size.width += animationWidth_;
[searchField_ setFrame:searchFieldFrame];
[sourceFilesController_ addObserver:self
forKeyPath:NSSelectionIndexesBinding
options:0
context:nil];
NSSortDescriptor *ascending = [[[NSSortDescriptor alloc] initWithKey:@"sourcePath"
ascending:YES] autorelease];
[sourceFilesController_ setSortDescriptors:[NSArray arrayWithObject:ascending]];
}
- (NSString *)windowNibName {
return @"CoverStoryDocument";
}
- (CoverStoryCoverageSet *)dataSet {
return dataSet_;
}
// Called as a performSelectorOnMainThread, so must check to make sure
// we haven't been closed.
- (BOOL)addFileData:(CoverStoryCoverageFileData *)fileData {
if ([self isClosed]) return NO;
++numFileDatas_;
BOOL isGood = [[self dataSet] addFileData:fileData messageReceiver:self];
return isGood;
}
- (BOOL)readFromFileWrapper:(NSFileWrapper *)fileWrapper
ofType:(NSString *)typeName
error:(NSError **)outError {
BOOL isGood = NO;
numFileDatas_ = 0;
#if DEBUG
[startDate_ release];
startDate_ = [[NSDate date] retain];
#endif
// the wrapper doesn't have the full path, but it's already set on us, so
// use that instead.
NSString *path = [[self fileURL] path];
if ([fileWrapper isDirectory]) {
NSString *message =
[NSString stringWithFormat:@"Scanning for coverage data in '%@'",
path];
[self addMessageFromThread:message messageType:kCSMessageTypeInfo];
[NSThread detachNewThreadSelector:@selector(openFolderInThread:)
toTarget:self
withObject:path];
isGood = YES;
} else if ([typeName isEqualToString:@kGCOVTypeName]) {
NSString *message =
[NSString stringWithFormat:@"Reading gcov data '%@'",
path];
[self addMessageFromThread:message messageType:kCSMessageTypeInfo];
// load it and add it to our set
CoverStoryCoverageFileData *fileData =
[CoverStoryCoverageFileData coverageFileDataFromPath:path
document:self
messageReceiver:self];
if (fileData) {
isGood = [self addFileData:fileData];
} else {
[self addMessageFromThread:@"Failed to load gcov data"
path:path
messageType:kCSMessageTypeError];
}
} else {
NSString *message =
[NSString stringWithFormat:@"Processing coverage data in '%@'",
path];
[self addMessageFromThread:message messageType:kCSMessageTypeInfo];
[NSThread detachNewThreadSelector:@selector(openFileInThread:)
toTarget:self
withObject:path];
isGood = YES;
}
return isGood;
}
- (IBAction)openSelectedSource:(id)sender {
BOOL didOpen = NO;
NSArray *fileSelection = [sourceFilesController_ selectedObjects];
CoverStoryCoverageFileData *fileData = [fileSelection objectAtIndex:0];
NSString *path = [fileData sourcePath];
NSIndexSet *selectedRows = [codeTableView_ selectedRowIndexes];
if ([selectedRows count]) {
NSString *scriptPath = [[NSBundle mainBundle] pathForResource:@"openscript"
ofType:@"scpt"
inDirectory:@"Scripts"];
if (scriptPath) {
GTMScriptRunner *runner = [GTMScriptRunner runnerWithInterpreter:@"/usr/bin/osascript"];
[runner runScript:scriptPath
withArgs:[NSArray arrayWithObjects:
path,
[NSString stringWithFormat:@"%d", [selectedRows firstIndex] + 1],
[NSString stringWithFormat:@"%d", [selectedRows lastIndex] + 1],
nil]];
}
}
if (!didOpen) {
[[NSWorkspace sharedWorkspace] openFile:path];
}
}
- (BOOL)isDocumentEdited {
return NO;
}
- (void)openFolderInThread:(NSString*)path {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self setOpenThreadState:YES];
// We'll use this to know when we're done.
[doneOperation_ release];
doneOperation_ =
[[NSInvocationOperation alloc] initWithTarget:self
selector:@selector(backgroundWorkDone:)
object:@"ignored"];
@try {
[self processCoverageForFolder:path];
}
@catch (NSException *e) {
NSString *msg =
[NSString stringWithFormat:@"Internal error while processing directory (%@ - %@).",
[e name], [e reason]];
[self addMessageFromThread:msg path:path messageType:kCSMessageTypeError];
}
// By now all the cleanup ops that got created are dependents of the done
// operation, so let it go.
[[NSOperationQueue cs_sharedOperationQueue] addOperation:doneOperation_];
[doneOperation_ autorelease];
doneOperation_ = nil;
// Clean up NSTask Zombies.
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
[pool release];
}
- (void)openFileInThread:(NSString*)path {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self setOpenThreadState:YES];
// We'll use this to know when we're done.
[doneOperation_ release];
doneOperation_ =
[[NSInvocationOperation alloc] initWithTarget:self
selector:@selector(backgroundWorkDone:)
object:@"ignored"];
NSString *folderPath = [path stringByDeletingLastPathComponent];
NSString *filename = [path lastPathComponent];
@try {
[self processCoverageForFiles:[NSArray arrayWithObject:filename]
inFolder:folderPath];
}
@catch (NSException *e) {
NSString *msg =
[NSString stringWithFormat:@"Internal error while processing file (%@ - %@).",
[e name], [e reason]];
[self addMessageFromThread:msg path:path messageType:kCSMessageTypeError];
}
// By now all the cleanup ops that got created are dependents of the done
// operation, so let it go.
[[NSOperationQueue cs_sharedOperationQueue] addOperation:doneOperation_];
[doneOperation_ autorelease];
doneOperation_ = nil;
// Clean up NSTask Zombies.
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
[pool release];
}
- (void)backgroundWorkDone:(id)sender {
// signal that we're done
[self performSelectorOnMainThread:@selector(finishedLoadingFileDatas:)
withObject:@"ignored"
waitUntilDone:NO];
[self setOpenThreadState:NO];
}
- (BOOL)processCoverageForFolder:(NSString *)path {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// cycle through the directory...
NSFileManager *fm = [NSFileManager threadSafeManager];
NSDirectoryEnumerator *enumerator = [fm enumeratorAtPath:path];
// ...filter to .gcda files...
NSEnumerator *enumerator2 =
[enumerator gtm_filteredEnumeratorByMakingEachObjectPerformSelector:@selector(hasSuffix:)
withObject:@".gcda"];
// ...turn them all into full paths...
NSEnumerator *enumerator3 =
[enumerator2 gtm_enumeratorByTarget:path
performOnEachSelector:@selector(stringByAppendingPathComponent:)];
// .. and collect them all.
NSArray *allFilePaths = [enumerator3 allObjects];
NSUInteger pathCount = [allFilePaths count];
if (pathCount == 0) {
[self addMessageFromThread:@"Found no gcda files to process."
messageType:kCSMessageTypeWarning];
} else if (pathCount == 1) {
[self addMessageFromThread:@"Found 1 gcda file to process."
messageType:kCSMessageTypeInfo];
} else {
NSString *message =
[NSString stringWithFormat:@"Found %u gcda files to process.", pathCount];
[self addMessageFromThread:message
messageType:kCSMessageTypeInfo];
}
// we want to batch process them by chunks w/in a given directory. so sort
// and then break them off into chunks.
allFilePaths = [allFilePaths sortedArrayUsingSelector:@selector(compare:)];
NSEnumerator *pathEnum = [allFilePaths objectEnumerator];
NSString *filename;
if ((filename = [pathEnum nextObject])) {
// seed our collecting w/ the first item
NSString *currentFolder = [filename stringByDeletingLastPathComponent];
NSMutableArray *currentFileList =
[NSMutableArray arrayWithObject:[filename lastPathComponent]];
// now spin the loop
while ((filename = [pathEnum nextObject])) {
// see if it has the same parent folder
if ([[filename stringByDeletingLastPathComponent] isEqualTo:currentFolder]) {
// add it
NSAssert([currentFileList count] > 0, @"file list should NOT be empty");
[currentFileList addObject:[filename lastPathComponent]];
} else {
// process what's in the list
if (![self processCoverageForFiles:currentFileList
inFolder:currentFolder]) {
NSString *message =
[NSString stringWithFormat:@"failed to process files: %@",
currentFileList];
[self addMessageFromThread:message path:currentFolder
messageType:kCSMessageTypeError];
}
// restart the collecting w/ this filename
currentFolder = [filename stringByDeletingLastPathComponent];
[currentFileList removeAllObjects];
[currentFileList addObject:[filename lastPathComponent]];
}
// Bail if we get closed
if ([self isClosed]) {
[pool release];
return YES;
}
}
// process whatever what we were collecting when we hit the end
if (![self processCoverageForFiles:currentFileList
inFolder:currentFolder]) {
NSString *message =
[NSString stringWithFormat:@"failed to process files: %@",
currentFileList];
[self addMessageFromThread:message
path:currentFolder
messageType:kCSMessageTypeError];
}
}
[pool release];
return YES;
}
- (NSString *)tempDirName {
// go w/ temp dir if anything goes wrong
NSString *result = NSTemporaryDirectory();
// throw a guid on it so if we're scanning >1 place at a time, each gets
// it's own sandbox.
CFUUIDRef uuidRef = CFUUIDCreate(NULL);
if (uuidRef) {
CFStringRef uuidStr = CFUUIDCreateString(NULL, uuidRef);
if (uuidStr) {
result = [result stringByAppendingPathComponent:(NSString*)uuidStr];
CFRelease(uuidStr);
} else {
NSLog(@"failed to convert our CFUUIDRef into a CFString");
}
CFRelease(uuidRef);
} else {
NSLog(@"failed to generate a CFUUIDRef");
}
return result;
}
- (void)cleanupTempDir:(NSString *)tempDir {
@try {
// nuke our temp dir tree
NSFileManager *fm = [NSFileManager threadSafeManager];
if (![fm removeItemAtPath:tempDir error:nil]) {
[self addMessageFromThread:@"failed to remove our tempdir"
path:tempDir
messageType:kCSMessageTypeError];
}
}
@catch (NSException * e) {
NSString *msg
= [NSString stringWithFormat:@"Internal error trying to cleanup tempdir (%@ - %@).",
[e name], [e reason]];
[self addMessageFromThread:msg messageType:kCSMessageTypeError];
}
}
- (void)loadCoveragePath:(NSString *)fullPath {
@try {
// load it and add it to our set
CoverStoryCoverageFileData *fileData
= [CoverStoryCoverageFileData coverageFileDataFromPath:fullPath
document:self
messageReceiver:self];
if (fileData) {
[self performSelectorOnMainThread:@selector(addFileData:)
withObject:fileData
waitUntilDone:NO];
}
}
@catch (NSException * e) {
NSString *msg
= [NSString stringWithFormat:@"Internal error trying load coverage data (%@ - %@).",
[e name], [e reason]];
[self addMessageFromThread:msg messageType:kCSMessageTypeError];
}
}
- (BOOL)processCoverageForFiles:(NSArray *)filenames
inFolder:(NSString *)folderPath {
if (([filenames count] == 0) || ([folderPath length] == 0)) {
return NO;
}
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString *tempDir = [self tempDirName];
// make sure all the filenames are just leaves
for (NSString *filename in filenames) {
NSRange range = [filename rangeOfString:@"/"];
if (range.location != NSNotFound) {
[self addMessageFromThread:@"skipped because filename had a slash"
path:[folderPath stringByAppendingPathComponent:filename]
messageType:kCSMessageTypeError];
[pool release];
return NO;
}
}
// make sure it ends in a slash
if (![folderPath hasSuffix:@"/"]) {
folderPath = [folderPath stringByAppendingString:@"/"];
}
// Figure out what version of gcov to use.
// NOTE: To be 100% correct, we should check *each* file and split them into
// sets based on what version of gcov will be invoked. But we're assuming
// most people will only set the gcc version of a per Xcode target level (at
// the lowest).
GCovVersionManager *gcovVerMgr = [GCovVersionManager defaultManager];
NSString *aFullPath =
[folderPath stringByAppendingPathComponent:[filenames objectAtIndex:0]];
NSString *gcovPath = [gcovVerMgr gcovForGCovFile:aFullPath];
// we write all the full file paths into a file w/ null chars after each
// so we can feed it into xargs -0
NSMutableData *fileList = [NSMutableData data];
NSData *folderPathUTF8 = [folderPath dataUsingEncoding:NSUTF8StringEncoding];
if (!folderPathUTF8 || !fileList) {
[pool release];
return NO;
}
char nullByte = 0;
for (NSString *filename in filenames) {
NSData *filenameUTF8 = [filename dataUsingEncoding:NSUTF8StringEncoding];
if (!filenameUTF8) {
[pool release];
return NO;
}
[fileList appendData:folderPathUTF8];
[fileList appendData:filenameUTF8];
[fileList appendBytes:&nullByte length:1];
}
GTMScriptRunner *runner = [GTMScriptRunner runnerWithBash];
if (!runner) {
[pool release];
return NO;
}
BOOL result = NO;
// make a scratch directory
NSFileManager *fm = [NSFileManager threadSafeManager];
if ([fm createDirectoryAtPath:tempDir
withIntermediateDirectories:YES
attributes:nil
error:NULL]) {
NSOperationQueue *opQueue = [NSOperationQueue cs_sharedOperationQueue];
// create our cleanup op since it will use the other ops as dependencies
NSInvocationOperation *cleanupOp
= [[[NSInvocationOperation alloc] initWithTarget:self
selector:@selector(cleanupTempDir:)
object:tempDir] autorelease];
// The done operation will depend on this cleanup op to know when things
// finish.
[doneOperation_ addDependency:cleanupOp];
// now write out our file
NSString *fileListPath = [tempDir stringByAppendingPathComponent:@"filelists.txt"];
if (fileListPath && [fileList writeToFile:fileListPath atomically:YES]) {
// run gcov (it writes to current directory, so we cd into our dir first)
// we use xargs to batch up the files into as few of runs of gcov as
// possible. (we could use -P [num_cpus] to do things in parallell)
NSString *script
= [NSString stringWithFormat:@"cd \"%@\" && /usr/bin/xargs -0 \"%@\" -l -o \"%@\" < \"%@\"",
tempDir, gcovPath, folderPath, fileListPath];
NSString *stdErr = nil;
NSString *stdOut = [runner run:script standardError:&stdErr];
if (([stdOut length] == 0) || ([stdErr length] > 0)) {
// we don't actually care about stdout since it's just the files
// that did work.
NSEnumerator *enumerator = [[stdErr componentsSeparatedByString:@"\n"] objectEnumerator];
NSString *message;
while ((message = [enumerator nextObject])) {
NSRange range = [message rangeOfString:@":"];
NSString *path = nil;
if (range.length != 0) {
path = [message substringToIndex:range.location];
message = [message substringFromIndex:NSMaxRange(range)];
}
[self addMessageFromThread:message
path:path
messageType:kCSMessageTypeError];
}
}
// since we batch process, we might have gotten some data even w/ an error
// so we check anyways for data
// collect the gcov files
NSArray *resultPaths = [fm gtm_filePathsWithExtension:@"gcov"
inDirectory:tempDir];
NSEnumerator *resultPathsEnum = [resultPaths objectEnumerator];
NSString *fullPath;
while ((fullPath = [resultPathsEnum nextObject]) && ![self isClosed]) {
NSInvocationOperation *op
= [[[NSInvocationOperation alloc] initWithTarget:self
selector:@selector(loadCoveragePath:)
object:fullPath] autorelease];
// cleanup can't be done until all our other ops are done
[cleanupOp addDependency:op];
// queue it up
[opQueue addOperation:op];
result = YES;
}
} else {
[self addMessageFromThread:@"failed to write out the file lists"
path:fileListPath
messageType:kCSMessageTypeError];
}
// now put in the cleanup operation
[opQueue addOperation:cleanupOp];
}
[pool release];
return result;
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
BOOL handled = NO;
if ([object isEqualTo:sourceFilesController_] &&
[keyPath isEqualToString:NSSelectionIndexesBinding]) {
NSArray *selectedObjects = [object selectedObjects];
CoverStoryCoverageFileData *data = nil;
if ([selectedObjects count]) {
data = (CoverStoryCoverageFileData*)[selectedObjects objectAtIndex:0];
}
if (data) {
// Update our scroll bar
[codeTableView_ setCoverageData:[data lines]];
// Jump to first missing code block
[self moveSelection:1];
}
handled = YES;
}
if (!handled) {
_GTMDevLog(@"Unexpected observance of %@ of %@ (%@)", keyPath, object, change);
}
}
- (NSString *)filterString {
return filterString_;
}
- (void)setFilterString:(NSString *)string {
if (filterString_ != string) {
[filterString_ release];
filterString_ = [string copy];
[sourceFilesController_ rearrangeObjects];
}
}
- (void)setFilterStringType:(CoverStoryFilterStringType)type {
[[NSUserDefaults standardUserDefaults] setInteger:type
forKey:kCoverStoryFilterStringTypeKey];
[sourceFilesController_ rearrangeObjects];
}
- (IBAction)setUseWildcardPattern:(id)sender {
[self setFilterStringType:kCoverStoryFilterStringTypeWildcardPattern];
}
- (IBAction)setUseRegularExpression:(id)sender {
[self setFilterStringType:kCoverStoryFilterStringTypeRegularExpression];
}
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem {
typedef struct {
CoverStoryFilterStringType type;
SEL selector;
} FilterSelectorMap;
FilterSelectorMap map[] = {
{ kCoverStoryFilterStringTypeWildcardPattern, @selector(setUseWildcardPattern:) },
{ kCoverStoryFilterStringTypeRegularExpression, @selector(setUseRegularExpression:) },
};
CoverStoryFilterStringType type;
type = [[NSUserDefaults standardUserDefaults] integerForKey:kCoverStoryFilterStringTypeKey];
BOOL isGood = NO;
SEL action = [menuItem action];
for (size_t i = 0; i <= sizeof(map) / sizeof(FilterSelectorMap); ++i) {
if (action == map[i].selector) {
isGood = YES;
[menuItem setState:map[i].type == type ? NSOnState : NSOffState];
break;
}
}
if (!isGood) {
NSInteger tag = [menuItem tag];
NSString *label = nil;
if (tag == kCoverStorySDKToolbarTag) {
if (hideSDKSources_) {
label = NSLocalizedString(@"Show SDK Source Files", nil);
} else {
label = NSLocalizedString(@"Hide SDK Source Files", nil);
}
} else if (tag == kCoverStoryUnittestToolbarTag) {
if (hideUnittestSources_) {
label = NSLocalizedString(@"Show Unittest Source Files", nil);
} else {
label = NSLocalizedString(@"Hide Unittest Source Files", nil);
}
} else if (tag == kCoverStoryCommonPrefixToolbarTag) {
if (removeCommonSourcePrefix_) {
label = NSLocalizedString(@"Show Full Paths", nil);
} else {
label = NSLocalizedString(@"Remove Common Path Prefix", nil);
}
}
if (label) {
isGood = YES;
[menuItem setTitle:label];
}
}
if (!isGood) {
if (action == @selector(saveDocumentTo:)) {
isGood = [self completelyOpened];
} else {
isGood = [super validateMenuItem:menuItem];
}
}
return isGood;
}
- (void)moveUpAndModifySelection:(id)sender {
[self moveSelection:-1];
}
- (void)moveDownAndModifySelection:(id)sender {
[self moveSelection:1];
}
// On up or down key we want to select the next block of code that has
// zero coverage.
- (void)moveSelection:(NSUInteger)offset {
// If no source, bail
NSArray *selection = [sourceFilesController_ selectedObjects];
if (![selection count]) return;
// Start with the current selection
CoverStoryCoverageFileData *fileData = [selection objectAtIndex:0];
NSArray *lines = [fileData lines];
NSIndexSet *currentSel = [codeTableView_ selectedRowIndexes];
// Choose direction based on key and set offset and stopping conditions
// as well as start.
NSUInteger stoppingCond = 0;
NSRange range = NSMakeRange(0, 0);
if (offset > 0) {
stoppingCond = [lines count] - 1;
if ([lines count] == 0) {
stoppingCond = 0;
}
}
NSUInteger startLine = 0;
if ([currentSel count]) {
NSUInteger first = [currentSel firstIndex];
NSUInteger last = [currentSel lastIndex];
range = NSMakeRange(first, last - first);
startLine = offset == 1 ? last : first;
}
// From start, look for first line in our given direction that has
// zero hits
NSUInteger i;
for (i = startLine + offset; i != stoppingCond; i += offset) {
CoverStoryCoverageLineData *lineData = [lines objectAtIndex:i];
if ([lineData hitCount] == 0) {
break;
}
}
// Check to see if we hit end of page (or beginning depending which way
// we went
if (i != stoppingCond) {
// Now select "forward" everything that is zero
NSUInteger j;
for (j = i; j != stoppingCond; j += offset) {
CoverStoryCoverageLineData *lineData = [lines objectAtIndex:j];
if ([lineData hitCount] != 0) {
break;
}
}
// Now if we started in a block, select "backwards"
NSUInteger k;
stoppingCond = offset == 1 ? 0 : [lines count] - 1;
offset *= -1;
for (k = i; k != stoppingCond; k+= offset) {
CoverStoryCoverageLineData *lineData = [lines objectAtIndex:k];
if ([lineData hitCount] != 0) {
k -= offset;
break;
}
}
// Update our selection
range = k > j ? NSMakeRange(j + 1, k - j) : NSMakeRange(k, j - k);
[codeTableView_ selectRowIndexes:[NSIndexSet indexSetWithIndexesInRange:range]
byExtendingSelection:NO];
}
[codeTableView_ scrollRowToVisible:NSMaxRange(range)];
[codeTableView_ scrollRowToVisible:range.location];
}
- (void)setSortKeyOfTableView:(NSTableView *)tableView
column:(NSString *)columnName
to:(NSString *)sortKeyName {
NSTableColumn *column = [tableView tableColumnWithIdentifier:columnName];
NSSortDescriptor *oldDesc = [column sortDescriptorPrototype];
NSSortDescriptor *descriptor
= [[[NSSortDescriptor alloc] initWithKey:sortKeyName
ascending:[oldDesc ascending]] autorelease];
[column setSortDescriptorPrototype:descriptor];
}
- (void)reloadData:(id)sender {
if (openingInThread_) {
// starting a reload keeps pushing to the existing data, so block it until
// we're done.
[self addMessageFromThread:@"Still loading data, can't start a reload."
messageType:kCSMessageTypeWarning];
return;
}
[dataSet_ removeAllData];
// clear the message view before we start
// add the message, color, and scroll
[messageView_ setString:@""];
NSError *error = nil;
if (![self readFromURL:[self fileURL]
ofType:[self fileType]
error:&error]) {
[self addMessageFromThread:@"couldn't reload file"
path:[[self fileURL] path]
messageType:kCSMessageTypeError];
}
}
- (IBAction)toggleMessageDrawer:(id)sender {
[drawer_ toggle:self];
}
- (void)setCommonPathPrefix:(NSString *)newPrefix {
[commonPathPrefix_ autorelease];
// we cheat, and if the pref is set, we just make sure we return no prefix
if (removeCommonSourcePrefix_) {
commonPathPrefix_ = [newPrefix copy];
} else {
commonPathPrefix_ = nil;
}
}
- (NSString *)commonPathPrefix {
return commonPathPrefix_;
}
// Moves our searchfield to display our spinner and starts it spinning.
// Called as a performSelectorOnMainThread, so must check to make sure
// we haven't been closed.
- (void)displayAndAnimateSpinner:(NSNumber*)start {
if ([self isClosed]) return;
if (spinner_) {
// force any running animation to end
if (currentAnimation_) {
[currentAnimation_ stopAnimation];
[currentAnimation_ release];
currentAnimation_ = nil;
}
BOOL starting = [start boolValue];
NSString *effect;
NSRect rect = [searchField_ frame];
if (starting) {
rect.origin.x += animationWidth_;
rect.size.width -= animationWidth_;
effect = NSViewAnimationFadeInEffect;
} else {
rect.origin.x -= animationWidth_;
rect.size.width += animationWidth_;
effect = NSViewAnimationFadeOutEffect;
}
NSValue *endFrameRectValue = [NSValue valueWithRect:rect];
NSDictionary *searchAnimation = [NSDictionary dictionaryWithObjectsAndKeys:
searchField_, NSViewAnimationTargetKey,
endFrameRectValue, NSViewAnimationEndFrameKey,
nil];
NSDictionary *spinnerAnimation = [NSDictionary dictionaryWithObjectsAndKeys:
spinner_, NSViewAnimationTargetKey,
effect, NSViewAnimationEffectKey,
nil];
NSArray *animations;
if (starting) {
animations = [NSArray arrayWithObjects:
searchAnimation,
spinnerAnimation,
nil];
} else {
animations = [NSArray arrayWithObjects:
spinnerAnimation,
searchAnimation,
nil];
}
currentAnimation_ =
[[NSViewAnimation alloc] initWithViewAnimations:animations];
[currentAnimation_ setDelegate:self];
[currentAnimation_ startAnimation];
if (starting) {
[spinner_ startAnimation:self];
} else {
[spinner_ stopAnimation:self];
}
}
}
- (void)animationDidEnd:(NSAnimation *)animation {
if (animation == currentAnimation_) {
// clear out our reference
[currentAnimation_ release];
currentAnimation_ = nil;
}
}
- (void)finishedLoadingFileDatas:(id)ignored {
if (numFileDatas_ == 0) {
[self addMessageFromThread:@"No coverage data read."
messageType:kCSMessageTypeWarning];
} else {
if (numFileDatas_ == 1) {
[self addMessageFromThread:@"Loaded one file of coverage data."
messageType:kCSMessageTypeInfo];
} else {
NSString *message =
[NSString stringWithFormat:@"Successfully loaded %u coverage fragments.",
numFileDatas_];
[self addMessageFromThread:message
messageType:kCSMessageTypeInfo];
}
NSInteger totalLines = 0;
NSInteger codeLines = 0;
NSInteger hitLines = 0;
NSInteger nonfeasible = 0;
NSString *coverage = nil;
[dataSet_ coverageTotalLines:&totalLines
codeLines:&codeLines
hitCodeLines:&hitLines
nonFeasibleLines:&nonfeasible
coverageString:&coverage
coverage:NULL];
NSString *summary = nil;
if (nonfeasible > 0) {
summary = [NSString stringWithFormat:
@"Full dataset executed %@%% of %d lines (%d executed, "
@"%d executable, %d non-feasible, %d total lines).",
coverage, codeLines, hitLines, codeLines, nonfeasible,
totalLines];
} else {
summary = [NSString stringWithFormat:
@"Full dataset executed %@%% of %d lines (%d executed, "
@"%d executable, %d total lines).",
coverage, codeLines, hitLines, codeLines, totalLines];
}
[self addMessageFromThread:summary
messageType:kCSMessageTypeInfo];
[self addMessageFromThread:@"There is a tooltip on the total above the file"
@" list that shows numbers for the currently"
@" displayed set."
messageType:kCSMessageTypeInfo];
}
#if DEBUG
if (startDate_) {
NSTimeInterval elapsed = -[startDate_ timeIntervalSinceNow];
UInt32 secs = (UInt32)elapsed % 60;
UInt32 mins = ((UInt32)elapsed / 60) % 60;
NSString *elapsedStr
= [NSString stringWithFormat:@"It took %u:%02u to process the data.",
mins, secs];
[self addMessageFromThread:elapsedStr messageType:kCSMessageTypeInfo];
}
#endif // DEBUG
}
- (void)setOpenThreadState:(BOOL)threadRunning {
openingInThread_ = threadRunning;
[self performSelectorOnMainThread:@selector(displayAndAnimateSpinner:)
withObject:[NSNumber numberWithBool:openingInThread_]
waitUntilDone:NO];
}
- (BOOL)completelyOpened {
return !openingInThread_;
}