pkg/logging: added stackTrace support

Plus:

* don't expose mutable children collection from Logger
* remove superfluous field
* other tweaks

BUG=https://code.google.com/p/dart/issues/detail?id=14416
R=sethladd@google.com, sigmund@google.com

Review URL: https://codereview.chromium.org//46233002

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/logging@29324 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/lib/logging.dart b/lib/logging.dart
index 3376145..071b496 100644
--- a/lib/logging.dart
+++ b/lib/logging.dart
@@ -5,9 +5,8 @@
 /**
  * Support for debugging and error logging.
  *
- * This library introduces abstractions similar to
- * those used in other languages, such as the Closure JS
- * Logger and java.util.logging.Logger.
+ * This library introduces abstractions similar to those used in other
+ * languages, such as the Closure JS Logger and java.util.logging.Logger.
  *
  * For information on installing and importing this library, see the
  * [logging package on pub.dartlang.org]
@@ -16,6 +15,8 @@
 library logging;
 
 import 'dart:async';
+import 'package:meta/meta.dart';
+import 'package:unmodifiable_collection/unmodifiable_collection.dart';
 
 /**
  * Whether to allow fine-grain logging and configuration of loggers in a
@@ -48,26 +49,26 @@
   /** Logging [Level] used for entries generated on this logger. */
   Level _level;
 
+  final Map<String, Logger> _children;
+
   /** Children in the hierarchy of loggers, indexed by their simple names. */
-  Map<String, Logger> children;
+  final Map<String, Logger> children;
 
   /** Controller used to notify when log entries are added to this logger. */
   StreamController<LogRecord> _controller;
 
-  /** The broadcast stream associated with the controller. */
-  Stream _stream;
-
   /**
    * Singleton constructor. Calling `new Logger(name)` will return the same
    * actual instance whenever it is called with the same string name.
    */
   factory Logger(String name) {
+    return _loggers.putIfAbsent(name, () => new Logger._named(name));
+  }
+
+  factory Logger._named(String name) {
     if (name.startsWith('.')) {
       throw new ArgumentError("name shouldn't start with a '.'");
     }
-    if (_loggers == null) _loggers = <String, Logger>{};
-    if (_loggers.containsKey(name)) return _loggers[name];
-
     // Split hierarchical names (separated with '.').
     int dot = name.lastIndexOf('.');
     Logger parent = null;
@@ -79,14 +80,13 @@
       parent = new Logger(name.substring(0, dot));
       thisName = name.substring(dot + 1);
     }
-    final res = new Logger._internal(thisName, parent);
-    _loggers[name] = res;
-    return res;
+    return new Logger._internal(thisName, parent, new Map<String, Logger>());
   }
 
