aboutsummaryrefslogtreecommitdiff
path: root/UnitTesting/GTMAppKitUnitTestingUtilities.m
blob: ff9442ecc7add191ee867845f233c3ed783fecd3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
//
//  GTMAppKitUnitTestingUtilities.m
//
//  Copyright 2006-2008 Google Inc.
//
//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
//  use this file except in compliance with the License.  You may obtain a copy
//  of the License at
//
//  http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
//  License for the specific language governing permissions and limitations under
//  the License.
//

#import "GTMAppKitUnitTestingUtilities.h"
#import <AppKit/AppKit.h>
#include <signal.h>
#include <unistd.h>
#import "GTMDefines.h"
#import "GTMGarbageCollection.h"

// The Users profile before we change it on them
static CMProfileRef gGTMCurrentColorProfile = NULL;

// Compares two color profiles
static BOOL GTMAreCMProfilesEqual(CMProfileRef a, CMProfileRef b);
// Stores the user's color profile away, and changes over to generic.
static void GTMSetColorProfileToGenericRGB();
// Restores the users profile.
static void GTMRestoreColorProfile(void);
// Signal handler to try and restore users profile.
static void GTMHandleCrashSignal(int signalNumber);

static CGKeyCode GTMKeyCodeForCharCode(CGCharCode charCode);

@implementation GTMAppKitUnitTestingUtilities

// Sets up the user interface so that we can run consistent UI unittests on it.
+ (void)setUpForUIUnitTests {
  // Give some names to undocumented defaults values
  const NSInteger MediumFontSmoothing = 2;
  const NSInteger BlueTintedAppearance = 1;

  // This sets up some basic values that we want as our defaults for doing pixel
  // based user interface tests. These defaults only apply to the unit test app,
  // except or the color profile which will be set system wide, and then
  // restored when the tests complete.
  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  // Scroll arrows together bottom
  [defaults setObject:@"DoubleMax" forKey:@"AppleScrollBarVariant"];
  // Smallest font size to CG should perform antialiasing on
  [defaults setInteger:4 forKey:@"AppleAntiAliasingThreshold"];
  // Type of smoothing
  [defaults setInteger:MediumFontSmoothing forKey:@"AppleFontSmoothing"];
  // Blue aqua
  [defaults setInteger:BlueTintedAppearance forKey:@"AppleAquaColorVariant"];
  // Standard highlight colors
  [defaults setObject:@"0.709800 0.835300 1.000000"
               forKey:@"AppleHighlightColor"];
  [defaults setObject:@"0.500000 0.500000 0.500000"
               forKey:@"AppleOtherHighlightColor"];
  // Use english plz
  [defaults setObject:[NSArray arrayWithObject:@"en"] forKey:@"AppleLanguages"];
  // How fast should we draw sheets. This speeds up the sheet tests considerably
  [defaults setFloat:.001f forKey:@"NSWindowResizeTime"];
  // Switch over the screen profile to "generic rgb". This installs an
  // atexit handler to return our profile back when we are done.
  GTMSetColorProfileToGenericRGB();
}

+ (void)setUpForUIUnitTestsIfBeingTested {
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  if ([GTMFoundationUnitTestingUtilities areWeBeingUnitTested]) {
    [self setUpForUIUnitTests];
  }
  [pool drain];
}

+ (BOOL)isScreenSaverActive {
  BOOL answer = NO;
  ProcessSerialNumber psn;
  if (GetFrontProcess(&psn) == noErr) {
    CFDictionaryRef cfProcessInfo
      = ProcessInformationCopyDictionary(&psn,
                                         kProcessDictionaryIncludeAllInformationMask);
    NSDictionary *processInfo = GTMCFAutorelease(cfProcessInfo);

    NSString *bundlePath = [processInfo objectForKey:@"BundlePath"];
    // ScreenSaverEngine is the frontmost app if the screen saver is actually
    // running Security Agent is the frontmost app if the "enter password"
    // dialog is showing
    NSString *bundleName = [bundlePath lastPathComponent];
    answer = ([bundleName isEqualToString:@"ScreenSaverEngine.app"]
              || [bundleName isEqualToString:@"SecurityAgent.app"]);
  }
  return answer;
}

