Changes by Ben Lamonica and Eric Seidel, reviewed mostly by Eric and
[WebKit-https.git] / WebKitTools / DumpRenderTree / DumpRenderTree.m
1 /*
2  * Copyright (C) 2005 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 <Foundation/NSURLRequest.h>
30 #import <Foundation/NSError.h>
31
32 #import <WebKit/DOMExtensions.h>
33 #import <WebKit/DOMRange.h>
34 #import <WebKit/WebCoreStatistics.h>
35 #import <WebKit/WebDataSource.h>
36 #import <WebKit/WebFrame.h>
37 #import <WebKit/WebFrameLoadDelegate.h>
38 #import <WebKit/WebEditingDelegate.h>
39 #import <WebKit/WebFrameView.h>
40 #import <WebKit/WebPreferences.h>
41 #import <WebKit/WebView.h>
42
43 #define COMMON_DIGEST_FOR_OPENSSL
44 #import <CommonCrypto/CommonDigest.h>
45 #import <getopt.h>
46
47 @interface WaitUntilDoneDelegate : NSObject
48 @end
49
50 @interface EditingDelegate : NSObject
51 @end
52
53 @interface LayoutTestController : NSObject
54 @end
55
56 static void dumpRenderTree(const char *filename);
57 NSString *md5HashStringForBitmap(NSBitmapImageRep *bitmap);
58
59
60 static volatile BOOL done;
61 static WebFrame *frame;
62 static BOOL readyToDump;
63 static BOOL waitToDump;
64 static BOOL dumpAsText;
65 static BOOL dumpTitleChanges;
66 static int dumpPixels = NO;
67 static int dumpTree = YES;
68 static BOOL printSeparators;
69 static NSString *currentTest = nil;
70
71 int main(int argc, const char *argv[])
72 {
73     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
74
75     WebPreferences *preferences = [WebPreferences standardPreferences];
76     
77     NSString *standardFontFamily = [preferences standardFontFamily];
78     NSString *fixedFontFamily = [preferences fixedFontFamily];
79     NSString *serifFontFamily = [preferences serifFontFamily];
80     NSString *sansSerifFontFamily = [preferences sansSerifFontFamily];
81     NSString *cursiveFontFamily = [preferences cursiveFontFamily];
82     NSString *fantasyFontFamily = [preferences fantasyFontFamily];
83     int defaultFontSize = [preferences defaultFontSize];
84     int defaultFixedFontSize = [preferences defaultFixedFontSize];
85     int minimumFontSize = [preferences minimumFontSize];
86     int width = 800;
87     int height = 600;
88     
89     struct option options[] = {
90         {"width", required_argument, NULL, 'w'},
91         {"height", required_argument, NULL, 'h'},
92         {"pixel-tests", no_argument, &dumpPixels, YES},
93         {"tree", no_argument, &dumpTree, YES},
94         {"notree", no_argument, &dumpTree, NO},
95         {NULL, 0, NULL, 0}
96     };
97     int option;
98
99     [preferences setStandardFontFamily:@"Times"];
100     [preferences setFixedFontFamily:@"Courier"];
101     [preferences setSerifFontFamily:@"Times"];
102     [preferences setSansSerifFontFamily:@"Helvetica"];
103     [preferences setCursiveFontFamily:@"Apple Chancery"];
104     [preferences setFantasyFontFamily:@"Papyrus"];
105     [preferences setDefaultFontSize:16];
106     [preferences setDefaultFixedFontSize:13];
107     [preferences setMinimumFontSize:9];
108
109     while ((option = getopt_long(argc, (char * const *)argv, "", options, NULL)) != -1)
110         switch (option) {
111             case 'w':
112                 width = strtol(optarg, NULL, 0);
113                 if (width <= 0) {
114                     fprintf(stderr, "%s: invalid width\n", argv[0]);
115                     exit(1);
116                 }
117                 break;
118             case 'h':
119                 height = strtol(optarg, NULL, 0);
120                 if (height <= 0) {
121                     fprintf(stderr, "%s: invalid height\n", argv[0]);
122                     exit(1);
123                 }
124                 break;
125             case '?':   // unknown or ambiguous option
126             case ':':   // missing argument
127                 exit(1);
128                 break;
129         }
130     
131     WebView *webView = [[WebView alloc] initWithFrame:NSMakeRect(0, 0, width, height)];
132     WaitUntilDoneDelegate *delegate = [[WaitUntilDoneDelegate alloc] init];
133     EditingDelegate *editingDelegate = [[EditingDelegate alloc] init];
134     [webView setFrameLoadDelegate:delegate];
135     [webView setEditingDelegate:editingDelegate];
136     [webView setUIDelegate:delegate];
137     frame = [webView mainFrame];
138     
139     // For reasons that are not entirely clear, the following pair of calls makes WebView handle its
140     // dynamic scrollbars properly. Without it, every frame will always have scrollbars.
141     NSBitmapImageRep *imageRep = [webView bitmapImageRepForCachingDisplayInRect:[webView bounds]];
142     [webView cacheDisplayInRect:[webView bounds] toBitmapImageRep:imageRep];
143
144     if (argc == optind+1 && strcmp(argv[optind], "-") == 0) {
145         char filenameBuffer[2048];
146         printSeparators = YES;
147         while (fgets(filenameBuffer, sizeof(filenameBuffer), stdin)) {
148             char *newLineCharacter = strchr(filenameBuffer, '\n');
149             if (newLineCharacter)
150                 *newLineCharacter = '\0';
151             
152             if (strlen(filenameBuffer) == 0)
153                 continue;
154                 
155             dumpRenderTree(filenameBuffer);
156             fflush(stdout);
157         }
158     } else {
159         printSeparators = (optind < argc-1 || (dumpPixels && dumpTree));
160         for (int i = optind; i != argc; ++i) {
161             dumpRenderTree(argv[i]);
162         }
163     }
164     
165     [preferences setStandardFontFamily:standardFontFamily];
166     [preferences setFixedFontFamily:fixedFontFamily];
167     [preferences setSerifFontFamily:serifFontFamily];
168     [preferences setSansSerifFontFamily:sansSerifFontFamily];
169     [preferences setCursiveFontFamily:cursiveFontFamily];
170     [preferences setFantasyFontFamily:fantasyFontFamily];
171     [preferences setDefaultFontSize:defaultFontSize];
172     [preferences setDefaultFixedFontSize:defaultFixedFontSize];
173     [preferences setMinimumFontSize:minimumFontSize];
174
175     [pool release];
176     return 0;
177 }
178
179 static void dump(void)
180 {
181     NSString *result = nil;
182     if (dumpTree) {
183         if (dumpAsText) {
184             DOMDocument *document = [frame DOMDocument];
185             if ([document isKindOfClass:[DOMHTMLDocument class]])
186                 result = [[[(DOMHTMLDocument *)document body] innerText] stringByAppendingString:@"\n"];
187         } else
188             result = [frame renderTreeAsExternalRepresentation];
189         
190         if (!result)
191             puts("error");
192         else
193             fputs([result UTF8String], stdout);
194         
195         if (printSeparators)
196             puts("#EOF");
197     }
198     
199     if (dumpPixels) {
200         if (!dumpAsText) {
201             NSString *baseTestPath = [currentTest stringByDeletingPathExtension];
202             NSString *baselineHashPath = [baseTestPath stringByAppendingString:@"-expected.checksum"];
203             NSString *baselineHash = [NSString stringWithContentsOfFile:baselineHashPath encoding:NSUTF8StringEncoding error:nil];
204             
205             // grab a bitmap from the view
206             WebView *view = [frame webView];
207             NSBitmapImageRep *imageRep = [view bitmapImageRepForCachingDisplayInRect:[view frame]];
208             [view cacheDisplayInRect:[view frame] toBitmapImageRep:imageRep];
209             
210             // has the actual hash to compare to the expected image's hash
211             NSString *actualHash = md5HashStringForBitmap(imageRep);
212             printf("\nActualHash: %s\n", [actualHash UTF8String]);
213             printf("BaselineHash: %s\n", [baselineHash UTF8String]);
214             
215             // if the hashes don't match, send image back to stdout for diff comparision
216             if ([baselineHash isEqualToString:actualHash] == NO) {            
217                 NSData *imageData = [imageRep representationUsingType:NSPNGFileType properties:nil];
218                 printf("Content-length: %d\n", [imageData length]);
219                 fwrite([imageData bytes], 1, [imageData length], stdout);
220             }
221         }
222
223         printf("#EOF\n");
224     }
225
226     done = YES;
227 }
228
229 @implementation WaitUntilDoneDelegate
230
231 - (void)webView:(WebView *)c locationChangeDone:(NSError *)error forDataSource:(WebDataSource *)dataSource
232 {
233     if ([dataSource webFrame] == frame) {
234         if (waitToDump)
235             readyToDump = YES;
236         else
237             dump();
238     }
239 }
240
241 - (void)webView:(WebView *)sender didCommitLoadForFrame:(WebFrame *)f
242 {
243     if (frame == f)
244         readyToDump = NO;
245 }
246
247 - (void)webView:(WebView *)sender didFailProvisionalLoadWithError:(NSError *)error forFrame:(WebFrame *)frame
248 {
249     [self webView:sender locationChangeDone:error forDataSource:[frame provisionalDataSource]];
250 }
251
252 - (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame
253 {
254     [self webView:sender locationChangeDone:nil forDataSource:[frame dataSource]];
255 }
256
257 - (void)webView:(WebView *)sender didFailLoadWithError:(NSError *)error forFrame:(WebFrame *)frame
258 {
259     [self webView:sender locationChangeDone:error forDataSource:[frame dataSource]];
260 }
261
262 - (void)webView:(WebView *)sender windowScriptObjectAvailable:(WebScriptObject *)obj 
263
264     LayoutTestController *ltc = [[LayoutTestController alloc] init];
265     [(id)obj setValue:ltc forKey:@"layoutTestController"];
266     [ltc release];
267
268 }
269
270 - (void)webView:(WebView *)sender runJavaScriptAlertPanelWithMessage:(NSString *)message
271 {
272     printf("ALERT: %s\n", [message UTF8String]);
273 }
274
275 - (void)webView:(WebView *)sender didReceiveTitle:(NSString *)title forFrame:(WebFrame *)frame
276 {
277     if (dumpTitleChanges)
278         printf("TITLE CHANGED: %s\n", [title UTF8String]);
279 }
280
281 @end
282
283 @interface DOMNode (dumpPath)
284 - (NSString *)dumpPath;
285 @end
286
287 @implementation DOMNode (dumpPath)
288 - (NSString *)dumpPath
289 {
290     DOMNode *parent = [self parentNode];
291     NSString *str = [NSString stringWithFormat:@"%@", [self nodeName]];
292     if (parent != nil) {
293         str = [str stringByAppendingString:@" > "];
294         str = [str stringByAppendingString:[parent dumpPath]];
295     }
296     return str;
297 }
298 @end
299
300 @interface DOMRange (dump)
301 - (NSString *)dump;
302 @end
303
304 @implementation DOMRange (dump)
305 - (NSString *)dump
306 {
307     return [NSString stringWithFormat:@"range from %ld of %@ to %ld of %@", [self startOffset], [[self startContainer] dumpPath], [self endOffset], [[self endContainer] dumpPath]];
308 }
309 @end
310
311
312 @implementation EditingDelegate
313
314 - (BOOL)webView:(WebView *)webView shouldBeginEditingInDOMRange:(DOMRange *)range
315 {
316     printf("EDITING DELEGATE: shouldBeginEditingInDOMRange:%s\n", [[range dump] UTF8String]);
317     return YES;
318 }
319
320 - (BOOL)webView:(WebView *)webView shouldEndEditingInDOMRange:(DOMRange *)range
321 {
322     printf("EDITING DELEGATE: shouldEndEditingInDOMRange:%s\n", [[range dump] UTF8String]);
323     return YES;
324 }
325
326 - (BOOL)webView:(WebView *)webView shouldInsertNode:(DOMNode *)node replacingDOMRange:(DOMRange *)range givenAction:(WebViewInsertAction)action
327 {
328     static const char *insertactionstring[] = {
329         "WebViewInsertActionTyped",
330         "WebViewInsertActionPasted",
331         "WebViewInsertActionDropped",
332     };
333
334     printf("EDITING DELEGATE: shouldInsertNode:%s replacingDOMRange:%s givenAction:%s\n", [[node dumpPath] UTF8String], [[range dump] UTF8String], insertactionstring[action]);
335     return YES;
336 }
337
338 - (BOOL)webView:(WebView *)webView shouldInsertText:(NSString *)text replacingDOMRange:(DOMRange *)range givenAction:(WebViewInsertAction)action
339 {
340     static const char *insertactionstring[] = {
341         "WebViewInsertActionTyped",
342         "WebViewInsertActionPasted",
343         "WebViewInsertActionDropped",
344     };
345
346     printf("EDITING DELEGATE: shouldInsertText:%s replacingDOMRange:%s givenAction:%s\n", [[text description] UTF8String], [[range dump] UTF8String], insertactionstring[action]);
347     return YES;
348 }
349
350 - (BOOL)webView:(WebView *)webView shouldDeleteDOMRange:(DOMRange *)range
351 {
352     printf("EDITING DELEGATE: shouldDeleteDOMRange:%s\n", [[range dump] UTF8String]);
353     return YES;
354 }
355
356 - (BOOL)webView:(WebView *)webView shouldChangeSelectedDOMRange:(DOMRange *)currentRange toDOMRange:(DOMRange *)proposedRange affinity:(NSSelectionAffinity)selectionAffinity stillSelecting:(BOOL)flag
357 {
358     static const char *affinitystring[] = {
359         "NSSelectionAffinityUpstream",
360         "NSSelectionAffinityDownstream"
361     };
362     static const char *boolstring[] = {
363         "FALSE",
364         "TRUE"
365     };
366
367     printf("EDITING DELEGATE: shouldChangeSelectedDOMRange:%s toDOMRange:%s affinity:%s stillSelecting:%s\n", [[currentRange dump] UTF8String], [[proposedRange dump] UTF8String], affinitystring[selectionAffinity], boolstring[flag]);
368     return YES;
369 }
370
371 - (BOOL)webView:(WebView *)webView shouldApplyStyle:(DOMCSSStyleDeclaration *)style toElementsInDOMRange:(DOMRange *)range
372 {
373     printf("EDITING DELEGATE: shouldApplyStyle:%s toElementsInDOMRange:%s\n", [[style description] UTF8String], [[range dump] UTF8String]);
374     return YES;
375 }
376
377 - (BOOL)webView:(WebView *)webView shouldChangeTypingStyle:(DOMCSSStyleDeclaration *)currentStyle toStyle:(DOMCSSStyleDeclaration *)proposedStyle
378 {
379     printf("EDITING DELEGATE: shouldChangeTypingStyle:%s toStyle:%s\n", [[currentStyle description] UTF8String], [[proposedStyle description] UTF8String]);
380     return YES;
381 }
382
383 - (void)webViewDidBeginEditing:(NSNotification *)notification
384 {
385     printf("EDITING DELEGATE: webViewDidBeginEditing:%s\n", [[notification name] UTF8String]);
386 }
387
388 - (void)webViewDidChange:(NSNotification *)notification
389 {
390     printf("EDITING DELEGATE: webViewDidChange:%s\n", [[notification name] UTF8String]);
391 }
392
393 - (void)webViewDidEndEditing:(NSNotification *)notification
394 {
395     printf("EDITING DELEGATE: webViewDidEndEditing:%s\n", [[notification name] UTF8String]);
396 }
397
398 - (void)webViewDidChangeTypingStyle:(NSNotification *)notification
399 {
400     printf("EDITING DELEGATE: webViewDidChangeTypingStyle:%s\n", [[notification name] UTF8String]);
401 }
402
403 - (void)webViewDidChangeSelection:(NSNotification *)notification
404 {
405     if (!done)
406         printf("EDITING DELEGATE: webViewDidChangeSelection:%s\n", [[notification name] UTF8String]);
407 }
408
409 @end
410
411 @implementation LayoutTestController
412
413 + (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector
414 {
415     if (aSelector == @selector(waitUntilDone)
416             || aSelector == @selector(notifyDone)
417             || aSelector == @selector(dumpAsText)
418             || aSelector == @selector(dumpTitleChanges))
419         return NO;
420     return YES;
421 }
422
423 - (void)waitUntilDone 
424 {
425     waitToDump = YES;
426 }
427
428 - (void)notifyDone
429 {
430     if (waitToDump && readyToDump)
431         dump();
432     waitToDump = NO;
433 }
434
435 - (void)dumpAsText
436 {
437     dumpAsText = YES;
438 }
439
440 - (void)dumpTitleChanges
441 {
442     dumpTitleChanges = YES;
443 }
444
445 @end
446
447 static void dumpRenderTree(const char *filename)
448 {
449
450     CFStringRef filenameString = CFStringCreateWithCString(NULL, filename, kCFStringEncodingUTF8);
451     if (filenameString == NULL) {
452         fprintf(stderr, "can't parse filename as UTF-8\n");
453         return;
454     }
455
456     CFURLRef URL = CFURLCreateWithFileSystemPath(NULL, filenameString, kCFURLPOSIXPathStyle, FALSE);
457     if (URL == NULL) {
458         fprintf(stderr, "can't turn %s into a CFURL\n", filename);
459         return;
460     }
461
462     done = NO;
463     readyToDump = NO;
464     waitToDump = NO;
465     dumpAsText = NO;
466     dumpTitleChanges = NO;
467     currentTest = (NSString *) filenameString;
468
469     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
470     [frame loadRequest:[NSURLRequest requestWithURL:(NSURL *)URL]];
471     [pool release];
472     while (!done) {
473         pool = [[NSAutoreleasePool alloc] init];
474         [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantPast]];
475         [pool release];
476     }
477     [[frame webView] setSelectedDOMRange:nil affinity:NSSelectionAffinityDownstream];
478 }
479
480 /* Hashes a bitmap and returns a text string for comparison and saving to a file */
481 NSString *md5HashStringForBitmap(NSBitmapImageRep *bitmap)
482 {
483     MD5_CTX md5Context;
484     unsigned char hash[16];
485
486     MD5_Init(&md5Context);
487     MD5_Update(&md5Context, [bitmap bitmapData], [bitmap bytesPerPlane]);
488     MD5_Final(hash, &md5Context);
489     
490     char hex[33] = "";
491     for (int i = 0; i < 16; i++) {
492        snprintf(hex, 33, "%s%02x", hex, hash[i]);
493     }
494
495     return [NSString stringWithUTF8String:hex];
496 }
497