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