aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar thomasvl@gmail.com <thomasvl@gmail.com@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3>2008-07-21 14:49:00 +0000
committerGravatar thomasvl@gmail.com <thomasvl@gmail.com@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3>2008-07-21 14:49:00 +0000
commit4963c8a9d57501b5ffb0fe52fbfe60cd71b3b916 (patch)
treedd70e4aba13b245427788ccdb075e04f1af0951c
parentef630a18e87e03f15a2ae2b7b90623d4a0183f89 (diff)
helps if I add the files before doing the commit
-rw-r--r--Foundation/GTMLogger+ASL.h87
-rw-r--r--Foundation/GTMLogger+ASL.m121
-rw-r--r--Foundation/GTMLogger+ASLTest.m91
-rw-r--r--Foundation/GTMLogger.h461
-rw-r--r--Foundation/GTMLogger.m446
-rw-r--r--Foundation/GTMLoggerRingBufferWriter.h96
-rw-r--r--Foundation/GTMLoggerRingBufferWriter.m248
-rw-r--r--Foundation/GTMLoggerRingBufferWriterTest.m364
-rw-r--r--Foundation/GTMLoggerTest.m443
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