diff options
author | thomasvl@gmail.com <thomasvl@gmail.com@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3> | 2008-07-21 14:49:00 +0000 |
---|---|---|
committer | thomasvl@gmail.com <thomasvl@gmail.com@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3> | 2008-07-21 14:49:00 +0000 |
commit | 4963c8a9d57501b5ffb0fe52fbfe60cd71b3b916 (patch) | |
tree | dd70e4aba13b245427788ccdb075e04f1af0951c /Foundation | |
parent | ef630a18e87e03f15a2ae2b7b90623d4a0183f89 (diff) |
helps if I add the files before doing the commit
Diffstat (limited to 'Foundation')
-rw-r--r-- | Foundation/GTMLogger+ASL.h | 87 | ||||
-rw-r--r-- | Foundation/GTMLogger+ASL.m | 121 | ||||
-rw-r--r-- | Foundation/GTMLogger+ASLTest.m | 91 | ||||
-rw-r--r-- | Foundation/GTMLogger.h | 461 | ||||
-rw-r--r-- | Foundation/GTMLogger.m | 446 | ||||
-rw-r--r-- | Foundation/GTMLoggerRingBufferWriter.h | 96 | ||||
-rw-r--r-- | Foundation/GTMLoggerRingBufferWriter.m | 248 | ||||
-rw-r--r-- | Foundation/GTMLoggerRingBufferWriterTest.m | 364 | ||||
-rw-r--r-- | Foundation/GTMLoggerTest.m | 443 |
9 files changed, 2357 insertions, 0 deletions
diff --git a/Foundation/GTMLogger+ASL.h b/Foundation/GTMLogger+ASL.h new file mode 100644 index 0000000..689a0d9 --- /dev/null +++ b/Foundation/GTMLogger+ASL.h @@ -0,0 +1,87 @@ +// +// GTMLogger+ASL.h +// +// Copyright 2007-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 <Foundation/Foundation.h> +#import <asl.h> +#import "GTMLogger.h" + + +// GTMLogger (GTMLoggerASLAdditions) +// +// Adds a convenience creation method that allows you to get a standard +// GTMLogger object that is configured to write to ASL (Apple System Log) using +// the GTMLogASLWriter (declared below). +// +@interface GTMLogger (GTMLoggerASLAdditions) + +// Returns a new autoreleased GTMLogger instance that will log to ASL, using +// the GTMLogStandardFormatter, and the GTMLogLevelFilter filter. ++ (id)standardLoggerWithASL; + +@end + + +@class GTMLoggerASLClient; + +// GTMLogASLWriter +// +// A GTMLogWriter implementation that will send log messages to ASL (Apple +// System Log facility). To use with GTMLogger simply set the "writer" for a +// GTMLogger to be an instance of this class. The following example sets the +// shared system logger to lot to ASL. +// +// [[GTMLogger sharedLogger] setWriter:[GTMLogASLWriter aslWriter]]; +// GTMLoggerInfo(@"Hi"); // This is sent to ASL +// +// See GTMLogger.h for more details and a higher-level view. +// +@interface GTMLogASLWriter : NSObject <GTMLogWriter> { + @private + __weak Class aslClientClass_; +} + +// Returns an autoreleased GTMLogASLWriter instance that uses an instance of +// GTMLoggerASLClient. ++ (id)aslWriter; + +// Designated initializer. Uses instances of the specified |clientClass| to talk +// to the ASL system. This method is typically only useful for testing. Users +// should generally NOT use this method to get an instance. Instead, simply use +// the +aslWriter method to obtain an instance. +- (id)initWithClientClass:(Class)clientClass; + +@end // GTMLogASLWriter + + +// Helper class used by GTMLogASLWriter to create an ASL client and write to the +// ASL log. This class is need to make management/cleanup of the aslclient work +// in a multithreaded environment. You'll need one of these GTMLoggerASLClient +// per thread (this is automatically handled by GTMLogASLWriter). +// +// This class should rarely (if EVER) be used directly. It's designed to be used +// internally by GTMLogASLWriter, and by some unit tests. It should not be +// used externally. +@interface GTMLoggerASLClient : NSObject { + @private + aslclient client_; +} + +// Sends the given string to ASL at the specified ASL log |level|. +- (void)log:(NSString *)msg level:(int)level; + +@end diff --git a/Foundation/GTMLogger+ASL.m b/Foundation/GTMLogger+ASL.m new file mode 100644 index 0000000..1a49406 --- /dev/null +++ b/Foundation/GTMLogger+ASL.m @@ -0,0 +1,121 @@ +// +// GTMLogger+ASL.m +// +// Copyright 2007-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 "GTMLogger+ASL.h" + + +@implementation GTMLogger (GTMLoggerASLAdditions) + ++ (id)standardLoggerWithASL { + id me = [self standardLogger]; + [me setWriter:[[[GTMLogASLWriter alloc] init] autorelease]]; + [me setFormatter:[[[GTMLogBasicFormatter alloc] init] autorelease]]; + return me; +} + +@end + + +@implementation GTMLogASLWriter + ++ (id)aslWriter { + return [[[self alloc] init] autorelease]; +} + +- (id)init { + return [self initWithClientClass:nil]; +} + +- (id)initWithClientClass:(Class)clientClass { + if ((self = [super init])) { + aslClientClass_ = clientClass; + if (aslClientClass_ == nil) { + aslClientClass_ = [GTMLoggerASLClient class]; + } + } + return self; +} + +- (void)logMessage:(NSString *)msg level:(GTMLoggerLevel)level { + static NSString *const kASLClientKey = @"GTMLoggerASLClientKey"; + + // Lookup the ASL client in the thread-local storage dictionary + NSMutableDictionary *tls = [[NSThread currentThread] threadDictionary]; + GTMLoggerASLClient *client = [tls objectForKey:kASLClientKey]; + + // If the ASL client wasn't found (e.g., the first call from this thread), + // then create it and store it in the thread-local storage dictionary + if (client == nil) { + client = [[[aslClientClass_ alloc] init] autorelease]; + [tls setObject:client forKey:kASLClientKey]; + } + + // Map the GTMLoggerLevel level to an ASL level. + int aslLevel = ASL_LEVEL_INFO; + switch (level) { + case kGTMLoggerLevelUnknown: + case kGTMLoggerLevelDebug: + case kGTMLoggerLevelInfo: + aslLevel = ASL_LEVEL_NOTICE; + break; + case kGTMLoggerLevelError: + aslLevel = ASL_LEVEL_ERR; + break; + case kGTMLoggerLevelAssert: + aslLevel = ASL_LEVEL_ALERT; + break; + } + + [client log:msg level:aslLevel]; +} + +@end // GTMLogASLWriter + + +@implementation GTMLoggerASLClient + +- (id)init { + if ((self = [super init])) { + client_ = asl_open(NULL, NULL, 0); + if (client_ == nil) { + [self release]; + return nil; + } + } + return self; +} + +- (void)dealloc { + if (client_) asl_close(client_); + [super dealloc]; +} + +- (void)finalize { + if (client_) asl_close(client_); + [super finalize]; +} + +// We don't test this one line because we don't want to pollute actual system +// logs with test messages. +// COV_NF_START +- (void)log:(NSString *)msg level:(int)level { + asl_log(client_, NULL, level, "%s", [msg UTF8String]); +} +// COV_NF_END + +@end // GTMLoggerASLClient diff --git a/Foundation/GTMLogger+ASLTest.m b/Foundation/GTMLogger+ASLTest.m new file mode 100644 index 0000000..97f7e29 --- /dev/null +++ b/Foundation/GTMLogger+ASLTest.m @@ -0,0 +1,91 @@ +// +// GTMLogger+ASLTest.m +// +// Copyright 2007-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 "GTMLogger+ASL.h" +#import "GTMSenTestCase.h" + +@interface DummyASLClient : GTMLoggerASLClient +@end + +static NSMutableArray *gDummyLog; // weak + +@implementation DummyASLClient + +- (void)log:(NSString *)msg level:(int)level { + NSString *line = [msg stringByAppendingFormat:@"@%d", level]; + [gDummyLog addObject:line]; +} + +@end + + +@interface GTMLogger_ASLTest : GTMTestCase +@end + +@implementation GTMLogger_ASLTest + +- (void)testCreation { + GTMLogger *aslLogger = [GTMLogger standardLoggerWithASL]; + STAssertNotNil(aslLogger, nil); + + GTMLogASLWriter *writer = [GTMLogASLWriter aslWriter]; + STAssertNotNil(writer, nil); +} + +- (void)testLogWriter { + gDummyLog = [[[NSMutableArray alloc] init] autorelease]; + GTMLogASLWriter *writer = [[[GTMLogASLWriter alloc] + initWithClientClass:[DummyASLClient class]] + autorelease]; + + + STAssertNotNil(writer, nil); + STAssertTrue([gDummyLog count] == 0, nil); + + // Log some messages + [writer logMessage:@"unknown" level:kGTMLoggerLevelUnknown]; + [writer logMessage:@"debug" level:kGTMLoggerLevelDebug]; + [writer logMessage:@"info" level:kGTMLoggerLevelInfo]; + [writer logMessage:@"error" level:kGTMLoggerLevelError]; + [writer logMessage:@"assert" level:kGTMLoggerLevelAssert]; + + // Inspect the logged message to make sure they were logged correctly. The + // dummy writer will save the messages w/ @level concatenated. The "level" + // will be the ASL level, not the GTMLogger level. GTMLogASLWriter will log + // all + NSArray *log = gDummyLog; + NSArray *expected = [NSArray arrayWithObjects: + @"unknown@5", + @"debug@5", + @"info@5", + @"error@3", + @"assert@1", + nil]; + + STAssertEqualObjects(log, expected, nil); + + gDummyLog = nil; +} + +- (void)testASLClient { + GTMLoggerASLClient *client = [[GTMLoggerASLClient alloc] init]; + STAssertNotNil(client, nil); + [client release]; +} + +@end diff --git a/Foundation/GTMLogger.h b/Foundation/GTMLogger.h new file mode 100644 index 0000000..0565462 --- /dev/null +++ b/Foundation/GTMLogger.h @@ -0,0 +1,461 @@ +// +// GTMLogger.h +// +// Copyright 2007-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. +// + +// Key Abstractions +// ---------------- +// +// This file declares multiple classes and protocols that are used by the +// GTMLogger logging system. The 4 main abstractions used in this file are the +// following: +// +// * logger (GTMLogger) - The main logging class that users interact with. It +// has methods for logging at different levels and uses a log writer, a log +// formatter, and a log filter to get the job done. +// +// * log writer (GTMLogWriter) - Writes a given string to some log file, where +// a "log file" can be a physical file on disk, a POST over HTTP to some URL, +// or even some in-memory structure (e.g., a ring buffer). +// +// * log formatter (GTMLogFormatter) - Given a format string and arguments as +// a va_list, returns a single formatted NSString. A "formatted string" could +// be a string with the date prepended, a string with values in a CSV format, +// or even a string of XML. +// +// * log filter (GTMLogFilter) - Given a formatted log message as an NSString +// and the level at which the message is to be logged, this class will decide +// whether the given message should be logged or not. This is a flexible way +// to filter out messages logged at a certain level, messages that contain +// certain text, or filter nothing out at all. This gives the caller the +// flexibility to dynamically enable debug logging in Release builds. +// +// A class diagram showing the relationship between these key abstractions can +// be found at: http://www.corp.google.com/eng/designdocs/maceng/GTMLogger.png +// +// This file also declares some classes to handle the common log writer, log +// formatter, and log filter cases. Callers can also create their own writers, +// formatters, and filters and they can even build them on top of the ones +// declared here. Keep in mind that your custom writer/formatter/filter may be +// called from multiple threads, so it must be thread-safe. + +#import <Foundation/Foundation.h> + +// Predeclaration of used protocols that are declared later in this file. +@protocol GTMLogWriter, GTMLogFormatter, GTMLogFilter; + +#if MAC_OS_X_VERSION_MIN_REQUIRED >= 1050 +#define CHECK_FORMAT_NSSTRING(a, b) __attribute__((format(__NSString__, a, b))) +#else +#define CHECK_FORMAT_NSSTRING(a, b) +#endif + +// GTMLogger +// +// GTMLogger is the primary user-facing class for an object-oriented logging +// system. It is built on the concept of log formatters (GTMLogFormatter), log +// writers (GTMLogWriter), and log filters (GTMLogFilter). When a message is +// sent to a GTMLogger to log a message, the message is formatted using the log +// formatter, then the log filter is consulted to see if the message should be +// logged, and if so, the message is sent to the log writer to be written out. +// +// GTMLogger is intended to be a flexible and thread-safe logging solution. Its +// flexibility comes from the fact that GTMLogger instances can be customized +// with user defined formatters, filters, and writers. And these writers, +// filters, and formatters can be combined, stacked, and customized in arbitrary +// ways to suit the needs at hand. For example, multiple writers can be used at +// the same time, and a GTMLogger instance can even be used as another +// GTMLogger's writer. This allows for arbitrarily deep logging trees. +// +// A standard GTMLogger uses a writer that sends messages to standard out, a +// formatter that smacks a timestamp and a few other bits of interesting +// information on the message, and a filter that filters out debug messages from +// release builds. Using the standard log settings, a log message will look like +// the following: +// +// 2007-12-30 10:29:24.177 myapp[4588/0xa07d0f60] [lvl=1] foo=<Foo: 0x123> +// +// The output contains the date and time of the log message, the name of the +// process followed by its process ID/thread ID, the log level at which the +// message was logged (in the previous example the level was 1: +// kGTMLoggerLevelDebug), and finally, the user-specified log message itself (in +// this case, the log message was @"foo=%@", foo). +// +// Multiple instances of GTMLogger can be created, each configured their own +// way. Though GTMLogger is not a singleton (in the GoF sense), it does provide +// access to a shared (i.e., globally accessible) GTMLogger instance. This makes +// it convenient for all code in a process to use the same GTMLogger instance. +// The shared GTMLogger instance can also be configured in an arbitrary, and +// these configuration changes will affect all code that logs through the shared +// instance. + +// +// Log Levels +// ---------- +// GTMLogger has 3 different log levels: Debug, Info, and Error. GTMLogger +// doesn't take any special action based on the log level; it simply forwards +// this information on to formatters, filters, and writers, each of which may +// optionally take action based on the level. Since log level filtering is +// performed at runtime, log messages are typically not filtered out at compile +// time. The exception to this rule is that calls to the GTMLoggerDebug() macro +// *ARE* filtered out of non-DEBUG builds. This is to be backwards compatible +// with behavior that many developers are currently used to. Note that this +// means that GTMLoggerDebug(@"hi") will be compiled out of Release builds, but +// [[GTMLogger sharedLogger] logDebug:@"hi"] will NOT be compiled out. +// +// Standard loggers are created with the GTMLogLevelFilter log filter, which +// filters out certain log messages based on log level, and some other settings. +// +// In addition to the -logDebug:, -logInfo:, and -logError: methods defined on +// GTMLogger itself, there are also C macros that make usage of the shared +// GTMLogger instance very convenient. These macros are: +// +// GTMLoggerDebug(...) +// GTMLoggerInfo(...) +// GTMLoggerError(...) +// +// Again, a notable feature of these macros is that GTMLogDebug() calls *will be +// compiled out of non-DEBUG builds*. +// +// Standard Loggers +// ---------------- +// GTMLogger has the concept of "standard loggers". A standard logger is simply +// a logger that is pre-configured with some standard/common writer, formatter, +// and filter combination. Standard loggers are created using the creation +// methods beginning with "standard". The alternative to a standard logger is a +// regular logger, which will send messages to stdout, with no special +// formatting, and no filtering. +// +// How do I use GTMLogger? +// ---------------------- +// The typical way you will want to use GTMLogger is to simply use the +// GTMLogger*() macros for logging from code. That way we can easily make +// changes to the GTMLogger class and simply update the macros accordingly. Only +// your application startup code (perhaps, somewhere in main()) should use the +// GTMLogger class directly in order to configure the shared logger, which all +// of the code using the macros will be using. Again, this is just the typical +// situation. +// +// To be complete, there are cases where you may want to use GTMLogger directly, +// or even create separate GTMLogger instances for some reason. That's fine, +// too. +// +// Examples +// -------- +// The following show some common GTMLogger use cases. +// +// 1. You want to log something as simply as possible. Also, this call will only +// appear in debug builds. In non-DEBUG builds it will be completely removed. +// +// GTMLoggerDebug(@"foo = %@", foo); +// +// 2. The previous example is similar to the following. The major difference is +// that the previous call (example 1) will be compiled out of Release builds +// but this statement will not be compiled out. +// +// [[GTMLogger sharedLogger] logDebug:@"foo = %@", foo]; +// +// 3. Send all logging output from the shared logger to a file. We do this by +// creating an NSFileHandle for writing associated with a file, and setting +// that file handle as the logger's writer. +// +// NSFileHandle *f = [NSFileHandle fileHandleForWritingAtPath:@"/tmp/f.log" +// create:YES]; +// [[GTMLogger sharedLogger] setWriter:f]; +// GTMLoggerError(@"hi"); // This will be sent to /tmp/f.log +// +// 4. Create a new GTMLogger that will log to a file. This example differs from +// the previous one because here we create a new GTMLogger that is different +// from the shared logger. +// +// GTMLogger *logger = [GTMLogger standardLoggerWithPath:@"/tmp/temp.log"]; +// [logger logInfo:@"hi temp log file"]; +// +// 5. Create a logger that writes to stdout and does NOT do any formatting to +// the log message. This might be useful, for example, when writing a help +// screen for a command-line tool to standard output. +// +// GTMLogger *logger = [GTMLogger logger]; +// [logger logInfo:@"%@ version 0.1 usage", progName]; +// +// 6. Send log output to stdout AND to a log file. The trick here is that +// NSArrays function as composite log writers, which means when an array is +// set as the log writer, it forwards all logging messages to all of its +// contained GTMLogWriters. +// +// // Create array of GTMLogWriters +// NSArray *writers = [NSArray arrayWithObjects: +// [NSFileHandle fileHandleForWritingAtPath:@"/tmp/f.log" create:YES], +// [NSFileHandle fileHandleWithStandardOutput], nil]; +// +// GTMLogger *logger = [GTMLogger standardLogger]; +// [logger setWriter:writers]; +// [logger logInfo:@"hi"]; // Output goes to stdout and /tmp/f.log +// +// For futher details on log writers, formatters, and filters, see the +// documentation below. +// +// NOTE: GTMLogger is application level logging. By default it does nothing +// with _GTMDevLog/_GTMDevAssert (see GTMDefines.h). An application can choose +// to bridge _GTMDevLog/_GTMDevAssert to GTMLogger by providing macro +// definitions in its prefix header (see GTMDefines.h for how one would do +// that). +// +@interface GTMLogger : NSObject { + @private + id<GTMLogWriter> writer_; + id<GTMLogFormatter> formatter_; + id<GTMLogFilter> filter_; +} + +// +// Accessors for the shared logger instance +// + +// Returns a shared/global standard GTMLogger instance. Callers should typically +// use this method to get a GTMLogger instance, unless they explicitly want +// their own instance to configure for their own needs. This is the only method +// that returns a shared instance; all the rest return new GTMLogger instances. ++ (id)sharedLogger; + +// Sets the shared logger instance to |logger|. Future calls to +sharedLogger +// will return |logger| instead. ++ (void)setSharedLogger:(GTMLogger *)logger; + +// +// Creation methods +// + +// Returns a new autoreleased GTMLogger instance that will log to stdout, using +// the GTMLogStandardFormatter, and the GTMLogLevelFilter filter. ++ (id)standardLogger; + +// Same as +standardLogger, but logs to stderr. ++ (id)standardLoggerWithStderr; + +// Returns a new standard GTMLogger instance with a log writer that will +// write to the file at |path|, and will use the GTMLogStandardFormatter and +// GTMLogLevelFilter classes. If |path| does not exist, it will be created. ++ (id)standardLoggerWithPath:(NSString *)path; + +// Returns an autoreleased GTMLogger instance that will use the specified +// |writer|, |formatter|, and |filter|. ++ (id)loggerWithWriter:(id<GTMLogWriter>)writer + formatter:(id<GTMLogFormatter>)formatter + filter:(id<GTMLogFilter>)filter; + +// Returns an autoreleased GTMLogger instance that logs to stdout, with the +// basic formatter, and no filter. The returned logger differs from the logger +// returned by +standardLogger because this one does not do any filtering and +// does not do any special log formatting; this is the difference between a +// "regular" logger and a "standard" logger. ++ (id)logger; + +// Designated initializer. This method returns a GTMLogger initialized with the +// specified |writer|, |formatter|, and |filter|. See the setter methods below +// for what values will be used if nil is passed for a parameter. +- (id)initWithWriter:(id<GTMLogWriter>)writer + formatter:(id<GTMLogFormatter>)formatter + filter:(id<GTMLogFilter>)filter; + +// +// Logging methods +// + +// Logs a message at the debug level (kGTMLoggerLevelDebug). +- (void)logDebug:(NSString *)fmt, ... CHECK_FORMAT_NSSTRING(1, 2); +// Logs a message at the info level (kGTMLoggerLevelInfo). +- (void)logInfo:(NSString *)fmt, ... CHECK_FORMAT_NSSTRING(1, 2); +// Logs a message at the error level (kGTMLoggerLevelError). +- (void)logError:(NSString *)fmt, ... CHECK_FORMAT_NSSTRING(1, 2); +// Logs a message at the assert level (kGTMLoggerLevelAssert). +- (void)logAssert:(NSString *)fmt, ... CHECK_FORMAT_NSSTRING(1, 2); + + +// +// Accessors +// + +// Accessor methods for the log writer. If the log writer is set to nil, +// [NSFileHandle fileHandleWithStandardOutput] is used. +- (id<GTMLogWriter>)writer; +- (void)setWriter:(id<GTMLogWriter>)writer; + +// Accessor methods for the log formatter. If the log formatter is set to nil, +// GTMLogBasicFormatter is used. This formatter will format log messages in a +// plain printf style. +- (id<GTMLogFormatter>)formatter; +- (void)setFormatter:(id<GTMLogFormatter>)formatter; + +// Accessor methods for the log filter. If the log filter is set to nil, +// GTMLogNoFilter is used, which allows all log messages through. +- (id<GTMLogFilter>)filter; +- (void)setFilter:(id<GTMLogFilter>)filter; + +@end // GTMLogger + + +// Helper functions that are used by the convenience GTMLogger*() macros that +// enable the logging of function names. +@interface GTMLogger (GTMLoggerMacroHelpers) +- (void)logFuncDebug:(const char *)func msg:(NSString *)fmt, ... + CHECK_FORMAT_NSSTRING(2, 3); +- (void)logFuncInfo:(const char *)func msg:(NSString *)fmt, ... + CHECK_FORMAT_NSSTRING(2, 3); +- (void)logFuncError:(const char *)func msg:(NSString *)fmt, ... + CHECK_FORMAT_NSSTRING(2, 3); +- (void)logFuncAssert:(const char *)func msg:(NSString *)fmt, ... + CHECK_FORMAT_NSSTRING(2, 3); +@end // GTMLoggerMacroHelpers + + +// Convenience macros that log to the shared GTMLogger instance. These macros +// are how users should typically log to GTMLogger. Notice that GTMLoggerDebug() +// calls will be compiled out of non-Debug builds. +#define GTMLoggerDebug(...) \ + [[GTMLogger sharedLogger] logFuncDebug:__func__ msg:__VA_ARGS__] +#define GTMLoggerInfo(...) \ + [[GTMLogger sharedLogger] logFuncInfo:__func__ msg:__VA_ARGS__] +#define GTMLoggerError(...) \ + [[GTMLogger sharedLogger] logFuncError:__func__ msg:__VA_ARGS__] +#define GTMLoggerAssert(...) \ + [[GTMLogger sharedLogger] logFuncAssert:__func__ msg:__VA_ARGS__] + +// If we're not in a debug build, remove the GTMLoggerDebug statements. This +// makes calls to GTMLoggerDebug "compile out" of Release builds +#ifndef DEBUG +#undef GTMLoggerDebug +#define GTMLoggerDebug(...) do {} while(0) +#endif + +// Log levels. +typedef enum { + kGTMLoggerLevelUnknown, + kGTMLoggerLevelDebug, + kGTMLoggerLevelInfo, + kGTMLoggerLevelError, + kGTMLoggerLevelAssert, +} GTMLoggerLevel; + + +// +// Log Writers +// + +// Protocol to be implemented by a GTMLogWriter instance. +@protocol GTMLogWriter <NSObject> +// Writes the given log message to where the log writer is configured to write. +- (void)logMessage:(NSString *)msg level:(GTMLoggerLevel)level; +@end // GTMLogWriter + + +// Simple category on NSFileHandle that makes NSFileHandles valid log writers. +// This is convenient because something like, say, +fileHandleWithStandardError +// now becomes a valid log writer. Log messages are written to the file handle +// with a newline appended. +@interface NSFileHandle (GTMFileHandleLogWriter) <GTMLogWriter> +// Opens the file at |path| in append mode, and creates the file with |mode| +// if it didn't previously exist. ++ (id)fileHandleForLoggingAtPath:(NSString *)path mode:(mode_t)mode; +@end // NSFileHandle + + +// This category makes NSArray a GTMLogWriter that can be composed of other +// GTMLogWriters. This is the classic Composite GoF design pattern. When the +// GTMLogWriter -logMessage:level: message is sent to the array, the array +// forwards the message to all of its elements that implement the GTMLogWriter +// protocol. +// +// This is useful in situations where you would like to send log output to +// multiple log writers at the same time. Simply create an NSArray of the log +// writers you wish to use, then set the array as the "writer" for your +// GTMLogger instance. +@interface NSArray (GTMArrayCompositeLogWriter) <GTMLogWriter> +@end // GTMArrayCompositeLogWriter + + +// This category adapts the GTMLogger interface so that it can be used as a log +// writer; it's an "adapter" in the GoF Adapter pattern sense. +// +// This is useful when you want to configure a logger to log to a specific +// writer with a specific formatter and/or filter. But you want to also compose +// that with a different log writer that may have its own formatter and/or +// filter. +@interface GTMLogger (GTMLoggerLogWriter) <GTMLogWriter> +@end // GTMLoggerLogWriter + + +// +// Log Formatters +// + +// Protocol to be implemented by a GTMLogFormatter instance. +@protocol GTMLogFormatter <NSObject> +// Returns a formatted string using the format specified in |fmt| and the va +// args specified in |args|. +- (NSString *)stringForFunc:(NSString *)func + withFormat:(NSString *)fmt + valist:(va_list)args + level:(GTMLoggerLevel)level; +@end // GTMLogFormatter + + +// A basic log formatter that formats a string the same way that NSLog (or +// printf) would. It does not do anything fancy, nor does it add any data of its +// own. +@interface GTMLogBasicFormatter : NSObject <GTMLogFormatter> +@end // GTMLogBasicFormatter + + +// A log formatter that formats the log string like the basic formatter, but +// also prepends a timestamp and some basic process info to the message, as +// shown in the following sample output. +// 2007-12-30 10:29:24.177 myapp[4588/0xa07d0f60] [lvl=1] log mesage here +@interface GTMLogStandardFormatter : GTMLogBasicFormatter { + @private + NSDateFormatter *dateFormatter_; // yyyy-MM-dd HH:mm:ss.SSS + NSString *pname_; + pid_t pid_; +} +@end // GTMLogStandardFormatter + + +// +// Log Filters +// + +// Protocol to be imlemented by a GTMLogFilter instance. +@protocol GTMLogFilter <NSObject> +// Returns YES if |msg| at |level| should be filtered out; NO otherwise. +- (BOOL)filterAllowsMessage:(NSString *)msg level:(GTMLoggerLevel)level; +@end // GTMLogFilter + + +// A log filter that filters messages at the kGTMLoggerLevelDebug level out of +// non-debug builds. Messages at the kGTMLoggerLevelInfo level are also filtered +// out of non-debug builds unless GTMVerboseLogging is set in the environment or +// the processes's defaults. Messages at the kGTMLoggerLevelError level are +// never filtered. +@interface GTMLogLevelFilter : NSObject <GTMLogFilter> +@end // GTMLogLevelFilter + + +// A simple log filter that does NOT filter anything out; +// -filterAllowsMessage:level will always return YES. This can be a convenient +// way to enable debug-level logging in release builds (if you so desire). +@interface GTMLogNoFilter : NSObject <GTMLogFilter> +@end // GTMLogNoFilter + diff --git a/Foundation/GTMLogger.m b/Foundation/GTMLogger.m new file mode 100644 index 0000000..cf1d2c8 --- /dev/null +++ b/Foundation/GTMLogger.m @@ -0,0 +1,446 @@ +// +// GTMLogger.m +// +// Copyright 2007-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 "GTMLogger.h" +#import "GTMGarbageCollection.h" +#import <fcntl.h> +#import <unistd.h> +#import <stdlib.h> +#import <pthread.h> + + +// Define a trivial assertion macro to avoid dependencies +#ifdef DEBUG + #define GTMLOGGER_ASSERT(expr) assert(expr) +#else + #define GTMLOGGER_ASSERT(expr) +#endif + + +@interface GTMLogger (PrivateMethods) + +- (void)logInternalFunc:(const char *)func + format:(NSString *)fmt + valist:(va_list)args + level:(GTMLoggerLevel)level; + +@end + + +// Reference to the shared GTMLogger instance. This is not a singleton, it's +// just an easy reference to one shared instance. +static GTMLogger *gSharedLogger = nil; + + +@implementation GTMLogger + +// Returns a pointer to the shared logger instance. If none exists, a standard +// logger is created and returned. ++ (id)sharedLogger { + @synchronized(self) { + if (gSharedLogger == nil) { + gSharedLogger = [[self standardLogger] retain]; + } + GTMLOGGER_ASSERT(gSharedLogger != nil); + } + return [[gSharedLogger retain] autorelease]; +} + ++ (void)setSharedLogger:(GTMLogger *)logger { + @synchronized(self) { + [gSharedLogger autorelease]; + gSharedLogger = [logger retain]; + } +} + ++ (id)standardLogger { + id<GTMLogWriter> writer = [NSFileHandle fileHandleWithStandardOutput]; + id<GTMLogFormatter> fr = [[[GTMLogStandardFormatter alloc] init] autorelease]; + id<GTMLogFilter> filter = [[[GTMLogLevelFilter alloc] init] autorelease]; + return [self loggerWithWriter:writer formatter:fr filter:filter]; +} + ++ (id)standardLoggerWithStderr { + id me = [self standardLogger]; + [me setWriter:[NSFileHandle fileHandleWithStandardError]]; + return me; +} + ++ (id)standardLoggerWithPath:(NSString *)path { + NSFileHandle *fh = [NSFileHandle fileHandleForLoggingAtPath:path mode:0644]; + if (fh == nil) return nil; + id me = [self standardLogger]; + [me setWriter:fh]; + return me; +} + ++ (id)loggerWithWriter:(id<GTMLogWriter>)writer + formatter:(id<GTMLogFormatter>)formatter + filter:(id<GTMLogFilter>)filter { + return [[[self alloc] initWithWriter:writer + formatter:formatter + filter:filter] autorelease]; +} + ++ (id)logger { + return [[[self alloc] init] autorelease]; +} + +- (id)init { + return [self initWithWriter:nil formatter:nil filter:nil]; +} + +- (id)initWithWriter:(id<GTMLogWriter>)writer + formatter:(id<GTMLogFormatter>)formatter + filter:(id<GTMLogFilter>)filter { + if ((self = [super init])) { + [self setWriter:writer]; + [self setFormatter:formatter]; + [self setFilter:filter]; + GTMLOGGER_ASSERT(formatter_ != nil); + GTMLOGGER_ASSERT(filter_ != nil); + GTMLOGGER_ASSERT(writer_ != nil); + } + return self; +} + +- (void)dealloc { + GTMLOGGER_ASSERT(writer_ != nil); + GTMLOGGER_ASSERT(formatter_ != nil); + GTMLOGGER_ASSERT(filter_ != nil); + [writer_ release]; + [formatter_ release]; + [filter_ release]; + [super dealloc]; +} + +- (id<GTMLogWriter>)writer { + GTMLOGGER_ASSERT(writer_ != nil); + return [[writer_ retain] autorelease]; +} + +- (void)setWriter:(id<GTMLogWriter>)writer { + @synchronized(self) { + [writer_ autorelease]; + if (writer == nil) + writer_ = [[NSFileHandle fileHandleWithStandardOutput] retain]; + else + writer_ = [writer retain]; + } + GTMLOGGER_ASSERT(writer_ != nil); +} + +- (id<GTMLogFormatter>)formatter { + GTMLOGGER_ASSERT(formatter_ != nil); + return [[formatter_ retain] autorelease]; +} + +- (void)setFormatter:(id<GTMLogFormatter>)formatter { + @synchronized(self) { + [formatter_ autorelease]; + if (formatter == nil) + formatter_ = [[GTMLogBasicFormatter alloc] init]; + else + formatter_ = [formatter retain]; + } + GTMLOGGER_ASSERT(formatter_ != nil); +} + +- (id<GTMLogFilter>)filter { + GTMLOGGER_ASSERT(filter_ != nil); + return [[filter_ retain] autorelease]; +} + +- (void)setFilter:(id<GTMLogFilter>)filter { + @synchronized(self) { + [filter_ autorelease]; + if (filter == nil) + filter_ = [[GTMLogNoFilter alloc] init]; + else + filter_ = [filter retain]; + } + GTMLOGGER_ASSERT(filter_ != nil); +} + +- (void)logDebug:(NSString *)fmt, ... { + va_list args; + va_start(args, fmt); + [self logInternalFunc:NULL format:fmt valist:args level:kGTMLoggerLevelDebug]; + va_end(args); +} + +- (void)logInfo:(NSString *)fmt, ... { + va_list args; + va_start(args, fmt); + [self logInternalFunc:NULL format:fmt valist:args level:kGTMLoggerLevelInfo]; + va_end(args); +} + +- (void)logError:(NSString *)fmt, ... { + va_list args; + va_start(args, fmt); + [self logInternalFunc:NULL format:fmt valist:args level:kGTMLoggerLevelError]; + va_end(args); +} + +- (void)logAssert:(NSString *)fmt, ... { + va_list args; + va_start(args, fmt); + [self logInternalFunc:NULL format:fmt valist:args level:kGTMLoggerLevelAssert]; + va_end(args); +} + +@end // GTMLogger + + +@implementation GTMLogger (GTMLoggerMacroHelpers) + +- (void)logFuncDebug:(const char *)func msg:(NSString *)fmt, ... { + va_list args; + va_start(args, fmt); + [self logInternalFunc:func format:fmt valist:args level:kGTMLoggerLevelDebug]; + va_end(args); +} + +- (void)logFuncInfo:(const char *)func msg:(NSString *)fmt, ... { + va_list args; + va_start(args, fmt); + [self logInternalFunc:func format:fmt valist:args level:kGTMLoggerLevelInfo]; + va_end(args); +} + +- (void)logFuncError:(const char *)func msg:(NSString *)fmt, ... { + va_list args; + va_start(args, fmt); + [self logInternalFunc:func format:fmt valist:args level:kGTMLoggerLevelError]; + va_end(args); +} + +- (void)logFuncAssert:(const char *)func msg:(NSString *)fmt, ... { + va_list args; + va_start(args, fmt); + [self logInternalFunc:func format:fmt valist:args level:kGTMLoggerLevelAssert]; + va_end(args); +} + +@end // GTMLoggerMacroHelpers + + +@implementation GTMLogger (PrivateMethods) + +- (void)logInternalFunc:(const char *)func + format:(NSString *)fmt + valist:(va_list)args + level:(GTMLoggerLevel)level { + GTMLOGGER_ASSERT(formatter_ != nil); + GTMLOGGER_ASSERT(filter_ != nil); + GTMLOGGER_ASSERT(writer_ != nil); + + NSString *fname = func ? [NSString stringWithUTF8String:func] : nil; + NSString *msg = [formatter_ stringForFunc:fname + withFormat:fmt + valist:args + level:level]; + if (msg && [filter_ filterAllowsMessage:msg level:level]) + [writer_ logMessage:msg level:level]; +} + +@end // PrivateMethods + + +@implementation NSFileHandle (GTMFileHandleLogWriter) + ++ (id)fileHandleForLoggingAtPath:(NSString *)path mode:(mode_t)mode { + int fd = -1; + if (path) { + int flags = O_WRONLY | O_APPEND | O_CREAT; + fd = open([path fileSystemRepresentation], flags, mode); + } + if (fd == -1) return nil; + return [[[self alloc] initWithFileDescriptor:fd + closeOnDealloc:YES] autorelease]; +} + +- (void)logMessage:(NSString *)msg level:(GTMLoggerLevel)level { + @synchronized(self) { + NSString *line = [NSString stringWithFormat:@"%@\n", msg]; + [self writeData:[line dataUsingEncoding:NSUTF8StringEncoding]]; + } +} + +@end // GTMFileHandleLogWriter + + +@implementation NSArray (GTMArrayCompositeLogWriter) + +- (void)logMessage:(NSString *)msg level:(GTMLoggerLevel)level { + @synchronized(self) { + id<GTMLogWriter> child = nil; + NSEnumerator *childEnumerator = [self objectEnumerator]; + while ((child = [childEnumerator nextObject])) { + if ([child conformsToProtocol:@protocol(GTMLogWriter)]) + [child logMessage:msg level:level]; + } + } +} + +@end // GTMArrayCompositeLogWriter + + +@implementation GTMLogger (GTMLoggerLogWriter) + +- (void)logMessage:(NSString *)msg level:(GTMLoggerLevel)level { + switch (level) { + case kGTMLoggerLevelDebug: + [self logDebug:@"%@", msg]; + break; + case kGTMLoggerLevelInfo: + [self logInfo:@"%@", msg]; + break; + case kGTMLoggerLevelError: + [self logError:@"%@", msg]; + break; + case kGTMLoggerLevelAssert: + [self logAssert:@"%@", msg]; + break; + default: + // Ignore the message. + break; + } +} + +@end // GTMLoggerLogWriter + + +@implementation GTMLogBasicFormatter + +- (NSString *)stringForFunc:(NSString *)func + withFormat:(NSString *)fmt + valist:(va_list)args + level:(GTMLoggerLevel)level { + // Performance note: since we always have to create a new NSString from the + // returned CFStringRef, we may want to do a quick check here to see if |fmt| + // contains a '%', and if not, simply return 'fmt'. + CFStringRef cfmsg = NULL; + cfmsg = CFStringCreateWithFormatAndArguments(kCFAllocatorDefault, + NULL, // format options + (CFStringRef)fmt, + args); + return [GTMNSMakeCollectable(cfmsg) autorelease]; +} + +@end // GTMLogBasicFormatter + + +@implementation GTMLogStandardFormatter + +- (id)init { + if ((self = [super init])) { + dateFormatter_ = [[NSDateFormatter alloc] init]; + [dateFormatter_ setFormatterBehavior:NSDateFormatterBehavior10_4]; + [dateFormatter_ setDateFormat:@"yyyy-MM-dd HH:mm:ss.SSS"]; + pname_ = [[[NSProcessInfo processInfo] processName] copy]; + pid_ = [[NSProcessInfo processInfo] processIdentifier]; + } + return self; +} + +- (void)dealloc { + [dateFormatter_ release]; + [pname_ release]; + [super dealloc]; +} + +- (NSString *)stringForFunc:(NSString *)func + withFormat:(NSString *)fmt + valist:(va_list)args + level:(GTMLoggerLevel)level { + GTMLOGGER_ASSERT(dateFormatter_ != nil); + NSString *tstamp = nil; + @synchronized (dateFormatter_) { + tstamp = [dateFormatter_ stringFromDate:[NSDate date]]; + } + return [NSString stringWithFormat:@"%@ %@[%d/%p] [lvl=%d] %@ %@", + tstamp, pname_, pid_, pthread_self(), + level, (func ? func : @"(no func)"), + [super stringForFunc:func withFormat:fmt valist:args level:level]]; +} + +@end // GTMLogStandardFormatter + + +@implementation GTMLogLevelFilter + +// Check the environment and the user preferences for the GTMVerboseLogging key +// to see if verbose logging has been enabled. The environment variable will +// override the defaults setting, so check the environment first. +// COV_NF_START +static BOOL IsVerboseLoggingEnabled(void) { + static NSString *const kVerboseLoggingKey = @"GTMVerboseLogging"; + static char *env = NULL; + if (env == NULL) + env = getenv([kVerboseLoggingKey UTF8String]); + + if (env && env[0]) { + return (strtol(env, NULL, 10) != 0); + } + + return [[NSUserDefaults standardUserDefaults] boolForKey:kVerboseLoggingKey]; +} +// COV_NF_END + +// In DEBUG builds, log everything. If we're not in a debug build we'll assume +// that we're in a Release build. +- (BOOL)filterAllowsMessage:(NSString *)msg level:(GTMLoggerLevel)level { +#if DEBUG + return YES; +#endif + + BOOL allow = YES; + + switch (level) { + case kGTMLoggerLevelDebug: + allow = NO; + break; + case kGTMLoggerLevelInfo: + allow = (IsVerboseLoggingEnabled() == YES); + break; + case kGTMLoggerLevelError: + allow = YES; + break; + case kGTMLoggerLevelAssert: + allow = YES; + break; + default: + allow = YES; + break; + } + + return allow; +} + +@end // GTMLogLevelFilter + + +@implementation GTMLogNoFilter + +- (BOOL)filterAllowsMessage:(NSString *)msg level:(GTMLoggerLevel)level { + return YES; // Allow everything through +} + +@end // GTMLogNoFilter diff --git a/Foundation/GTMLoggerRingBufferWriter.h b/Foundation/GTMLoggerRingBufferWriter.h new file mode 100644 index 0000000..e749a33 --- /dev/null +++ b/Foundation/GTMLoggerRingBufferWriter.h @@ -0,0 +1,96 @@ +// +// GTMLoggerRingBufferWriter.h +// +// Copyright 2007-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 "GTMLogger.h" + +typedef struct GTMRingBufferPair GTMRingBufferPair; + +// GTMLoggerRingBufferWriter is a GTMLogWriter that accumulates logged Info +// and Debug messages (when they're not compiled out in a release build) +// into a ring buffer. If an Error or Assert message is +// logged, all of the previously logged messages (up to the size of the +// buffer) are then logged. At that point the buffer resets itself. +// +// How to use: +// +// * Create a logger writer that you want to use to do the ultimate writing, +// such as to stdErr, or a log file, or an NSArray that aggregates other +// writers. +// id<GTMLoggerWriter> someWriter = ... +// +// * Make a new ring buffer with this writer, along with the buffer's +// capacity (which must be >= 1): +// rbw = +// [GTMLoggerRingBufferWriter ringBufferWriterWithCapacity:32 +// writer:someWriter]; +// +// * Set your logger's writer to be the ring buffer writer: +// [[GTMLogger sharedLogger] setWriter:rbw]; +// +// Note that this writer is at the end of the GTMLogger food chain, where the +// default filter removes Info messages in Release mode (Debug messages are +// compiled out). You can pass nil to GTMLogger's -setFilter to have it pass +// along all the messages. +// +@interface GTMLoggerRingBufferWriter : NSObject <GTMLogWriter> { + @private + id<GTMLogWriter> writer_; + GTMRingBufferPair *buffer_; + int capacity_; + int nextIndex_; // Index of the next element of |buffer_| to fill. + int totalLogged_; // This > 0 and |nextIndex_| == 0 means we've wrapped. +} + +// Returns an autoreleased ring buffer writer. If |capacity| is +// non-positive, or |writer| is nil, then nil is returned. ++ (id)ringBufferWriterWithCapacity:(int)capacity + writer:(id<GTMLogWriter>)loggerWriter; + +// Designated initializer. If |capacity| is non-positive, or |writer| +// is nil, then nil is returned. If you just use -init, nil will be returned. +- (id)initWithCapacity:(int)capacity + writer:(id<GTMLogWriter>)loggerWriter; + +// How many messages will be logged before older messages get dropped +// on the floor. +- (int)capacity; + +// The log writer that will get the buffered log messages if/when they +// need to be displayed. +- (id<GTMLogWriter>)writer; + +// How many log messages are currently in the buffer. +- (int)count; + +// How many have been dropped on the floor since creation, or the last +// reset. +- (int)droppedLogCount; + +// The total number of messages processed since creation, or the last +// reset. +- (int)totalLogged; + +// Purge the contents and reset the counters. +- (void)reset; + +// Print out the contents without resetting anything. +// Contents are automatically printed and reset when an error-level +// message comes through. +- (void)dumpContents; + +@end // GTMLoggerRingBufferWriter diff --git a/Foundation/GTMLoggerRingBufferWriter.m b/Foundation/GTMLoggerRingBufferWriter.m new file mode 100644 index 0000000..f5502b8 --- /dev/null +++ b/Foundation/GTMLoggerRingBufferWriter.m @@ -0,0 +1,248 @@ +// +// GTMLoggerRingBufferWriter.m +// +// Copyright 2007-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 "GTMLoggerRingBufferWriter.h" + +// Define a trivial assertion macro to avoid dependencies +#ifdef DEBUG +#define GTMLOGGER_ASSERT(expr) assert(expr) +#else +#define GTMLOGGER_ASSERT(expr) +#endif + +// Holds a message and a level. +struct GTMRingBufferPair { + NSString *logMessage_; + GTMLoggerLevel level_; +}; + + +// There are two operations that involve iterating over the buffer +// contents and doing Something to them. This is a callback function +// that is called for every pair living in the buffer. +typedef void (GTMRingBufferPairCallback)(GTMLoggerRingBufferWriter *rbw, + GTMRingBufferPair *pair); + + +@interface GTMLoggerRingBufferWriter (PrivateMethods) + +// Add the message and level to the ring buffer. +- (void)addMessage:(NSString *)message level:(GTMLoggerLevel)level; + +// Walk the buffer invoking the callback. +- (void)iterateBufferWithCallback:(GTMRingBufferPairCallback)callback; + +@end // PrivateMethods + + +@implementation GTMLoggerRingBufferWriter + ++ (id)ringBufferWriterWithCapacity:(int)capacity + writer:(id<GTMLogWriter>)writer { + GTMLoggerRingBufferWriter *rbw = + [[[self alloc] initWithCapacity:capacity + writer:writer] autorelease]; + return rbw; + +} // ringBufferWriterWithCapacity + + +- (id)initWithCapacity:(int)capacity + writer:(id<GTMLogWriter>)writer { + if ((self = [super init])) { + if (capacity > 0) { + writer_ = [writer retain]; + capacity_ = capacity; + + buffer_ = calloc(capacity_, sizeof(GTMRingBufferPair)); + + nextIndex_ = 0; + } + + if (buffer_ == NULL || writer_ == nil) { + [self release]; + return nil; + } + } + + return self; + +} // initWithCapacity + + +- (id)init { + return [self initWithCapacity:0 writer:nil]; +} // init + + +- (void)dealloc { + [self reset]; + + [writer_ release]; + free(buffer_); + + [super dealloc]; + +} // dealloc + + +- (int)capacity { + return capacity_; +} // capacity + + +- (id<GTMLogWriter>)writer { + return writer_; +} // writer + + +- (int)count { + int count = 0; + @synchronized(self) { + if ((nextIndex_ == 0 && totalLogged_ > 0) + || totalLogged_ >= capacity_) { + // We've wrapped around + count = capacity_; + } else { + count = nextIndex_; + } + } + + return count; + +} // count + + +- (int)droppedLogCount { + int droppedCount = 0; + + @synchronized(self) { + droppedCount = totalLogged_ - capacity_; + } + + if (droppedCount < 0) droppedCount = 0; + + return droppedCount; + +} // droppedLogCount + + +- (int)totalLogged { + return totalLogged_; +} // totalLogged + + +// Assumes caller will do any necessary synchronization. +// This walks over the buffer, taking into account any wrap-around, +// and calls the callback on each pair. +- (void)iterateBufferWithCallback:(GTMRingBufferPairCallback)callback { + GTMRingBufferPair *scan, *stop; + + // If we've wrapped around, print out the ring buffer from |nextIndex_| + // to the end. + if (totalLogged_ >= capacity_) { + scan = buffer_ + nextIndex_; + stop = buffer_ + capacity_; + while (scan < stop) { + callback(self, scan); + ++scan; + } + } + + // And then print from the beginning to right before |nextIndex_| + scan = buffer_; + stop = buffer_ + nextIndex_; + while (scan < stop) { + callback(self, scan); + ++scan; + } + +} // iterateBufferWithCallback + + +// Used when resetting the buffer. This frees the string and zeros out +// the structure. +static void ResetCallback(GTMLoggerRingBufferWriter *rbw, + GTMRingBufferPair *pair) { + [pair->logMessage_ release]; + pair->logMessage_ = nil; + pair->level_ = 0; +} // ResetCallback + + +// Reset the contents. +- (void)reset { + @synchronized(self) { + [self iterateBufferWithCallback:ResetCallback]; + nextIndex_ = 0; + totalLogged_ = 0; + } + +} // reset + + +// Go ahead and log the stored backlog, writing it through the +// ring buffer's |writer_|. +static void PrintContentsCallback(GTMLoggerRingBufferWriter *rbw, + GTMRingBufferPair *pair) { + [[rbw writer] logMessage:pair->logMessage_ level:pair->level_]; +} // PrintContentsCallback + + +- (void)dumpContents { + @synchronized(self) { + [self iterateBufferWithCallback:PrintContentsCallback]; + } +} // printContents + + +// Assumes caller will do any necessary synchronization. +- (void)addMessage:(NSString *)message level:(GTMLoggerLevel)level { + int newIndex = nextIndex_; + nextIndex_ = (nextIndex_ + 1) % capacity_; + + ++totalLogged_; + + // Sanity check + GTMLOGGER_ASSERT(buffer_ != NULL); + GTMLOGGER_ASSERT(nextIndex_ >= 0 && nextIndex_ < capacity_); + GTMLOGGER_ASSERT(newIndex >= 0 && newIndex < capacity_); + + // Now store the goodies. + GTMRingBufferPair *pair = buffer_ + newIndex; + [pair->logMessage_ release]; + pair->logMessage_ = [message copy]; + pair->level_ = level; + +} // addMessage + + +// From the GTMLogWriter protocol. +- (void)logMessage:(NSString *)message level:(GTMLoggerLevel)level { + @synchronized(self) { + [self addMessage:message level:level]; + + if (level >= kGTMLoggerLevelError) { + [self dumpContents]; + [self reset]; + } + } + +} // logMessage + +@end // GTMLoggerRingBufferWriter diff --git a/Foundation/GTMLoggerRingBufferWriterTest.m b/Foundation/GTMLoggerRingBufferWriterTest.m new file mode 100644 index 0000000..41b78e2 --- /dev/null +++ b/Foundation/GTMLoggerRingBufferWriterTest.m @@ -0,0 +1,364 @@ +// +// GTMLoggerRingBufferWriterTest.m +// +// Copyright 2007-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 "GTMSenTestCase.h" +#import "GTMLoggerRingBufferWriter.h" +#import "GTMLogger.h" + + +// -------------------------------------------------- +// CountingWriter keeps a count of the number of times it has been +// told to write something, and also keeps track of what it was +// asked to log. + +@interface CountingWriter : NSObject<GTMLogWriter> { + @private + NSMutableArray *loggedContents_; +} + +- (int)count; +- (NSArray *)loggedContents; +- (void)reset; + +@end // CountingWriter + +@implementation CountingWriter + +- (void)logMessage:(NSString *)msg level:(GTMLoggerLevel)level { + if (loggedContents_ == nil) loggedContents_ = [[NSMutableArray alloc] init]; + [loggedContents_ addObject:msg]; +} // logMessage + +- (void)dealloc { + [loggedContents_ release]; + [super dealloc]; +} // dealloc + +- (void)reset { + [loggedContents_ release]; + loggedContents_ = nil; +} // reset + +- (int)count { + return (int)[loggedContents_ count]; +} // count + +- (NSArray *)loggedContents { + return loggedContents_; +} // loggedContents + +@end // CountingWriter + + +@interface GTMLoggerRingBufferWriterTest : GTMTestCase { + @private + GTMLogger *logger_; + __weak CountingWriter *countingWriter_; +} +@end // GTMLoggerRingBufferWriterTest + + +// -------------------------------------------------- + +@implementation GTMLoggerRingBufferWriterTest + +// Utilty to compare the set of messages captured by a CountingWriter +// with an array of expected messages. The messages are expected to +// be in the same order in both places. + +- (void)compareWriter:(CountingWriter *)writer + withExpectedLogging:(NSArray *)expected + line:(int)line { + NSArray *loggedContents = [writer loggedContents]; + + STAssertEquals([expected count], [loggedContents count], + @"count mismatch from line %d", line); + + for (unsigned int i = 0; i < [expected count]; i++) { + STAssertEqualObjects([expected objectAtIndex:i], + [loggedContents objectAtIndex:i], + @"logging mistmatch at index %d from line %d", + i, line); + } + +} // compareWithExpectedLogging + + +- (void)setUp { + countingWriter_ = [[[CountingWriter alloc] init] autorelease]; + logger_ = [[GTMLogger alloc] init]; +} // setUp + + +- (void)tearDown { + [logger_ release]; +} // tearDown + + +- (void)testCreation { + + // Make sure initializers work. + GTMLoggerRingBufferWriter *writer = + [GTMLoggerRingBufferWriter ringBufferWriterWithCapacity:32 + writer:countingWriter_]; + STAssertEquals([writer capacity], 32, nil); + STAssertEquals([writer writer], countingWriter_, nil); + STAssertEquals([writer count], 0, nil); + STAssertEquals([writer droppedLogCount], 0, nil); + STAssertEquals([writer totalLogged], 0, nil); + + // Try with invalid arguments. Should always get nil back. + writer = + [GTMLoggerRingBufferWriter ringBufferWriterWithCapacity:0 + writer:countingWriter_]; + STAssertNil(writer, nil); + writer = + [GTMLoggerRingBufferWriter ringBufferWriterWithCapacity:-1 + writer:countingWriter_]; + STAssertNil(writer, nil); + writer = [GTMLoggerRingBufferWriter ringBufferWriterWithCapacity:32 + writer:nil]; + STAssertNil(writer, nil); + + writer = [[GTMLoggerRingBufferWriter alloc] init]; + STAssertNil(writer, nil); + +} // testCreation + + +- (void)testLogging { + GTMLoggerRingBufferWriter *writer = + [GTMLoggerRingBufferWriter ringBufferWriterWithCapacity:4 + writer:countingWriter_]; + [logger_ setWriter:writer]; + + // Shouldn't do anything if there are no contents. + [writer dumpContents]; + STAssertEquals([writer count], 0, nil); + STAssertEquals([countingWriter_ count], 0, nil); + + // Log a single item. Make sure the counts are accurate. + [logger_ logDebug:@"oop"]; + STAssertEquals([writer count], 1, nil); + STAssertEquals([writer totalLogged], 1, nil); + STAssertEquals([writer droppedLogCount], 0, nil); + STAssertEquals([countingWriter_ count], 0, nil); + + // Log a second item. Also make sure counts are accurate. + [logger_ logDebug:@"ack"]; + STAssertEquals([writer count], 2, nil); + STAssertEquals([writer totalLogged], 2, nil); + STAssertEquals([writer droppedLogCount], 0, nil); + STAssertEquals([countingWriter_ count], 0, nil); + + // Print them, and make sure the countingWriter sees the right stuff. + [writer dumpContents]; + STAssertEquals([countingWriter_ count], 2, nil); + STAssertEquals([writer count], 2, nil); // Should not be zeroed. + STAssertEquals([writer totalLogged], 2, nil); + + [self compareWriter:countingWriter_ + withExpectedLogging:[NSArray arrayWithObjects:@"oop",@"ack", nil] + line:__LINE__]; + + + // Wipe the slates clean. + [writer reset]; + [countingWriter_ reset]; + STAssertEquals([writer count], 0, nil); + STAssertEquals([writer totalLogged], 0, nil); + + // An error log level should print the buffer and empty it. + [logger_ logDebug:@"oop"]; + [logger_ logInfo:@"ack"]; + STAssertEquals([writer droppedLogCount], 0, nil); + STAssertEquals([writer totalLogged], 2, nil); + + [logger_ logError:@"blargh"]; + STAssertEquals([countingWriter_ count], 3, nil); + STAssertEquals([writer droppedLogCount], 0, nil); + + [self compareWriter:countingWriter_ + withExpectedLogging:[NSArray arrayWithObjects:@"oop", @"ack", + @"blargh", nil] + line:__LINE__]; + + + // An assert log level should do the same. This also fills the + // buffer to its limit without wrapping. + [countingWriter_ reset]; + [logger_ logDebug:@"oop"]; + [logger_ logInfo:@"ack"]; + [logger_ logDebug:@"blargh"]; + STAssertEquals([writer droppedLogCount], 0, nil); + STAssertEquals([writer count], 3, nil); + STAssertEquals([writer totalLogged], 3, nil); + + [logger_ logAssert:@"ouch"]; + STAssertEquals([countingWriter_ count], 4, nil); + STAssertEquals([writer droppedLogCount], 0, nil); + [self compareWriter:countingWriter_ + withExpectedLogging:[NSArray arrayWithObjects:@"oop", @"ack", + @"blargh", @"ouch", nil] + line:__LINE__]; + + + // Try with exactly one wrap around. + [countingWriter_ reset]; + [logger_ logInfo:@"ack"]; + [logger_ logDebug:@"oop"]; + [logger_ logDebug:@"blargh"]; + [logger_ logDebug:@"flong"]; // Fills buffer + STAssertEquals([writer droppedLogCount], 0, nil); + STAssertEquals([writer count], 4, nil); + + [logger_ logAssert:@"ouch"]; // should drop "ack" + STAssertEquals([countingWriter_ count], 4, nil); + + [self compareWriter:countingWriter_ + withExpectedLogging:[NSArray arrayWithObjects:@"oop", @"blargh", + @"flong", @"ouch", nil] + line:__LINE__]; + + + // Try with more than one wrap around. + [countingWriter_ reset]; + [logger_ logInfo:@"ack"]; + [logger_ logDebug:@"oop"]; + [logger_ logDebug:@"blargh"]; + [logger_ logDebug:@"flong"]; // Fills buffer + [logger_ logDebug:@"bloogie"]; // should drop "ack" + STAssertEquals([writer droppedLogCount], 1, nil); + STAssertEquals([writer count], 4, nil); + + [logger_ logAssert:@"ouch"]; // should drop "oop" + STAssertEquals([countingWriter_ count], 4, nil); + + [self compareWriter:countingWriter_ + withExpectedLogging:[NSArray arrayWithObjects:@"blargh", + @"flong", @"bloogie", @"ouch", nil] + line:__LINE__]; +} // testBasics + + +- (void)testCornerCases { + // make sure we work with small buffer sizes. + + GTMLoggerRingBufferWriter *writer = + [GTMLoggerRingBufferWriter ringBufferWriterWithCapacity:1 + writer:countingWriter_]; + [logger_ setWriter:writer]; + + [logger_ logInfo:@"ack"]; + STAssertEquals([countingWriter_ count], 0, nil); + STAssertEquals([writer count], 1, nil); + [writer dumpContents]; + STAssertEquals([countingWriter_ count], 1, nil); + + [self compareWriter:countingWriter_ + withExpectedLogging:[NSArray arrayWithObjects:@"ack", nil] + line:__LINE__]; + + [logger_ logDebug:@"oop"]; // should drop "ack" + STAssertEquals([writer count], 1, nil); + STAssertEquals([writer droppedLogCount], 1, nil); + + [countingWriter_ reset]; + [logger_ logError:@"snoogy"]; // should drop "oop" + STAssertEquals([countingWriter_ count], 1, nil); + + [self compareWriter:countingWriter_ + withExpectedLogging:[NSArray arrayWithObjects:@"snoogy", nil] + line:__LINE__]; + +} // testCornerCases + + + +// Run 10 threads, all logging through the same logger. + +static volatile int gStoppedThreads = 0; // Total number that have stopped. + +- (void)bangMe:(id)info { + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + GTMLogger *logger = (GTMLogger *)info; + + // Log a string. + for (int i = 0; i < 27; i++) { + [logger logDebug:@"oop"]; + } + + // log another string which should push the first string out. + // if we see any "oop"s in the logger output, then we know it got + // confused. + for (int i = 0; i < 15; i++) { + [logger logDebug:@"ack"]; + } + + [pool release]; + @synchronized ([self class]) { + gStoppedThreads++; + } + +} // bangMe + + +- (void)testThreading { + const int kThreadCount = 10; + const int kCapacity = 10; + + GTMLoggerRingBufferWriter *writer = + [GTMLoggerRingBufferWriter ringBufferWriterWithCapacity:kCapacity + writer:countingWriter_]; + [logger_ setWriter:writer]; + + for (int i = 0; i < kThreadCount; i++) { + [NSThread detachNewThreadSelector:@selector(bangMe:) + toTarget:self + withObject:logger_]; + } + + // The threads are running, so wait for them all to finish. + while (1) { + NSDate *quick = [NSDate dateWithTimeIntervalSinceNow:0.2]; + [[NSRunLoop currentRunLoop] runUntilDate:quick]; + @synchronized ([self class]) { + if (gStoppedThreads == kThreadCount) break; + } + } + + // Now make sure we get back what's expected. + STAssertEquals([writer count], kThreadCount, nil); + STAssertEquals([countingWriter_ count], 0, nil); // Nothing should be logged + STAssertEquals([writer totalLogged], 420, nil); + + [logger_ logError:@"bork"]; + STAssertEquals([countingWriter_ count], kCapacity, nil); + + NSArray *expected = [NSArray arrayWithObjects: + @"ack", @"ack", @"ack", @"ack", @"ack", + @"ack", @"ack", @"ack", @"ack", @"bork", + nil]; + [self compareWriter:countingWriter_ + withExpectedLogging:expected + line:__LINE__]; + +} // testThreading + +@end // GTMLoggerRingBufferWriterTest diff --git a/Foundation/GTMLoggerTest.m b/Foundation/GTMLoggerTest.m new file mode 100644 index 0000000..9093c7a --- /dev/null +++ b/Foundation/GTMLoggerTest.m @@ -0,0 +1,443 @@ +// +// GTMLoggerTest.m +// +// Copyright 2007-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 "GTMLogger.h" +#import "GTMRegex.h" +#import "GTMSenTestCase.h" + + +// A test writer that stores log messages in an array for easy retrieval. +@interface ArrayWriter : NSObject <GTMLogWriter> { + @private + NSMutableArray *messages_; +} +- (NSArray *)messages; +- (void)clear; +@end +@implementation ArrayWriter +- (id)init { + if ((self = [super init])) { + messages_ = [[NSMutableArray alloc] init]; + } + return self; +} +- (void)dealloc { + [messages_ release]; + [super dealloc]; +} +- (NSArray *)messages { + return messages_; +} +- (void)logMessage:(NSString *)msg level:(GTMLoggerLevel)level { + [messages_ addObject:msg]; +} +- (void)clear { + [messages_ removeAllObjects]; +} +@end // ArrayWriter + + +// A formatter for testing that prepends the word DUMB to log messages, along +// with the log level number. +@interface DumbFormatter : GTMLogBasicFormatter +@end +@implementation DumbFormatter +- (NSString *)stringForFunc:(NSString *)func + withFormat:(NSString *)fmt + valist:(va_list)args + level:(GTMLoggerLevel)level { + return [NSString stringWithFormat:@"DUMB [%d] %@", level, + [super stringForFunc:nil withFormat:fmt valist:args level:level]]; +} +@end // DumbFormatter + + +// A test filter that ignores messages with the string "ignore". +@interface IgnoreFilter : NSObject <GTMLogFilter> +@end +@implementation IgnoreFilter +- (BOOL)filterAllowsMessage:(NSString *)msg level:(GTMLoggerLevel)level { + NSRange range = [msg rangeOfString:@"ignore"]; + return (range.location == NSNotFound); +} +@end // IgnoreFilter + +// +// Begin test harness +// + +@interface GTMLoggerTest : GTMTestCase { + @private + NSString *path_; +} +@end + +@implementation GTMLoggerTest + +- (void)setUp { + path_ = [[NSTemporaryDirectory() stringByAppendingPathComponent: + @"GTMLoggerUnitTest.log"] retain]; + STAssertNotNil(path_, nil); + // Make sure we're cleaned up from the last run + [[NSFileManager defaultManager] removeFileAtPath:path_ handler:nil]; +} + +- (void)tearDown { + STAssertNotNil(path_, nil); + [[NSFileManager defaultManager] removeFileAtPath:path_ handler:nil]; + [path_ release]; + path_ = nil; +} + +- (void)testCreation { + GTMLogger *logger1 = nil, *logger2 = nil; + + logger1 = [GTMLogger sharedLogger]; + logger2 = [GTMLogger sharedLogger]; + + STAssertTrue(logger1 == logger2, nil); + + STAssertNotNil([logger1 writer], nil); + STAssertNotNil([logger1 formatter], nil); + STAssertNotNil([logger1 filter], nil); + + // Get a new instance; not the shared instance + logger2 = [GTMLogger standardLogger]; + + STAssertTrue(logger1 != logger2, nil); + STAssertNotNil([logger2 writer], nil); + STAssertNotNil([logger2 formatter], nil); + STAssertNotNil([logger2 filter], nil); + + // Set the new instance to be the shared logger. + [GTMLogger setSharedLogger:logger2]; + STAssertTrue(logger2 == [GTMLogger sharedLogger], nil); + STAssertTrue(logger1 != [GTMLogger sharedLogger], nil); + + // Set the shared logger to nil, which should reset it to a new "standard" + // logger. + [GTMLogger setSharedLogger:nil]; + STAssertNotNil([GTMLogger sharedLogger], nil); + STAssertTrue(logger2 != [GTMLogger sharedLogger], nil); + STAssertTrue(logger1 != [GTMLogger sharedLogger], nil); + + GTMLogger *logger = [GTMLogger logger]; + STAssertNotNil(logger, nil); + + logger = [GTMLogger standardLoggerWithStderr]; + STAssertNotNil(logger, nil); + + logger = [GTMLogger standardLoggerWithPath:path_]; + STAssertNotNil(logger, nil); +} + +- (void)testAccessors { + GTMLogger *logger = [GTMLogger standardLogger]; + STAssertNotNil(logger, nil); + + STAssertNotNil([logger writer], nil); + STAssertNotNil([logger formatter], nil); + STAssertNotNil([logger filter], nil); + + [logger setWriter:nil]; + [logger setFormatter:nil]; + [logger setFilter:nil]; + + // These attributes should NOT be nil. They should be set to their defaults. + STAssertNotNil([logger writer], nil); + STAssertNotNil([logger formatter], nil); + STAssertNotNil([logger filter], nil); +} + +- (void)testLogger { + ArrayWriter *writer = [[[ArrayWriter alloc] init] autorelease]; + IgnoreFilter *filter = [[[IgnoreFilter alloc] init] autorelease]; + + // We actually only need the array writer instance for this unit test to pass, + // but we combine that writer with a stdout writer for two reasons: + // + // 1. To test the NSArray composite writer object + // 2. To make debugging easier by sending output to stdout + // + // We also include in the array an object that is not a GTMLogWriter to make + // sure that we don't crash when presented with an array of non-GTMLogWriters. + NSArray *writers = [NSArray arrayWithObjects:writer, + [NSFileHandle fileHandleWithStandardOutput], + @"blah", nil]; + + GTMLogger *logger = [GTMLogger loggerWithWriter:writers + formatter:nil // basic formatter + filter:filter]; + + STAssertNotNil(logger, nil); + + // Log a few messages to test with + [logger logInfo:@"hi"]; + [logger logDebug:@"foo"]; + [logger logError:@"blah"]; + [logger logAssert:@"baz"]; + + // Makes sure the messages got logged + NSArray *messages = [writer messages]; + STAssertNotNil(messages, nil); + STAssertEquals([messages count], (NSUInteger)4, nil); + STAssertEqualObjects([messages objectAtIndex:0], @"hi", nil); + STAssertEqualObjects([messages objectAtIndex:1], @"foo", nil); + STAssertEqualObjects([messages objectAtIndex:2], @"blah", nil); + STAssertEqualObjects([messages objectAtIndex:3], @"baz", nil); + + // Log a message that should be ignored, and make sure it did NOT get logged + [logger logInfo:@"please ignore this"]; + messages = [writer messages]; + STAssertNotNil(messages, nil); + STAssertEquals([messages count], (NSUInteger)4, nil); + STAssertEqualObjects([messages objectAtIndex:0], @"hi", nil); + STAssertEqualObjects([messages objectAtIndex:1], @"foo", nil); + STAssertEqualObjects([messages objectAtIndex:2], @"blah", nil); + STAssertEqualObjects([messages objectAtIndex:3], @"baz", nil); + + // Change the formatter to our "dumb formatter" + id<GTMLogFormatter> formatter = [[[DumbFormatter alloc] init] autorelease]; + [logger setFormatter:formatter]; + + [logger logInfo:@"bleh"]; + messages = [writer messages]; + STAssertNotNil(messages, nil); + STAssertEquals([messages count], (NSUInteger)5, nil); // Message count should increase + // The previously logged messages should not change + STAssertEqualObjects([messages objectAtIndex:0], @"hi", nil); + STAssertEqualObjects([messages objectAtIndex:1], @"foo", nil); + STAssertEqualObjects([messages objectAtIndex:2], @"blah", nil); + STAssertEqualObjects([messages objectAtIndex:3], @"baz", nil); + STAssertEqualObjects([messages objectAtIndex:4], @"DUMB [2] bleh", nil); +} + +- (void)testConvenienceMacros { + ArrayWriter *writer = [[[ArrayWriter alloc] init] autorelease]; + NSArray *writers = [NSArray arrayWithObjects:writer, + [NSFileHandle fileHandleWithStandardOutput], nil]; + + [[GTMLogger sharedLogger] setWriter:writers]; + + // Here we log a message using a convenience macro, which should log the + // message along with the name of the function it was called from. Here we + // test to make sure the logged message does indeed contain the name of the + // current function "testConvenienceMacros". + GTMLoggerError(@"test ========================"); + STAssertEquals([[writer messages] count], (NSUInteger)1, nil); + NSRange rangeOfFuncName = + [[[writer messages] objectAtIndex:0] rangeOfString:@"testConvenienceMacros"]; + STAssertTrue(rangeOfFuncName.location != NSNotFound, nil); + [writer clear]; + + [[GTMLogger sharedLogger] setFormatter:nil]; + + GTMLoggerInfo(@"test %d", 1); + GTMLoggerDebug(@"test %d", 2); + GTMLoggerError(@"test %d", 3); + GTMLoggerAssert(@"test %d", 4); + + NSArray *messages = [writer messages]; + STAssertNotNil(messages, nil); + +#ifdef DEBUG + STAssertEquals([messages count], (NSUInteger)4, nil); + STAssertEqualObjects([messages objectAtIndex:0], @"test 1", nil); + STAssertEqualObjects([messages objectAtIndex:1], @"test 2", nil); + STAssertEqualObjects([messages objectAtIndex:2], @"test 3", nil); + STAssertEqualObjects([messages objectAtIndex:3], @"test 4", nil); +#else + // In Release builds, only the Error and Assert messages will be logged + STAssertEquals([messages count], (NSUInteger)2, nil); + STAssertEqualObjects([messages objectAtIndex:0], @"test 3", nil); + STAssertEqualObjects([messages objectAtIndex:1], @"test 4", nil); +#endif + +} + +- (void)testFileHandleWriter { + NSFileHandle *fh = nil; + + fh = [NSFileHandle fileHandleForWritingAtPath:path_]; + STAssertNil(fh, nil); + + fh = [NSFileHandle fileHandleForLoggingAtPath:path_ mode:0644]; + STAssertNotNil(fh, nil); + + [fh logMessage:@"test 0" level:kGTMLoggerLevelUnknown]; + [fh logMessage:@"test 1" level:kGTMLoggerLevelDebug]; + [fh logMessage:@"test 2" level:kGTMLoggerLevelInfo]; + [fh logMessage:@"test 3" level:kGTMLoggerLevelError]; + [fh logMessage:@"test 4" level:kGTMLoggerLevelAssert]; + [fh closeFile]; + + NSString *contents = [NSString stringWithContentsOfFile:path_]; + + STAssertEqualObjects(@"test 0\ntest 1\ntest 2\ntest 3\ntest 4\n", contents, nil); +} + +- (void)testLoggerAdapterWriter { + ArrayWriter *writer = [[[ArrayWriter alloc] init] autorelease]; + STAssertNotNil(writer, nil); + + GTMLogger *sublogger = [GTMLogger loggerWithWriter:writer + formatter:nil + filter:nil]; + STAssertNotNil(sublogger, nil); + + GTMLogger *logger = [GTMLogger loggerWithWriter:sublogger + formatter:nil + filter:nil]; + + STAssertNotNil(logger, nil); + + // Log a few messages to test with + [logger logInfo:@"hi"]; + [logger logDebug:@"foo"]; + [logger logError:@"blah"]; + [logger logAssert:@"assert"]; + + // Makes sure the messages got logged + NSArray *messages = [writer messages]; + STAssertNotNil(messages, nil); + STAssertEquals([messages count], (NSUInteger)4, nil); + STAssertEqualObjects([messages objectAtIndex:0], @"hi", nil); + STAssertEqualObjects([messages objectAtIndex:1], @"foo", nil); + STAssertEqualObjects([messages objectAtIndex:2], @"blah", nil); + STAssertEqualObjects([messages objectAtIndex:3], @"assert", nil); +} + +// Helper method to help testing GTMLogFormatters +- (NSString *)stringFromFormatter:(id<GTMLogFormatter>)formatter + level:(GTMLoggerLevel)level + format:(NSString *)fmt, ... { + va_list args; + va_start(args, fmt); + NSString *msg = [formatter stringForFunc:nil + withFormat:fmt + valist:args + level:level]; + va_end(args); + return msg; +} + +- (void)testBasicFormatter { + id<GTMLogFormatter> fmtr = [[[GTMLogBasicFormatter alloc] init] autorelease]; + STAssertNotNil(fmtr, nil); + NSString *msg = nil; + + msg = [self stringFromFormatter:fmtr + level:kGTMLoggerLevelDebug + format:@"test"]; + STAssertEqualObjects(msg, @"test", nil); + + msg = [self stringFromFormatter:fmtr + level:kGTMLoggerLevelDebug + format:@"test %d", 1]; + STAssertEqualObjects(msg, @"test 1", nil); + + msg = [self stringFromFormatter:fmtr + level:kGTMLoggerLevelDebug + format:@"test %@", @"foo"]; + STAssertEqualObjects(msg, @"test foo", nil); + + msg = [self stringFromFormatter:fmtr + level:kGTMLoggerLevelDebug + format:@""]; + STAssertEqualObjects(msg, @"", nil); + + msg = [self stringFromFormatter:fmtr + level:kGTMLoggerLevelDebug + format:@" ", 1]; + STAssertEqualObjects(msg, @" ", nil); +} + +- (void)testStandardFormatter { + id<GTMLogFormatter> fmtr = [[[GTMLogStandardFormatter alloc] init] autorelease]; + STAssertNotNil(fmtr, nil); + + // E.g. 2008-01-04 09:16:26.906 otest[5567/0xa07d0f60] [lvl=1] (no func) test + static NSString *const kFormatBasePattern = + @"[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3} otest\\[[0-9]+/0x[0-9a-f]+\\] \\[lvl=[0-3]\\] \\(no func\\) "; + NSString *msg = nil; + + msg = [self stringFromFormatter:fmtr + level:kGTMLoggerLevelDebug + format:@"test"]; + STAssertTrue([msg gtm_matchesPattern:[kFormatBasePattern stringByAppendingString:@"test"]], nil); + + msg = [self stringFromFormatter:fmtr + level:kGTMLoggerLevelError + format:@"test %d", 1]; + STAssertTrue([msg gtm_matchesPattern:[kFormatBasePattern stringByAppendingString:@"test 1"]], nil); + + + msg = [self stringFromFormatter:fmtr + level:kGTMLoggerLevelInfo + format:@"test %@", @"hi"]; + STAssertTrue([msg gtm_matchesPattern:[kFormatBasePattern stringByAppendingString:@"test hi"]], nil); + + + msg = [self stringFromFormatter:fmtr + level:kGTMLoggerLevelUnknown + format:@"test"]; + STAssertTrue([msg gtm_matchesPattern:[kFormatBasePattern stringByAppendingString:@"test"]], nil); +} + +- (void)testNoFilter { + id<GTMLogFilter> filter = [[[GTMLogNoFilter alloc] init] autorelease]; + STAssertNotNil(filter, nil); + + STAssertTrue([filter filterAllowsMessage:@"hi" level:kGTMLoggerLevelUnknown], nil); + STAssertTrue([filter filterAllowsMessage:@"hi" level:kGTMLoggerLevelDebug], nil); + STAssertTrue([filter filterAllowsMessage:@"hi" level:kGTMLoggerLevelInfo], nil); + STAssertTrue([filter filterAllowsMessage:@"hi" level:kGTMLoggerLevelError], nil); + STAssertTrue([filter filterAllowsMessage:@"hi" level:kGTMLoggerLevelAssert], nil); + STAssertTrue([filter filterAllowsMessage:@"" level:kGTMLoggerLevelDebug], nil); + STAssertTrue([filter filterAllowsMessage:nil level:kGTMLoggerLevelDebug], nil); +} + +- (void)testFileHandleCreation { + NSFileHandle *fh = nil; + + fh = [NSFileHandle fileHandleForLoggingAtPath:nil mode:0644]; + STAssertNil(fh, nil); + + fh = [NSFileHandle fileHandleForLoggingAtPath:path_ mode:0644]; + STAssertNotNil(fh, nil); + + [fh logMessage:@"test 1" level:kGTMLoggerLevelInfo]; + [fh logMessage:@"test 2" level:kGTMLoggerLevelInfo]; + [fh logMessage:@"test 3" level:kGTMLoggerLevelInfo]; + [fh closeFile]; + + // Re-open file and make sure our log messages get appended + fh = [NSFileHandle fileHandleForLoggingAtPath:path_ mode:0644]; + STAssertNotNil(fh, nil); + + [fh logMessage:@"test 4" level:kGTMLoggerLevelInfo]; + [fh logMessage:@"test 5" level:kGTMLoggerLevelInfo]; + [fh logMessage:@"test 6" level:kGTMLoggerLevelInfo]; + [fh closeFile]; + + NSString *contents = [NSString stringWithContentsOfFile:path_]; + STAssertNotNil(contents, nil); + STAssertEqualObjects(@"test 1\ntest 2\ntest 3\ntest 4\ntest 5\ntest 6\n", contents, nil); +} + +@end |