// Allows for posting either a keydown or a keyup with all the modifiers being
// applied. Passing a 'g' with NSKeyDown and NSShiftKeyMask
// generates two events (a shift key key down and a 'g' key keydown). Make sure
// to balance this with a keyup, or things could get confused. Events get posted
// using the CGRemoteOperation events which means that it gets posted in the
// system event queue. Thus you can affect other applications if your app isn't
// the active app (or in some cases, such as hotkeys, even if it is).
//  Arguments:
//    type - Event type. Currently accepts NSKeyDown and NSKeyUp
//    keyChar - character on the keyboard to type. Make sure it is lower case.
//              If you need upper case, pass in the NSShiftKeyMask in the
//              modifiers. i.e. to generate "G" pass in 'g' and NSShiftKeyMask.
//              to generate "+" pass in '=' and NSShiftKeyMask.
//    cocoaModifiers - an int made up of bit masks. Handles NSAlphaShiftKeyMask,
//                    NSShiftKeyMask, NSControlKeyMask, NSAlternateKeyMask, and
//                    NSCommandKeyMask
+ (void)postKeyEvent:(NSEventType)type
           character:(CGCharCode)keyChar
           modifiers:(UInt32)cocoaModifiers {
  require(![self isScreenSaverActive], CantWorkWithScreenSaver);
  require(type == NSKeyDown || type == NSKeyUp, CantDoEvent);
  CGKeyCode code = GTMKeyCodeForCharCode(keyChar);
  verify(code != 256);
  CGEventRef event = CGEventCreateKeyboardEvent(NULL, code, type == NSKeyDown);
  require(event, CantCreateEvent);
  CGEventSetFlags(event, cocoaModifiers);
  CGEventPost(kCGSessionEventTap, event);
  CFRelease(event);
CantCreateEvent:
CantDoEvent:
CantWorkWithScreenSaver:
  return;
}

// Syntactic sugar for posting a keydown immediately followed by a key up event
// which is often what you really want.
//  Arguments:
//    keyChar - character on the keyboard to type. Make sure it is lower case.
//              If you need upper case, pass in the NSShiftKeyMask in the
//              modifiers. i.e. to generate "G" pass in 'g' and NSShiftKeyMask.
//              to generate "+" pass in '=' and NSShiftKeyMask.
//    cocoaModifiers - an int made up of bit masks. Handles NSAlphaShiftKeyMask,
//                    NSShiftKeyMask, NSControlKeyMask, NSAlternateKeyMask, and
//                    NSCommandKeyMask
+ (void)postTypeCharacterEvent:(CGCharCode)keyChar modifiers:(UInt32)cocoaModifiers {
  [self postKeyEvent:NSKeyDown character:keyChar modifiers:cocoaModifiers];
  [self postKeyEvent:NSKeyUp character:keyChar modifiers:cocoaModifiers];
}

@end

BOOL GTMAreCMProfilesEqual(CMProfileRef a, CMProfileRef b) {
  BOOL equal = YES;
  if (a != b) {
    CMProfileMD5 aMD5;
    CMProfileMD5 bMD5;
    CMError aMD5Err = CMGetProfileMD5(a, aMD5);
    CMError bMD5Err = CMGetProfileMD5(b, bMD5);
    equal = (!aMD5Err &&
             !bMD5Err &&
             !memcmp(aMD5, bMD5, sizeof(CMProfileMD5))) ? YES : NO;
  }
  return equal;
}

void GTMRestoreColorProfile(void) {
  if (gGTMCurrentColorProfile) {
    CGDirectDisplayID displayID = CGMainDisplayID();
    CMError error = CMSetProfileByAVID((UInt32)displayID,
                                       gGTMCurrentColorProfile);
    CMCloseProfile(gGTMCurrentColorProfile);
    if (error) {
      // COV_NF_START
      // No way to force this case in a unittest.
      _GTMDevLog(@"Failed to restore previous color profile! "
            "You may need to open System Preferences : Displays : Color "
            "and manually restore your color settings. (Error: %i)", error);
      // COV_NF_END
    } else {
      _GTMDevLog(@"Color profile restored");
    }
    gGTMCurrentColorProfile = NULL;
  }
}

void GTMHandleCrashSignal(int signalNumber) {
  // Going down in flames, might as well try to restore the color profile
  // anyways.
  GTMRestoreColorProfile();
  // Go ahead and exit with the signal value relayed just incase.
  _exit(signalNumber + 128);
}

