cf93d206c6c54419c8d57aa81a4aaea8149ee192
[WebKit-https.git] / WebKitTools / DumpRenderTree / DumpRenderTree.m
1 /*
2  * Copyright (C) 2005, 2006 Apple Computer, Inc.  All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer. 
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution. 
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission. 
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28  
29 #import "DumpRenderTree.h"
30
31 #import <WebKit/DOMExtensions.h>
32 #import <WebKit/DOMRange.h>
33 #import <WebKit/WebBackForwardList.h>
34 #import <WebKit/WebCoreStatistics.h>
35 #import <WebKit/WebDataSource.h>
36 #import <WebKit/WebEditingDelegate.h>
37 #import <WebKit/WebFramePrivate.h>
38 #import <WebKit/WebFrameView.h>
39 #import <WebKit/WebHistory.h>
40 #import <WebKit/WebPreferences.h>
41 #import <WebKit/WebView.h>
42 #import <WebKit/WebHTMLViewPrivate.h>
43 #import <WebKit/WebDocumentPrivate.h>
44 #import <WebKit/WebPluginDatabase.h>
45 #import <WebKit/WebHistoryItemPrivate.h>
46
47 #import <ApplicationServices/ApplicationServices.h> // for CMSetDefaultProfileBySpace
48 #import <objc/objc-runtime.h>                       // for class_poseAs
49
50 #define COMMON_DIGEST_FOR_OPENSSL
51 #import <CommonCrypto/CommonDigest.h>               // for MD5 functions
52
53 #import <getopt.h>
54 #import <malloc/malloc.h>
55
56 #import "AppleScriptController.h"
57 #import "DumpRenderTreeDraggingInfo.h"
58 #import "EditingDelegate.h"
59 #import "EventSendingController.h"
60 #import "GCController.h"
61 #import "NavigationController.h"
62 #import "ObjCPlugin.h"
63 #import "ObjCPluginFunction.h"
64 #import "TextInputController.h"
65
66 @interface DumpRenderTreeWindow : NSWindow
67 @end
68
69 @interface DumpRenderTreePasteboard : NSPasteboard
70 @end
71
72 @interface DumpRenderTreeEvent : NSEvent
73 @end
74
75 @interface WaitUntilDoneDelegate : NSObject
76 @end
77
78 @interface LayoutTestController : NSObject
79 @end
80
81 static void runTest(const char *pathOrURL);
82 static NSString *md5HashStringForBitmap(CGImageRef bitmap);
83
84 WebFrame *frame = 0;
85 DumpRenderTreeDraggingInfo *draggingInfo = 0;
86
87 static volatile BOOL done;
88 static NavigationController *navigationController;
89
90 // Deciding when it's OK to dump out the state is a bit tricky.  All these must be true:
91 // - There is no load in progress
92 // - There is no work queued up (see workQueue var, below)
93 // - waitToDump==NO.  This means either waitUntilDone was never called, or it was called
94 //       and notifyDone was called subsequently.
95 // Note that the call to notifyDone and the end of the load can happen in either order.
96
97 // This is the topmost frame that is loading, during a given load, or nil when no load is 
98 // in progress.  Usually this is the same as the main frame, but not always.  In the case
99 // where a frameset is loaded, and then new content is loaded into one of the child frames,
100 // that child frame is the "topmost frame that is loading".
101 static WebFrame *topLoadingFrame;     // !nil iff a load is in progress
102 static BOOL waitToDump;     // TRUE if waitUntilDone() has been called, but notifyDone() has not yet been called
103
104 static BOOL dumpAsText;
105 static BOOL dumpSelectionRect;
106 static BOOL dumpTitleChanges;
107 static BOOL dumpBackForwardList;
108 static int dumpPixels = NO;
109 static int dumpAllPixels = NO;
110 static BOOL readFromWindow = NO;
111 static int testRepaintDefault = NO;
112 static BOOL testRepaint = NO;
113 static int repaintSweepHorizontallyDefault = NO;
114 static BOOL repaintSweepHorizontally = NO;
115 static int dumpTree = YES;
116 static BOOL printSeparators;
117 static NSString *currentTest = nil;
118 static NSPasteboard *localPasteboard;
119 static WebHistoryItem *prevTestBFItem = nil;  // current b/f item at the end of the previous test
120 static BOOL windowIsKey = YES;
121 static unsigned char* screenCaptureBuffer;
122 static CGColorSpaceRef sharedColorSpace;
123 // a queue of NSInvocations, queued by callouts from the test, to be exec'ed when the load is done
124 static NSMutableArray *workQueue = nil;
125 // to prevent infinite loops, only the first page of a test can add to a work queue
126 // (since we may well come back to that same page)
127 BOOL workQueueFrozen;
128
129 const unsigned maxViewHeight = 600;
130 const unsigned maxViewWidth = 800;
131
132 BOOL doneLoading(void)
133 {
134     return done;
135 }
136
137 static CMProfileRef currentColorProfile = 0;
138 static void restoreColorSpace(int ignored)
139 {
140     if (currentColorProfile) {
141         int error = CMSetDefaultProfileByUse(cmDisplayUse, currentColorProfile);
142         if (error)
143             fprintf(stderr, "Failed to retore previous color profile!  You may need to open System Preferences : Displays : Color and manually restore your color settings.  (Error: %i)", error);
144         currentColorProfile = 0;
145     }
146 }
147
148 static void crashHandler(int sig)
149 {
150     fprintf(stderr, "%s\n", strsignal(sig));
151     restoreColorSpace(0);
152     exit(128 + sig);
153 }
154
155 static void setDefaultColorProfileToRGB(void)
156 {
157     CMProfileRef genericProfile = [[NSColorSpace genericRGBColorSpace] colorSyncProfile];
158     CMProfileRef previousProfile;
159     int error = CMGetDefaultProfileByUse(cmDisplayUse, &previousProfile);
160     if (error) {
161         fprintf(stderr, "Failed to get current color profile.  I will not be able to restore your current profile, thus I'm not changing it.  Many pixel tests may fail as a result.  (Error: %i)\n", error);
162         return;
163     }
164     if (previousProfile == genericProfile)
165         return;
166     CFStringRef previousProfileName;
167     CFStringRef genericProfileName;
168     char previousProfileNameString[1024];
169     char genericProfileNameString[1024];
170     CMCopyProfileDescriptionString(previousProfile, &previousProfileName);
171     CMCopyProfileDescriptionString(genericProfile, &genericProfileName);
172     CFStringGetCString(previousProfileName, previousProfileNameString, sizeof(previousProfileNameString), kCFStringEncodingUTF8);
173     CFStringGetCString(genericProfileName, genericProfileNameString, sizeof(previousProfileNameString), kCFStringEncodingUTF8);
174     CFRelease(genericProfileName);
175     CFRelease(previousProfileName);
176     
177     fprintf(stderr, "\n\nWARNING: Temporarily changing your system color profile from \"%s\" to \"%s\".\n", previousProfileNameString, genericProfileNameString);
178     fprintf(stderr, "This allows the WebKit pixel-based regression tests to have consistent color values across all machines.\n");
179     fprintf(stderr, "The colors on your screen will change for the duration of the testing.\n\n");
180     
181     if ((error = CMSetDefaultProfileByUse(cmDisplayUse, genericProfile)))
182         fprintf(stderr, "Failed to set color profile to \"%s\"! Many pixel tests will fail as a result.  (Error: %i)",
183             genericProfileNameString, error);
184     else {
185         currentColorProfile = previousProfile;
186         signal(SIGINT, restoreColorSpace);
187         signal(SIGHUP, restoreColorSpace);
188         signal(SIGTERM, restoreColorSpace);
189     }
190 }
191
192 static void* (*savedMalloc)(malloc_zone_t*, size_t);
193 static void* (*savedRealloc)(malloc_zone_t*, void*, size_t);
194
195 static void* checkedMalloc(malloc_zone_t* zone, size_t size)
196 {
197     if (size >= 0x10000000)
198         return 0;
199     return savedMalloc(zone, size);
200 }
201
202 static void* checkedRealloc(malloc_zone_t* zone, void* ptr, size_t size)
203 {
204     if (size >= 0x10000000)
205         return 0;
206     return savedRealloc(zone, ptr, size);
207 }
208
209 static void makeLargeMallocFailSilently(void)
210 {
211     malloc_zone_t* zone = malloc_default_zone();
212     savedMalloc = zone->malloc;
213     savedRealloc = zone->realloc;
214     zone->malloc = checkedMalloc;
215     zone->realloc = checkedRealloc;
216 }
217
218 int main(int argc, const char *argv[])
219 {
220     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
221     
222     [NSApplication sharedApplication];
223
224     class_poseAs(objc_getClass("DumpRenderTreePasteboard"), objc_getClass("NSPasteboard"));
225     class_poseAs(objc_getClass("DumpRenderTreeWindow"), objc_getClass("NSWindow"));
226     class_poseAs(objc_getClass("DumpRenderTreeEvent"), objc_getClass("NSEvent"));
227
228     struct option options[] = {
229         {"dump-all-pixels", no_argument, &dumpAllPixels, YES},
230         {"horizontal-sweep", no_argument, &repaintSweepHorizontallyDefault, YES},
231         {"notree", no_argument, &dumpTree, NO},
232         {"pixel-tests", no_argument, &dumpPixels, YES},
233         {"repaint", no_argument, &testRepaintDefault, YES},
234         {"tree", no_argument, &dumpTree, YES},
235         {NULL, 0, NULL, 0}
236     };
237
238     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
239     [defaults setObject:@"DoubleMax" forKey:@"AppleScrollBarVariant"];
240     [defaults setInteger:4 forKey:@"AppleAntiAliasingThreshold"];
241     // 2 is the "Medium" font smoothing mode
242     [defaults setInteger:2 forKey:@"AppleFontSmoothing"];
243
244     [defaults setInteger:1 forKey:@"AppleAquaColorVariant"];
245     [defaults setObject:@"0.709800 0.835300 1.000000" forKey:@"AppleHighlightColor"];
246     [defaults setObject:@"0.500000 0.500000 0.500000" forKey:@"AppleOtherHighlightColor"];
247     
248     [defaults setObject:[NSArray arrayWithObject:@"en"] forKey:@"AppleLanguages"];
249     
250     WebPreferences *preferences = [WebPreferences standardPreferences];
251     
252     [preferences setStandardFontFamily:@"Times"];
253     [preferences setFixedFontFamily:@"Courier"];
254     [preferences setSerifFontFamily:@"Times"];
255     [preferences setSansSerifFontFamily:@"Helvetica"];
256     [preferences setCursiveFontFamily:@"Apple Chancery"];
257     [preferences setFantasyFontFamily:@"Papyrus"];
258     [preferences setDefaultFontSize:16];
259     [preferences setDefaultFixedFontSize:13];
260     [preferences setMinimumFontSize:9];
261     [preferences setJavaEnabled:NO];
262     [preferences setJavaScriptCanOpenWindowsAutomatically:NO];
263
264     int option;
265     while ((option = getopt_long(argc, (char * const *)argv, "", options, NULL)) != -1)
266         switch (option) {
267             case '?':   // unknown or ambiguous option
268             case ':':   // missing argument
269                 exit(1);
270                 break;
271         }
272     
273     if ([[[NSFontManager sharedFontManager] availableMembersOfFontFamily:@"Ahem"] count] == 0) {
274         fprintf(stderr, "\nAhem font is not available. This special simple font is used to construct certain types of predictable tests.\n\nTo run regression tests, please get it from <http://webkit.opendarwin.org/quality/Ahem.ttf>.\n");
275         exit(1);
276     }
277     
278     if (dumpPixels) {
279         setDefaultColorProfileToRGB();
280         screenCaptureBuffer = malloc(maxViewHeight * maxViewWidth * 4);
281         sharedColorSpace = CGColorSpaceCreateDeviceRGB();
282     }
283     
284     localPasteboard = [NSPasteboard pasteboardWithUniqueName];
285     navigationController = [[NavigationController alloc] init];
286
287     NSRect rect = NSMakeRect(0, 0, maxViewWidth, maxViewHeight);
288     
289     WebView *webView = [[WebView alloc] initWithFrame:rect];
290     WaitUntilDoneDelegate *delegate = [[WaitUntilDoneDelegate alloc] init];
291     EditingDelegate *editingDelegate = [[EditingDelegate alloc] init];
292     [webView setFrameLoadDelegate:delegate];
293     [webView setEditingDelegate:editingDelegate];
294     [webView setUIDelegate:delegate];
295     frame = [webView mainFrame];
296     
297     [[webView preferences] setTabsToLinks:YES];
298     
299     NSString *pwd = [[NSString stringWithUTF8String:argv[0]] stringByDeletingLastPathComponent];
300     [WebPluginDatabase setAdditionalWebPlugInPaths:[NSArray arrayWithObject:pwd]];
301     [[WebPluginDatabase installedPlugins] refresh];
302
303     // The back/forward cache is causing problems due to layouts during transition from one page to another.
304     // So, turn it off for now, but we might want to turn it back on some day.
305     [[webView backForwardList] setPageCacheSize:0];
306
307     // To make things like certain NSViews, dragging, and plug-ins work, put the WebView a window, but put it off-screen so you don't see it.
308     // Put it at -10000, -10000 in "flipped coordinates", since WebCore and the DOM use flipped coordinates.
309     NSRect windowRect = NSOffsetRect(rect, -10000, [[[NSScreen screens] objectAtIndex:0] frame].size.height - rect.size.height + 10000);
310     NSWindow *window = [[NSWindow alloc] initWithContentRect:windowRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:YES];
311     [[window contentView] addSubview:webView];
312     [window orderBack:nil];
313     [window setAutodisplay:NO];
314
315     workQueue = [[NSMutableArray alloc] init];
316
317     [webView setContinuousSpellCheckingEnabled:YES];
318
319     makeLargeMallocFailSilently();
320
321     signal(SIGILL, crashHandler);    /* 4:   illegal instruction (not reset when caught) */
322     signal(SIGTRAP, crashHandler);   /* 5:   trace trap (not reset when caught) */
323     signal(SIGEMT, crashHandler);    /* 7:   EMT instruction */
324     signal(SIGFPE, crashHandler);    /* 8:   floating point exception */
325     signal(SIGBUS, crashHandler);    /* 10:  bus error */
326     signal(SIGSEGV, crashHandler);   /* 11:  segmentation violation */
327     signal(SIGSYS, crashHandler);    /* 12:  bad argument to system call */
328     signal(SIGPIPE, crashHandler);   /* 13:  write on a pipe with no reader */
329     signal(SIGXCPU, crashHandler);   /* 24:  exceeded CPU time limit */
330     signal(SIGXFSZ, crashHandler);   /* 25:  exceeded file size limit */
331     
332     [[NSURLCache sharedURLCache] removeAllCachedResponses];
333     
334     // For reasons that are not entirely clear, the following pair of calls makes WebView handle its
335     // dynamic scrollbars properly. Without it, every frame will always have scrollbars.
336     NSBitmapImageRep *imageRep = [webView bitmapImageRepForCachingDisplayInRect:[webView bounds]];
337     [webView cacheDisplayInRect:[webView bounds] toBitmapImageRep:imageRep];
338     
339     if (argc == optind+1 && strcmp(argv[optind], "-") == 0) {
340         char filenameBuffer[2048];
341         printSeparators = YES;
342         while (fgets(filenameBuffer, sizeof(filenameBuffer), stdin)) {
343             char *newLineCharacter = strchr(filenameBuffer, '\n');
344             if (newLineCharacter)
345                 *newLineCharacter = '\0';
346             
347             if (strlen(filenameBuffer) == 0)
348                 continue;
349                 
350             runTest(filenameBuffer);
351             fflush(stdout);
352         }
353     } else {
354         printSeparators = (optind < argc-1 || (dumpPixels && dumpTree));
355         for (int i = optind; i != argc; ++i)
356             runTest(argv[i]);
357     }
358     
359     [workQueue release];
360
361     [webView setFrameLoadDelegate:nil];
362     [webView setEditingDelegate:nil];
363     [webView setUIDelegate:nil];
364     frame = nil;
365
366     // Work around problem where registering drag types leaves an outstanding
367     // "perform selector" on the window, which retains the window. It's a bit
368     // inelegant and perhaps dangerous to just blow them all away, but in practice
369     // it probably won't cause any trouble (and this is just a test tool, after all).
370     [NSObject cancelPreviousPerformRequestsWithTarget:window];
371     
372     [window close]; // releases when closed
373     [webView release];
374     [delegate release];
375     [editingDelegate release];
376
377     [localPasteboard releaseGlobally];
378     localPasteboard = nil;
379     
380     [navigationController release];
381     navigationController = nil;
382     
383     if (dumpPixels)
384         restoreColorSpace(0);
385     
386     [pool release];
387
388     return 0;
389 }
390
391 static int compareHistoryItems(id item1, id item2, void *context)
392 {
393     return [[item1 target] caseInsensitiveCompare:[item2 target]];
394 }
395
396 static void dumpHistoryItem(WebHistoryItem *item, int indent, BOOL current)
397 {
398     int start = 0;
399     if (current) {
400         printf("curr->");
401         start = 6;
402     }
403     for (int i = start; i < indent; i++)
404         putchar(' ');
405     printf("%s", [[item URLString] UTF8String]);
406     NSString *target = [item target];
407     if (target && [target length] > 0)
408         printf(" (in frame \"%s\")", [target UTF8String]);
409     if ([item isTargetItem])
410         printf("  **nav target**");
411     putchar('\n');
412     NSArray *kids = [item children];
413     if (kids) {
414         // must sort to eliminate arbitrary result ordering which defeats reproducible testing
415         kids = [kids sortedArrayUsingFunction:&compareHistoryItems context:nil];
416         for (unsigned i = 0; i < [kids count]; i++)
417             dumpHistoryItem([kids objectAtIndex:i], indent+4, NO);
418     }
419 }
420
421 static void dumpFrameScrollPosition(WebFrame *f)
422 {
423     NSPoint scrollPosition = [[[[f frameView] documentView] superview] bounds].origin;
424     if (ABS(scrollPosition.x) > 0.00000001 || ABS(scrollPosition.y) > 0.00000001) {
425         if ([f parentFrame] != nil)
426             printf("frame '%s' ", [[f name] UTF8String]);
427         printf("scrolled to %.f,%.f\n", scrollPosition.x, scrollPosition.y);
428     }
429     NSArray *kids = [f childFrames];
430     if (kids)
431         for (unsigned i = 0; i < [kids count]; i++)
432             dumpFrameScrollPosition([kids objectAtIndex:i]);
433 }
434
435 static void dump(void)
436 {
437     NSString *result = nil;
438     if (dumpTree) {
439         dumpAsText |= [[[[frame dataSource] response] MIMEType] isEqualToString:@"text/plain"];
440         if (dumpAsText) {
441             DOMElement *documentElement = [[frame DOMDocument] documentElement];
442             if ([documentElement isKindOfClass:[DOMHTMLElement class]])
443                 result = [[(DOMHTMLElement *)documentElement innerText] stringByAppendingString:@"\n"];
444             else
445                 result = [[documentElement valueForKey:@"textContent"] stringByAppendingString:@"\n"];
446         } else {
447             bool isSVGW3CTest = ([currentTest rangeOfString:@"svg/W3C-SVG-1.1"].length);
448             if (isSVGW3CTest)
449                 [[frame webView] setFrameSize:NSMakeSize(480, 360)];
450             else 
451                 [[frame webView] setFrameSize:NSMakeSize(maxViewWidth, maxViewHeight)];
452             result = [frame renderTreeAsExternalRepresentation];
453         }
454         
455         if (!result)
456             printf("ERROR: nil result from %s", dumpAsText ? "[documentElement innerText]" : "[frame renderTreeAsExternalRepresentation]");
457         else {
458             fputs([result UTF8String], stdout);
459             if (!dumpAsText)
460                 dumpFrameScrollPosition(frame);
461         }
462
463         if (dumpBackForwardList) {
464             printf("\n============== Back Forward List ==============\n");
465             WebBackForwardList *bfList = [[frame webView] backForwardList];
466
467             // Print out all items in the list after prevTestBFItem, which was from the previous test
468             // Gather items from the end of the list, the print them out from oldest to newest
469             NSMutableArray *itemsToPrint = [[NSMutableArray alloc] init];
470             for (int i = [bfList forwardListCount]; i > 0; i--) {
471                 WebHistoryItem *item = [bfList itemAtIndex:i];
472                 // something is wrong if the item from the last test is in the forward part of the b/f list
473                 assert(item != prevTestBFItem);
474                 [itemsToPrint addObject:item];
475             }
476             
477             assert([bfList currentItem] != prevTestBFItem);
478             [itemsToPrint addObject:[bfList currentItem]];
479             int currentItemIndex = [itemsToPrint count] - 1;
480
481             for (int i = -1; i >= -[bfList backListCount]; i--) {
482                 WebHistoryItem *item = [bfList itemAtIndex:i];
483                 if (item == prevTestBFItem)
484                     break;
485                 [itemsToPrint addObject:item];
486             }
487
488             for (int i = [itemsToPrint count]-1; i >= 0; i--) {
489                 dumpHistoryItem([itemsToPrint objectAtIndex:i], 8, i == currentItemIndex);
490             }
491             [itemsToPrint release];
492             printf("===============================================\n");
493         }
494
495         if (printSeparators)
496             puts("#EOF");
497     }
498     
499     if (dumpPixels) {
500         if (!dumpAsText) {
501             // grab a bitmap from the view
502             WebView* view = [frame webView];
503             NSSize webViewSize = [view frame].size;
504
505             CGContextRef cgContext = CGBitmapContextCreate(screenCaptureBuffer, webViewSize.width, webViewSize.height, 8, webViewSize.width * 4, sharedColorSpace, kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedLast);
506
507             NSGraphicsContext* savedContext = [[[NSGraphicsContext currentContext] retain] autorelease];
508             NSGraphicsContext* nsContext = [NSGraphicsContext graphicsContextWithGraphicsPort:cgContext flipped:NO];
509             [NSGraphicsContext setCurrentContext:nsContext];
510
511             if (readFromWindow) {
512                 NSBitmapImageRep *imageRep;
513                 [view displayIfNeeded];
514                 [view lockFocus];
515                 imageRep = [[NSBitmapImageRep alloc] initWithFocusedViewRect:[view frame]];
516                 [view unlockFocus];
517                 [imageRep draw];
518                 [imageRep release];
519             } else if (!testRepaint)
520                 [view displayRectIgnoringOpacity:NSMakeRect(0, 0, webViewSize.width, webViewSize.height) inContext:nsContext];
521             else if (!repaintSweepHorizontally) {
522                 NSRect line = NSMakeRect(0, 0, webViewSize.width, 1);
523                 while (line.origin.y < webViewSize.height) {
524                     [view displayRectIgnoringOpacity:line inContext:nsContext];
525                     line.origin.y++;
526                 }
527             } else {
528                 NSRect column = NSMakeRect(0, 0, 1, webViewSize.height);
529                 while (column.origin.x < webViewSize.width) {
530                     [view displayRectIgnoringOpacity:column inContext:nsContext];
531                     column.origin.x++;
532                 }
533             }
534             if (dumpSelectionRect) {
535                 NSView *documentView = [[frame frameView] documentView];
536                 if ([documentView conformsToProtocol:@protocol(WebDocumentSelection)]) {
537                     [[NSColor redColor] set];
538                     [NSBezierPath strokeRect:[documentView convertRect:[(id <WebDocumentSelection>)documentView selectionRect] fromView:nil]];
539                 }
540             }
541
542             [NSGraphicsContext setCurrentContext:savedContext];
543             
544             CGImageRef bitmapImage = CGBitmapContextCreateImage(cgContext);
545             CGContextRelease(cgContext);
546
547             // compute the actual hash to compare to the expected image's hash
548             NSString *actualHash = md5HashStringForBitmap(bitmapImage);
549             printf("\nActualHash: %s\n", [actualHash UTF8String]);
550
551             BOOL dumpImage;
552             if (dumpAllPixels)
553                 dumpImage = YES;
554             else {
555                 // FIXME: It's unfortunate that we hardcode the file naming scheme here.
556                 // At one time, the perl script had all the knowledge about file layout.
557                 // Some day we should restore that setup by passing in more parameters to this tool.
558                 NSString *baseTestPath = [currentTest stringByDeletingPathExtension];
559                 NSString *baselineHashPath = [baseTestPath stringByAppendingString:@"-expected.checksum"];
560                 NSString *baselineHash = [NSString stringWithContentsOfFile:baselineHashPath encoding:NSUTF8StringEncoding error:nil];
561                 NSString *baselineImagePath = [baseTestPath stringByAppendingString:@"-expected.png"];
562
563                 printf("BaselineHash: %s\n", [baselineHash UTF8String]);
564
565                 /// send the image to stdout if the hash mismatches or if there's no file in the file system
566                 dumpImage = ![baselineHash isEqualToString:actualHash] || access([baselineImagePath fileSystemRepresentation], F_OK) != 0;
567             }
568             
569             if (dumpImage) {
570                 CFMutableDataRef imageData = CFDataCreateMutable(0, 0);
571                 CGImageDestinationRef imageDest = CGImageDestinationCreateWithData(imageData, CFSTR("public.png"), 1, 0);
572                 CGImageDestinationAddImage(imageDest, bitmapImage, 0);
573                 CGImageDestinationFinalize(imageDest);
574                 CFRelease(imageDest);
575                 printf("Content-length: %lu\n", CFDataGetLength(imageData));
576                 fwrite(CFDataGetBytePtr(imageData), 1, CFDataGetLength(imageData), stdout);
577                 CFRelease(imageData);
578             }
579
580             CGImageRelease(bitmapImage);
581         }
582
583         printf("#EOF\n");
584     }
585
586     done = YES;
587 }
588
589 @implementation WaitUntilDoneDelegate
590
591 // Exec messages in the work queue until they're all done, or one of them starts a new load
592 - (void)processWork:(id)dummy
593 {
594     // quit doing work once a load is in progress
595     while ([workQueue count] > 0 && !topLoadingFrame) {
596         [[workQueue objectAtIndex:0] invoke];
597         [workQueue removeObjectAtIndex:0];
598     }
599     
600     // if we didn't start a new load, then we finished all the commands, so we're ready to dump state
601     if (!topLoadingFrame && !waitToDump)
602         dump();
603 }
604
605 - (void)webView:(WebView *)c locationChangeDone:(NSError *)error forDataSource:(WebDataSource *)dataSource
606 {
607     if ([dataSource webFrame] == topLoadingFrame) {
608         topLoadingFrame = nil;
609         workQueueFrozen = YES;      // first complete load freezes the queue for the rest of this test
610         if (!waitToDump) {
611             if ([workQueue count] > 0)
612                 [self performSelector:@selector(processWork:) withObject:nil afterDelay:0];
613             else
614                 dump();
615         }
616     }
617 }
618
619 - (void)webView:(WebView *)sender didStartProvisionalLoadForFrame:(WebFrame *)f
620 {
621     // Make sure we only set this once per test.  If it gets cleared, and then set again, we might
622     // end up doing two dumps for one test.
623     if (!topLoadingFrame && !done)
624         topLoadingFrame = f;
625 }
626
627 - (void)webView:(WebView *)sender didCommitLoadForFrame:(WebFrame *)f
628 {
629     windowIsKey = YES;
630     NSView *documentView = [[frame frameView] documentView];
631     [[[frame webView] window] makeFirstResponder:documentView];
632     if ([documentView isKindOfClass:[WebHTMLView class]])
633         [(WebHTMLView *)documentView _updateActiveState];
634 }
635
636 - (void)webView:(WebView *)sender didFailProvisionalLoadWithError:(NSError *)error forFrame:(WebFrame *)frame
637 {
638     [self webView:sender locationChangeDone:error forDataSource:[frame provisionalDataSource]];
639 }
640
641 - (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame
642 {
643     [self webView:sender locationChangeDone:nil forDataSource:[frame dataSource]];
644     [navigationController webView:sender didFinishLoadForFrame:frame];
645 }
646
647 - (void)webView:(WebView *)sender didFailLoadWithError:(NSError *)error forFrame:(WebFrame *)frame
648 {
649     [self webView:sender locationChangeDone:error forDataSource:[frame dataSource]];
650 }
651
652 - (void)webView:(WebView *)sender windowScriptObjectAvailable:(WebScriptObject *)obj 
653
654     LayoutTestController *ltc = [[LayoutTestController alloc] init];
655     [obj setValue:ltc forKey:@"layoutTestController"];
656     [ltc release];
657     
658     EventSendingController *esc = [[EventSendingController alloc] init];
659     [obj setValue:esc forKey:@"eventSender"];
660     [esc release];
661     
662     TextInputController *tic = [[TextInputController alloc] initWithWebView:sender];
663     [obj setValue:tic forKey:@"textInputController"];
664     [tic release];
665     
666     AppleScriptController *asc = [[AppleScriptController alloc] initWithWebView:sender];
667     [obj setValue:asc forKey:@"appleScriptController"];
668     [asc release];
669     
670     GCController *gcc = [[GCController alloc] init];
671     [obj setValue:gcc forKey:@"GCController"];
672     [gcc release];
673     
674     [obj setValue:navigationController forKey:@"navigationController"];
675     
676     ObjCPlugin *plugin = [[ObjCPlugin alloc] init];
677     [obj setValue:plugin forKey:@"objCPlugin"];
678     [plugin release];
679     
680     ObjCPluginFunction *pluginFunction = [[ObjCPluginFunction alloc] init];
681     [obj setValue:pluginFunction forKey:@"objCPluginFunction"];
682     [pluginFunction release];
683 }
684
685 - (void)webView:(WebView *)sender runJavaScriptAlertPanelWithMessage:(NSString *)message
686 {
687     printf("ALERT: %s\n", [message UTF8String]);
688 }
689
690 - (void)webView:(WebView *)sender didReceiveTitle:(NSString *)title forFrame:(WebFrame *)frame
691 {
692     if (dumpTitleChanges)
693         printf("TITLE CHANGED: %s\n", [title UTF8String]);
694 }
695
696 - (void)webView:(WebView *)sender dragImage:(NSImage *)anImage at:(NSPoint)viewLocation offset:(NSSize)initialOffset event:(NSEvent *)event pasteboard:(NSPasteboard *)pboard source:(id)sourceObj slideBack:(BOOL)slideFlag forView:(NSView *)view
697 {
698     // A new drag was started before the old one ended.  Probably shouldn't happen.
699     if (draggingInfo) {
700         [[draggingInfo draggingSource] draggedImage:[draggingInfo draggedImage] endedAt:lastMousePosition operation:NSDragOperationNone];
701         [draggingInfo release];
702     }
703     draggingInfo = [[DumpRenderTreeDraggingInfo alloc] initWithImage:anImage offset:initialOffset pasteboard:pboard source:sourceObj];
704 }
705
706 - (void)webViewFocus:(WebView *)webView
707 {
708     windowIsKey = YES;
709     NSView *documentView = [[frame frameView] documentView];
710     if ([documentView isKindOfClass:[WebHTMLView class]])
711         [(WebHTMLView *)documentView _updateActiveState];
712 }
713
714 @end
715
716 @implementation LayoutTestController
717
718 + (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector
719 {
720     if (aSelector == @selector(waitUntilDone)
721             || aSelector == @selector(notifyDone)
722             || aSelector == @selector(dumpAsText)
723             || aSelector == @selector(dumpTitleChanges)
724             || aSelector == @selector(dumpBackForwardList)
725             || aSelector == @selector(setWindowIsKey:)
726             || aSelector == @selector(setMainFrameIsFirstResponder:)
727             || aSelector == @selector(dumpSelectionRect)
728             || aSelector == @selector(display)
729             || aSelector == @selector(testRepaint)
730             || aSelector == @selector(repaintSweepHorizontally)
731             || aSelector == @selector(queueBackNavigation:)
732             || aSelector == @selector(queueForwardNavigation:)
733             || aSelector == @selector(queueReload)
734             || aSelector == @selector(queueScript:)
735             || aSelector == @selector(queueLoad:target:)
736             || aSelector == @selector(clearBackForwardList)
737             || aSelector == @selector(keepWebHistory)
738             || aSelector == @selector(setAcceptsEditing:))
739         return NO;
740     return YES;
741 }
742
743 + (NSString *)webScriptNameForSelector:(SEL)aSelector
744 {
745     if (aSelector == @selector(setWindowIsKey:))
746         return @"setWindowIsKey";
747     if (aSelector == @selector(setMainFrameIsFirstResponder:))
748         return @"setMainFrameIsFirstResponder";
749     if (aSelector == @selector(queueBackNavigation:))
750         return @"queueBackNavigation";
751     if (aSelector == @selector(queueForwardNavigation:))
752         return @"queueForwardNavigation";
753     if (aSelector == @selector(queueScript:))
754         return @"queueScript";
755     if (aSelector == @selector(queueLoad:target:))
756         return @"queueLoad";
757     if (aSelector == @selector(setAcceptsEditing:))
758         return @"setAcceptsEditing";
759     return nil;
760 }
761
762 - (void)clearBackForwardList
763 {
764     WebBackForwardList *backForwardList = [[frame webView] backForwardList];
765     WebHistoryItem *item = [[backForwardList currentItem] retain];
766
767     // We clear the history by setting the back/forward list's capacity to 0
768     // then restoring it back and adding back the current item.
769     int capacity = [backForwardList capacity];
770     [backForwardList setCapacity:0];
771     [backForwardList setCapacity:capacity];
772     [backForwardList addItem:item];
773     [backForwardList goToItem:item];
774     [item release];
775 }
776
777 - (void)keepWebHistory
778 {
779     if (![WebHistory optionalSharedHistory]) {
780         WebHistory *history = [[WebHistory alloc] init];
781         [WebHistory setOptionalSharedHistory:history];
782         [history release];
783     }
784 }
785
786 - (void)waitUntilDone 
787 {
788     waitToDump = YES;
789 }
790
791 - (void)notifyDone
792 {
793     if (waitToDump && !topLoadingFrame && [workQueue count] == 0)
794         dump();
795     waitToDump = NO;
796 }
797
798 - (void)dumpAsText
799 {
800     dumpAsText = YES;
801 }
802
803 - (void)dumpSelectionRect
804 {
805     dumpSelectionRect = YES;
806 }
807
808 - (void)dumpTitleChanges
809 {
810     dumpTitleChanges = YES;
811 }
812
813 - (void)dumpBackForwardList
814 {
815     dumpBackForwardList = YES;
816 }
817
818 - (void)setWindowIsKey:(BOOL)flag
819 {
820     windowIsKey = flag;
821     NSView *documentView = [[frame frameView] documentView];
822     if ([documentView isKindOfClass:[WebHTMLView class]])
823         [(WebHTMLView *)documentView _updateActiveState];
824 }
825
826 - (void)setMainFrameIsFirstResponder:(BOOL)flag
827 {
828     NSView *documentView = [[frame frameView] documentView];
829     
830     NSResponder *firstResponder = flag ? documentView : nil;
831     [[[frame webView] window] makeFirstResponder:firstResponder];
832         
833     if ([documentView isKindOfClass:[WebHTMLView class]])
834         [(WebHTMLView *)documentView _updateActiveState];
835 }
836
837 - (void)display
838 {
839     NSView *webView = [frame webView];
840     [webView display];
841     [webView lockFocus];
842     [[[NSColor blackColor] colorWithAlphaComponent:0.66] set];
843     NSRectFillUsingOperation([webView frame], NSCompositeSourceOver);
844     [webView unlockFocus];
845     readFromWindow = YES;
846 }
847
848 - (void)testRepaint
849 {
850     testRepaint = YES;
851 }
852
853 - (void)repaintSweepHorizontally
854 {
855     repaintSweepHorizontally = YES;
856 }
857
858 - (id)invokeUndefinedMethodFromWebScript:(NSString *)name withArguments:(NSArray *)args
859 {
860     return nil;
861 }
862
863 - (void)_addWorkForTarget:(id)target selector:(SEL)selector arg1:(id)arg1 arg2:(id)arg2
864 {
865     if (workQueueFrozen)
866         return;
867     NSMethodSignature *sig = [target methodSignatureForSelector:selector];
868     NSInvocation *work = [NSInvocation invocationWithMethodSignature:sig];
869     [work retainArguments];
870     [work setTarget:target];
871     [work setSelector:selector];
872     if (arg1) {
873         [work setArgument:&arg1 atIndex:2];
874         if (arg2)
875             [work setArgument:&arg2 atIndex:3];
876     }
877     [workQueue addObject:work];
878 }
879
880 - (void)_doLoad:(NSURL *)url target:(NSString *)target
881 {
882     WebFrame *targetFrame;
883     if (target && ![target isKindOfClass:[WebUndefined class]])
884         targetFrame = [frame findFrameNamed:target];
885     else
886         targetFrame = frame;
887     [targetFrame loadRequest:[NSURLRequest requestWithURL:url]];
888 }
889
890 - (void)_doBackOrForwardNavigation:(NSNumber *)index
891 {
892     int bfIndex = [index intValue];
893     if (bfIndex == 1)
894         [[frame webView] goForward];
895     if (bfIndex == -1)
896         [[frame webView] goBack];
897     else {        
898         WebBackForwardList *bfList = [[frame webView] backForwardList];
899         [[frame webView] goToBackForwardItem:[bfList itemAtIndex:bfIndex]];
900     }
901 }
902
903 - (void)queueBackNavigation:(int)howFarBack
904 {
905     [self _addWorkForTarget:self selector:@selector(_doBackOrForwardNavigation:) arg1:[NSNumber numberWithInt:-howFarBack] arg2:nil];
906 }
907
908 - (void)queueForwardNavigation:(int)howFarForward
909 {
910     [self _addWorkForTarget:self selector:@selector(_doBackOrForwardNavigation:) arg1:[NSNumber numberWithInt:howFarForward] arg2:nil];
911 }
912
913 - (void)queueReload
914 {
915     [self _addWorkForTarget:[frame webView] selector:@selector(reload:) arg1:self arg2:nil];
916 }
917
918 - (void)queueScript:(NSString *)script
919 {
920     [self _addWorkForTarget:[frame webView] selector:@selector(stringByEvaluatingJavaScriptFromString:) arg1:script arg2:nil];
921 }
922
923 - (void)queueLoad:(NSString *)URLString target:(NSString *)target
924 {
925     NSURL *URL = [NSURL URLWithString:URLString relativeToURL:[[[frame dataSource] response] URL]];
926     [self _addWorkForTarget:self selector:@selector(_doLoad:target:) arg1:URL arg2:target];
927 }
928
929 - (void)setAcceptsEditing:(BOOL)newAcceptsEditing
930 {
931     [(EditingDelegate *)[[frame webView] editingDelegate] setAcceptsEditing:newAcceptsEditing];
932 }
933
934 @end
935
936 static void runTest(const char *pathOrURL)
937 {
938     CFStringRef pathOrURLString = CFStringCreateWithCString(NULL, pathOrURL, kCFStringEncodingUTF8);
939     if (!pathOrURLString) {
940         fprintf(stderr, "can't parse filename as UTF-8\n");
941         return;
942     }
943
944     CFURLRef URL;
945     if (CFStringHasPrefix(pathOrURLString, CFSTR("http://")))
946         URL = CFURLCreateWithString(NULL, pathOrURLString, NULL);
947     else
948         URL = CFURLCreateWithFileSystemPath(NULL, pathOrURLString, kCFURLPOSIXPathStyle, FALSE);
949     
950     if (!URL) {
951         CFRelease(pathOrURLString);
952         fprintf(stderr, "can't turn %s into a CFURL\n", pathOrURL);
953         return;
954     }
955
956     [(EditingDelegate *)[[frame webView] editingDelegate] setAcceptsEditing:YES];
957     done = NO;
958     topLoadingFrame = nil;
959     waitToDump = NO;
960     dumpAsText = NO;
961     dumpSelectionRect = NO;
962     dumpTitleChanges = NO;
963     dumpBackForwardList = NO;
964     readFromWindow = NO;
965     testRepaint = testRepaintDefault;
966     repaintSweepHorizontally = repaintSweepHorizontallyDefault;
967     if ([WebHistory optionalSharedHistory])
968         [WebHistory setOptionalSharedHistory:nil];
969     lastMousePosition = NSMakePoint(0, 0);
970
971     if (currentTest != nil)
972         CFRelease(currentTest);
973     currentTest = (NSString *)pathOrURLString;
974     [prevTestBFItem release];
975     prevTestBFItem = [[[[frame webView] backForwardList] currentItem] retain];
976     [workQueue removeAllObjects];
977     workQueueFrozen = NO;
978
979     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
980     [frame loadRequest:[NSURLRequest requestWithURL:(NSURL *)URL]];
981     CFRelease(URL);
982     [pool release];
983     while (!done) {
984         pool = [[NSAutoreleasePool alloc] init];
985         [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantPast]];
986         [pool release];
987     }
988     pool = [[NSAutoreleasePool alloc] init];
989     [[frame webView] setSelectedDOMRange:nil affinity:NSSelectionAffinityDownstream];
990     if (draggingInfo)
991         [draggingInfo release];
992     draggingInfo = nil;
993     [pool release];
994 }
995
996 /* Hashes a bitmap and returns a text string for comparison and saving to a file */
997 static NSString *md5HashStringForBitmap(CGImageRef bitmap)
998 {
999     MD5_CTX md5Context;
1000     unsigned char hash[16];
1001     
1002     unsigned bitsPerPixel = CGImageGetBitsPerPixel(bitmap);
1003     assert(bitsPerPixel == 32); // ImageDiff assumes 32 bit RGBA, we must as well.
1004     unsigned bytesPerPixel = bitsPerPixel / 8;
1005     unsigned pixelsHigh = CGImageGetHeight(bitmap);
1006     unsigned pixelsWide = CGImageGetWidth(bitmap);
1007     unsigned bytesPerRow = CGImageGetBytesPerRow(bitmap);
1008     assert(bytesPerRow >= (pixelsWide * bytesPerPixel));
1009     
1010     MD5_Init(&md5Context);
1011     unsigned char *bitmapData = screenCaptureBuffer;
1012     for (unsigned row = 0; row < pixelsHigh; row++) {
1013         MD5_Update(&md5Context, bitmapData, pixelsWide * bytesPerPixel);
1014         bitmapData += bytesPerRow;
1015     }
1016     MD5_Final(hash, &md5Context);
1017     
1018     char hex[33] = "";
1019     for (int i = 0; i < 16; i++) {
1020        snprintf(hex, 33, "%s%02x", hex, hash[i]);
1021     }
1022
1023     return [NSString stringWithUTF8String:hex];
1024 }
1025
1026 @implementation DumpRenderTreePasteboard
1027
1028 // Return a local pasteboard so we don't disturb the real pasteboard when running tests.
1029 + (NSPasteboard *)generalPasteboard
1030 {
1031     return localPasteboard;
1032 }
1033
1034 @end
1035
1036 @implementation DumpRenderTreeWindow
1037
1038 - (BOOL)isKeyWindow
1039 {
1040     return windowIsKey;
1041 }
1042
1043 @end
1044
1045 @implementation DumpRenderTreeEvent
1046
1047 + (NSPoint)mouseLocation
1048 {
1049     return [[[frame webView] window] convertBaseToScreen:lastMousePosition];
1050 }
1051
1052 @end