-  Logger._internal(this.name, this.parent)
-      : children = new Map<String, Logger>() {
-    if (parent != null) parent.children[name] = this;
+  Logger._internal(this.name, this.parent, Map<String, Logger> children) :
+    this._children = children,
+    this.children = new UnmodifiableMapView(children) {
+    if (parent != null) parent._children[name] = this;
   }
 
   /**
@@ -102,7 +102,7 @@
   }
 
   /** Override the level for this particular [Logger] and its children. */
-  set level(Level value) {
+  void set level(Level value) {
     if (hierarchicalLoggingEnabled && parent != null) {
       _level = value;
     } else {
@@ -138,14 +138,18 @@
 
   /**
    * Adds a log record for a [message] at a particular [logLevel] if
-   * `isLoggable(logLevel)` is true. Use this method to create log entries for
-   * user-defined levels. To record a message at a predefined level (e.g.
-   * [Level.INFO], [Level.WARNING], etc) you can use their specialized methods
-   * instead (e.g. [info], [warning], etc).
+   * `isLoggable(logLevel)` is true.
+   *
+   * Use this method to create log entries for user-defined levels. To record a
+   * message at a predefined level (e.g. [Level.INFO], [Level.WARNING], etc) you
+   * can use their specialized methods instead (e.g. [info], [warning], etc).
    */
-  void log(Level logLevel, String message, [exception]) {
+  void log(Level logLevel, String message, [Object error,
+                                            StackTrace stackTrace]) {
     if (isLoggable(logLevel)) {
-      var record = new LogRecord(logLevel, message, fullName, exception);
+      var record = new LogRecord(logLevel, message, fullName, error,
+          stackTrace);
+
       if (hierarchicalLoggingEnabled) {
         var target = this;
         while (target != null) {
@@ -159,44 +163,43 @@
   }
 
   /** Log message at level [Level.FINEST]. */
-  void finest(String message, [exception]) =>
-      log(Level.FINEST, message, exception);
+  void finest(String message, [Object error, StackTrace stackTrace]) =>
+      log(Level.FINEST, message, error, stackTrace);
 
   /** Log message at level [Level.FINER]. */
-  void finer(String message, [exception]) =>
-      log(Level.FINER, message, exception);
+  void finer(String message, [Object error, StackTrace stackTrace]) =>
+      log(Level.FINER, message, error, stackTrace);
 
   /** Log message at level [Level.FINE]. */
-  void fine(String message, [exception]) =>
-      log(Level.FINE, message, exception);
+  void fine(String message, [Object error, StackTrace stackTrace]) =>
+      log(Level.FINE, message, error, stackTrace);
 
   /** Log message at level [Level.CONFIG]. */
-  void config(String message, [exception]) =>
-      log(Level.CONFIG, message, exception);
+  void config(String message, [Object error, StackTrace stackTrace]) =>
+      log(Level.CONFIG, message, error, stackTrace);
 
   /** Log message at level [Level.INFO]. */
-  void info(String message, [exception]) =>
-      log(Level.INFO, message, exception);
+  void info(String message, [Object error, StackTrace stackTrace]) =>
+      log(Level.INFO, message, error, stackTrace);
 
   /** Log message at level [Level.WARNING]. */
-  void warning(String message, [exception]) =>
-      log(Level.WARNING, message, exception);
+  void warning(String message, [Object error, StackTrace stackTrace]) =>
+      log(Level.WARNING, message, error, stackTrace);
 
   /** Log message at level [Level.SEVERE]. */
-  void severe(String message, [exception]) =>
-      log(Level.SEVERE, message, exception);
+  void severe(String message, [Object error, StackTrace stackTrace]) =>
+      log(Level.SEVERE, message, error, stackTrace);
 
   /** Log message at level [Level.SHOUT]. */
-  void shout(String message, [exception]) =>
-      log(Level.SHOUT, message, exception);
+  void shout(String message, [Object error, StackTrace stackTrace]) =>
+      log(Level.SHOUT, message, error, stackTrace);
 
   Stream<LogRecord> _getStream() {
     if (hierarchicalLoggingEnabled || parent == null) {
       if (_controller == null) {
         _controller = new StreamController<LogRecord>.broadcast(sync: true);
-        _stream = _controller.stream;
       }
-      return _stream;
+      return _controller.stream;
     } else {
       return root._getStream();
     }
@@ -212,7 +215,7 @@
   static Logger get root => new Logger('');
 
   /** All [Logger]s in the system. */
-  static Map<String, Logger> _loggers;
+  static final Map<String, Logger> _loggers = <String, Logger>{};
 }
 
 
@@ -233,7 +236,6 @@
  */
 class Level implements Comparable<Level> {
 
-  // TODO(sigmund): mark name/value as 'const' when the language supports it.
   final String name;
 
   /**
@@ -274,7 +276,7 @@
   /** Key for extra debugging loudness ([value] = 1200). */
   static const Level SHOUT = const Level('SHOUT', 1200);
 
-  bool operator ==(Level other) => other != null && value == other.value;
+  bool operator ==(Object other) => other is Level && value == other.value;
   bool operator <(Level other) => value < other.value;
   bool operator <=(Level other) => value <= other.value;
   bool operator >(Level other) => value > other.value;
@@ -304,10 +306,21 @@
 
   static int _nextNumber = 0;
 
-  /** Associated exception (if any) when recording errors messages. */
-  var exception;
+  /** Associated error (if any) when recording errors messages. */
+  final Object error;
 
-  LogRecord(this.level, this.message, this.loggerName, [this.exception])
+  // TODO(kevmoo) - remove before V1
+  /**
+   * DEPRECATED. Use [error] instead.
+   */
+  @deprecated
+  Object get exception => error;
+
+  /** Associated stackTrace (if any) when recording errors messages. */
+  final StackTrace stackTrace;
+
+  LogRecord(this.level, this.message, this.loggerName, [this.error,
+                                                        this.stackTrace])
       : time = new DateTime.now(),
         sequenceNumber = LogRecord._nextNumber++;
 
diff --git a/pubspec.yaml b/pubspec.yaml
index 37ed46e..c23bd3e 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -3,5 +3,8 @@
 description: Provides APIs for debugging and error logging. This library introduces abstractions similar to those used in other languages, such as the Closure JS Logger and java.util.logging.Logger.
 homepage: http://www.dartlang.org
 documentation: http://api.dartlang.org/docs/pkg/logging
+dependencies:
+  meta: any
+  unmodifiable_collection: any
 dev_dependencies:
   unittest: any
diff --git a/test/logging_test.dart b/test/logging_test.dart
index a631e8c..1c97350 100644
--- a/test/logging_test.dart
+++ b/test/logging_test.dart
@@ -24,8 +24,8 @@
     expect(level2 > level1, isTrue);
 
     var level3 = const Level('NOT_REAL3', 253);
-    expect(!identical(level1, level3), isTrue); // different instances
-    expect(level1 == level3, isTrue); // same value.
+    expect(level1, isNot(same(level3))); // different instances
+    expect(level1, equals(level3)); // same value.
   });
 
   test('default levels are in order', () {
@@ -60,8 +60,8 @@
     var map = new Map<Level, String>();
     map[Level.INFO] = 'info';
     map[Level.SHOUT] = 'shout';
-    expect(map[Level.INFO], equals('info'));
-    expect(map[Level.SHOUT], equals('shout'));
+    expect(map[Level.INFO], same('info'));
+    expect(map[Level.SHOUT], same('shout'));
   });
 
   test('logger name cannot start with a "." ', () {
@@ -90,10 +90,10 @@
     Logger a = new Logger('a');
     Logger b = new Logger('a.b');
     Logger c = new Logger('a.c');
-    expect(a == b.parent, isTrue);
-    expect(a == c.parent, isTrue);
-    expect(a.children['b'] == b, isTrue);
-    expect(a.children['c'] == c, isTrue);
+    expect(a, same(b.parent));
+    expect(a, same(c.parent));
+    expect(a.children['b'], same(b));
+    expect(a.children['c'], same(c));
   });
 
   test('loggers are singletons', () {
@@ -101,10 +101,57 @@
     Logger a2 = new Logger('a');
     Logger b = new Logger('a.b');
     Logger root = Logger.root;
-    expect(identical(a1, a2), isTrue);
-    expect(identical(a1, b.parent), isTrue);
-    expect(identical(root, a1.parent), isTrue);
-    expect(identical(root, new Logger('')), isTrue);
+    expect(a1, same(a2));
+    expect(a1, same(b.parent));
+    expect(root, same(a1.parent));
+    expect(root, same(new Logger('')));
+  });
+
+  test('cannot directly manipulate Logger.children', () {
+    var loggerAB = new Logger('a.b');
+    var loggerA = loggerAB.parent;
+
+    expect(loggerA.children['b'], same(loggerAB), reason: 'can read Children');
+
+    expect(() {
+        loggerAB.children['test'] = null;
+    }, throwsUnsupportedError, reason: 'Children is read-only');
+  });
+
+  test('stackTrace gets throw to LogRecord', () {
+    Logger.root.level = Level.INFO;
+
+    var records = new List<LogRecord>();
+
+    var sub = Logger.root.onRecord.listen(records.add);
+
+    try {
+      throw new UnsupportedError('test exception');
+    } catch(error, stack) {
+      Logger.root.log(Level.SEVERE, 'severe', error, stack);
+      Logger.root.warning('warning', error, stack);
+    }
+
+    Logger.root.log(Level.SHOUT, 'shout');
+
+    sub.cancel();
+
+    expect(records, hasLength(3));
+
+    var severe = records[0];
+    expect(severe.message, 'severe');
+    expect(severe.error is UnsupportedError, isTrue);
+    expect(severe.stackTrace is StackTrace, isTrue);
+
+    var warning = records[1];
+    expect(warning.message, 'warning');
+    expect(warning.error is UnsupportedError, isTrue);
+    expect(warning.stackTrace is StackTrace, isTrue);
+
+    var shout = records[2];
+    expect(shout.message, 'shout');
+    expect(shout.error, isNull);
+    expect(shout.stackTrace, isNull);
   });
 
   group('mutating levels', () {
@@ -134,7 +181,7 @@
     });
 
     test('cannot set level if hierarchy is disabled', () {
-      expect(() {a.level = Level.FINE;}, throws);
+      expect(() {a.level = Level.FINE;}, throwsUnsupportedError);
     });
 
     test('loggers effective level - no hierarchy', () {
@@ -176,7 +223,7 @@
       expect(root.isLoggable(Level.WARNING), isFalse);
       expect(c.isLoggable(Level.FINEST), isTrue);
       expect(c.isLoggable(Level.FINE), isTrue);
-      expect(!e.isLoggable(Level.SHOUT), isTrue);
+      expect(e.isLoggable(Level.SHOUT), isFalse);
     });
 
     test('add/remove handlers - no hierarchy', () {