void GTMSetColorProfileToGenericRGB(void) {
  NSColorSpace *genericSpace = [NSColorSpace genericRGBColorSpace];
  CMProfileRef genericProfile = (CMProfileRef)[genericSpace colorSyncProfile];
  CMProfileRef previousProfile;
  CGDirectDisplayID displayID = CGMainDisplayID();
  CMError error = CMGetProfileByAVID((UInt32)displayID, &previousProfile);
  if (error) {
    // COV_NF_START
    // No way to force this case in a unittest.
    _GTMDevLog(@"Failed to get current color profile. "
               "I will not be able to restore your current profile, thus I'm "
               "not changing it. Many unit tests may fail as a result. (Error: %i)",
          error);
    return;
    // COV_NF_END
  }
  if (GTMAreCMProfilesEqual(genericProfile, previousProfile)) {
    CMCloseProfile(previousProfile);
    return;
  }
  CFStringRef previousProfileName;
  CFStringRef genericProfileName;
  CMCopyProfileDescriptionString(previousProfile, &previousProfileName);
  CMCopyProfileDescriptionString(genericProfile, &genericProfileName);

  _GTMDevLog(@"Temporarily changing your system color profile from \"%@\" to \"%@\".",
             previousProfileName, genericProfileName);
  _GTMDevLog(@"This allows the pixel-based unit-tests to have consistent color "
             "values across all machines.");
  _GTMDevLog(@"The colors on your screen will change for the duration of the testing.");


  if ((error = CMSetProfileByAVID((UInt32)displayID, genericProfile))) {
    // COV_NF_START
    // No way to force this case in a unittest.
    _GTMDevLog(@"Failed to set color profile to \"%@\"! Many unit tests will fail as "
               "a result.  (Error: %i)", genericProfileName, error);
    // COV_NF_END
  } else {
    gGTMCurrentColorProfile = previousProfile;
    atexit(GTMRestoreColorProfile);
    // WebKit DRT and Chrome TestShell both use this trick. If the test is
    // already crashing, might as well try restoring the color profile, and if
    // it fails, it is no worse than crashing without having tried.
    signal(SIGILL, GTMHandleCrashSignal);
    signal(SIGTRAP, GTMHandleCrashSignal);
    signal(SIGEMT, GTMHandleCrashSignal);
    signal(SIGFPE, GTMHandleCrashSignal);
    signal(SIGBUS, GTMHandleCrashSignal);
    signal(SIGSEGV, GTMHandleCrashSignal);
    signal(SIGSYS, GTMHandleCrashSignal);
    signal(SIGPIPE, GTMHandleCrashSignal);
    signal(SIGXCPU, GTMHandleCrashSignal);
    signal(SIGXFSZ, GTMHandleCrashSignal);
  }
  CFRelease(previousProfileName);
  CFRelease(genericProfileName);
}

// Returns a virtual key code for a given charCode. Handles all of the
// NS*FunctionKeys as well.
static CGKeyCode GTMKeyCodeForCharCode(CGCharCode charCode) {
  // character map taken from http://classicteck.com/rbarticles/mackeyboard.php
  int characters[] = {
    'a', 's', 'd', 'f', 'h', 'g', 'z', 'x', 'c', 'v', 256, 'b', 'q', 'w',
    'e', 'r', 'y', 't', '1', '2', '3', '4', '6', '5', '=', '9', '7', '-',
    '8', '0', ']', 'o', 'u', '[', 'i', 'p', '\n', 'l', 'j', '\'', 'k', ';',
    '\\', ',', '/', 'n', 'm', '.', '\t', ' ', '`', '\b', 256, '\e'
  };

  // function key map taken from
  // file:///Developer/ADC%20Reference%20Library/documentation/Cocoa/Reference/ApplicationKit/ObjC_classic/Classes/NSEvent.html
  int functionKeys[] = {
    // NSUpArrowFunctionKey - NSF12FunctionKey
    126, 125, 123, 124, 122, 120, 99, 118, 96, 97, 98, 100, 101, 109, 103, 111,
    // NSF13FunctionKey - NSF28FunctionKey
    105, 107, 113, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256,
    // NSF29FunctionKey - NSScrollLockFunctionKey
    256, 256, 256, 256, 256, 256, 256, 256, 117, 115, 256, 119, 116, 121, 256, 256,
    // NSPauseFunctionKey - NSPrevFunctionKey
    256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256, 256,
    // NSNextFunctionKey - NSModeSwitchFunctionKey
    256, 256, 256, 256, 256, 256, 114, 1
  };

  CGKeyCode outCode = 0;

  // Look in the function keys
  if (charCode >= NSUpArrowFunctionKey && charCode <= NSModeSwitchFunctionKey) {
    outCode = functionKeys[charCode - NSUpArrowFunctionKey];
  } else {
    // Look in our character map
    for (size_t i = 0; i < (sizeof(characters) / sizeof (int)); i++) {
      if (characters[i] == charCode) {
        outCode = i;
        break;
      }
    }
  }
  return outCode;
}

@implementation NSApplication (GTMUnitTestingRunAdditions)

- (BOOL)gtm_runUntilDate:(NSDate *)date
                 context:(id<GTMUnitTestingRunLoopContext>)context {
  BOOL contextShouldStop = NO;
  while (1) {
    contextShouldStop = [context shouldStop];
    if (contextShouldStop) break;
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSEvent *event = [NSApp nextEventMatchingMask:NSAnyEventMask
                                         untilDate:date
                                            inMode:NSDefaultRunLoopMode
                                           dequeue:YES];
    if (!event) {
      [pool drain];
      break;
    }
    [NSApp sendEvent:event];
    [pool drain];
  }
  return contextShouldStop;
}

- (BOOL)gtm_runUpToSixtySecondsWithContext:(id<GTMUnitTestingRunLoopContext>)context {
  return [self gtm_runUntilDate:[NSDate dateWithTimeIntervalSinceNow:60]
                        context:context];
}

@end