From 8d7f29fe04a95c7e2e485c0a7302a5a545a79812 Mon Sep 17 00:00:00 2001
From: "rnystrom@google.com" <rnystrom@google.com>
Date: Wed, 26 Feb 2014 21:14:14 +0000
Subject: [PATCH] Allow transformers to exclude and include assets.

BUG=https://code.google.com/p/dart/issues/detail?id=14594
R=nweiz@google.com

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

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge@33089 260f80e4-7a28-3924-810f-c04153c831b5
---
 lib/src/barback.dart                          | 77 ++++++++++++++++++-
 lib/src/barback/excluding_transformer.dart    | 58 ++++++++++++++
 lib/src/barback/load_all_transformers.dart    |  5 ++
 lib/src/barback/load_transformers.dart        | 20 +++--
 lib/src/pubspec.dart                          | 11 +--
 test/pubspec_test.dart                        | 53 +++++++++++--
 .../exclusion/exclude_asset_list_test.dart    | 48 ++++++++++++
 .../exclusion/exclude_asset_string_test.dart  | 45 +++++++++++
 .../exclusion/include_asset_list_test.dart    | 48 ++++++++++++
 .../exclusion/include_asset_string_test.dart  | 45 +++++++++++
 .../includes_before_excludes_test.dart        | 44 +++++++++++
 .../exclusion/works_on_dart2js_test.dart      | 43 +++++++++++
 .../works_on_transformer_group_test.dart      | 59 ++++++++++++++
 ...ubspec_with_reserved_transformer_test.dart |  2 +-
 14 files changed, 534 insertions(+), 24 deletions(-)
 create mode 100644 lib/src/barback/excluding_transformer.dart
 create mode 100644 test/transformer/exclusion/exclude_asset_list_test.dart
 create mode 100644 test/transformer/exclusion/exclude_asset_string_test.dart
 create mode 100644 test/transformer/exclusion/include_asset_list_test.dart
 create mode 100644 test/transformer/exclusion/include_asset_string_test.dart
 create mode 100644 test/transformer/exclusion/includes_before_excludes_test.dart
 create mode 100644 test/transformer/exclusion/works_on_dart2js_test.dart
 create mode 100644 test/transformer/exclusion/works_on_transformer_group_test.dart

diff --git a/lib/src/barback.dart b/lib/src/barback.dart
index ffd301f8..b0a5581a 100644
--- a/lib/src/barback.dart
+++ b/lib/src/barback.dart
@@ -60,6 +60,27 @@ class TransformerId {
   /// This will be an empty map if no configuration was provided.
   final Map configuration;
 
+  /// The primary input inclusions.
+  ///
+  /// Each inclusion is an asset path. If this set is non-empty, than *only*
+  /// matching assets are allowed as a primary input by this transformer. If
+  /// `null`, all assets are included.
+  ///
+  /// This is processed before [excludes]. If a transformer has both includes
+  /// and excludes, then the set of included assets is determined and assets
+  /// are excluded from that resulting set.
+  final Set<String> includes;
+
+  /// The primary input exclusions.
+  ///
+  /// Any asset whose pach is in this is not allowed as a primary input by
+  /// this transformer.
+  ///
+  /// This is processed after [includes]. If a transformer has both includes
+  /// and excludes, then the set of included assets is determined and assets
+  /// are excluded from that resulting set.
+  final Set<String> excludes;
+
   /// Whether this ID points to a built-in transformer exposed by pub.
   bool get isBuiltInTransformer => package.startsWith('\$');
 
@@ -79,11 +100,63 @@ class TransformerId {
     if (parts.length == 1) {
       return new TransformerId(parts.single, null, configuration);
     }
+
     return new TransformerId(parts.first, parts.last, configuration);
   }
 
-  TransformerId(this.package, this.path, Map configuration)
-      : configuration = configuration == null ? {} : configuration {
+  factory TransformerId(String package, String path, Map configuration) {
+    parseField(key) {
+      if (!configuration.containsKey(key)) return null;
+      var field = configuration.remove(key);
+
+      if (field is String) return new Set<String>.from([field]);
+
+      if (field is List) {
+        var nonstrings = field
+            .where((element) => element is! String)
+            .map((element) => '"$element"');
+
+        if (nonstrings.isNotEmpty) {
+          throw new FormatException(
+              '"$key" list field may only contain strings, but contained '
+              '${toSentence(nonstrings)}.');
+        }
+
+        return new Set<String>.from(field);
+      } else {
+        throw new FormatException(
+            '"$key" field must be a string or list, but was "$field".');
+      }
+    }
+
+    var includes = null;
+    var excludes = null;
+
+    if (configuration == null) {
+      configuration = {};
+    } else {
+      // Pull out the exclusions/inclusions.
+      includes = parseField("\$include");
+      excludes = parseField("\$exclude");
+
+      // All other keys starting with "$" are unexpected.
+      var reservedKeys = configuration.keys
+          .where((key) => key is String && key.startsWith(r'$'))
+          .map((key) => '"$key"');
+
+      if (reservedKeys.isNotEmpty) {
+        throw new FormatException(
+            'Unknown reserved ${pluralize('field', reservedKeys.length)} '
+            '${toSentence(reservedKeys)}.');
+      }
+    }
+
+    return new TransformerId._(package, path, configuration,
+        includes, excludes);
+  }
+
+  TransformerId._(this.package, this.path, this.configuration,
+      this.includes, this.excludes) {
     if (!package.startsWith('\$')) return;
     if (_BUILT_IN_TRANSFORMERS.contains(package)) return;
     throw new FormatException('Unsupported built-in transformer $package.');
diff --git a/lib/src/barback/excluding_transformer.dart b/lib/src/barback/excluding_transformer.dart
new file mode 100644
index 00000000..03b485b6
--- /dev/null
+++ b/lib/src/barback/excluding_transformer.dart
@@ -0,0 +1,58 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library pub.excluding_transformer;
+
+import 'dart:async';
+
+import 'package:barback/barback.dart';
+
+/// Decorates an inner [Transformer] and handles including and excluding
+/// primary inputs.
+class ExcludingTransformer extends Transformer {
+  /// If [includes] or [excludes] is non-null, wraps [inner] in an
+  /// [ExcludingTransformer] that handles those.
+  ///
+  /// Otherwise, just returns [inner] unmodified.
+  static Transformer wrap(Transformer inner, Set<String> includes,
+      Set<String> excludes) {
+    if (includes == null && excludes == null) return inner;
+
+    return new ExcludingTransformer._(inner, includes, excludes);
+  }
+
+  final Transformer _inner;
+
+  /// The set of asset paths which should be included.
+  ///
+  /// If `null`, all non-excluded assets are allowed. Otherwise, only included
+  /// assets are allowed.
+  final Set<String> _includes;
+
+  /// The set of assets which should be excluded.
+  ///
+  /// Exclusions are applied after inclusions.
+  final Set<String> _excludes;
+
+  ExcludingTransformer._(this._inner, this._includes, this._excludes);
+
+  Future<bool> isPrimary(Asset asset) {
+    // TODO(rnystrom): Support globs in addition to paths. See #17093.
+    if (_includes != null) {
+      // If there are any includes, it must match one of them.
+      if (!_includes.contains(asset.id.path)) return new Future.value(false);
+    }
+
+    // It must not be excluded.
+    if (_excludes != null && _excludes.contains(asset.id.path)) {
+      return new Future.value(false);
+    }
+
+    return _inner.isPrimary(asset);
+  }
+
+  Future apply(Transform transform) => _inner.apply(transform);
+
+  String toString() => _inner.toString();
+}
diff --git a/lib/src/barback/load_all_transformers.dart b/lib/src/barback/load_all_transformers.dart
index 7ff0dc56..21585cd9 100644
--- a/lib/src/barback/load_all_transformers.dart
+++ b/lib/src/barback/load_all_transformers.dart
@@ -10,6 +10,7 @@ import 'package:barback/barback.dart';
 
 import 'build_environment.dart';
 import 'dart2js_transformer.dart';
+import 'excluding_transformer.dart';
 import 'load_transformers.dart';
 import 'rewrite_import_transformer.dart';
 import '../barback.dart';
@@ -275,6 +276,10 @@ class _TransformerLoader {
     try {
       transformer = new Dart2JSTransformer.withSettings(_environment,
           new BarbackSettings(id.configuration, _environment.mode));
+
+      // Handle any exclusions.
+      transformer = ExcludingTransformer.wrap(transformer,
+          id.includes, id.excludes);
     } on FormatException catch (error, stackTrace) {
       fail(error.message, error, stackTrace);
     }
diff --git a/lib/src/barback/load_transformers.dart b/lib/src/barback/load_transformers.dart
index 72768f47..12e5c499 100644
--- a/lib/src/barback/load_transformers.dart
+++ b/lib/src/barback/load_transformers.dart
@@ -19,6 +19,7 @@ import '../dart.dart' as dart;
 import '../log.dart' as log;
 import '../utils.dart';
 import 'build_environment.dart';
+import 'excluding_transformer.dart';
 
 /// A Dart script to run in an isolate.
 ///
@@ -386,7 +387,9 @@ Future<Set> loadTransformers(BuildEnvironment environment, TransformerId id) {
         // TODO(nweiz): support non-JSON-encodable configuration maps.
         'configuration': JSON.encode(id.configuration)
       }).then((transformers) {
-        transformers = transformers.map(_deserializeTransformerOrGroup).toSet();
+        transformers = transformers.map(
+            (transformer) => _deserializeTransformerOrGroup(transformer, id))
+            .toSet();
         log.fine("Transformers from $assetId: $transformers");
         return transformers;
       });
@@ -447,9 +450,10 @@ class _ForeignGroup implements TransformerGroup {
   /// The result of calling [toString] on the transformer group in the isolate.
   final String _toString;
 
-  _ForeignGroup(Map map)
+  _ForeignGroup(TransformerId id, Map map)
       : phases = map['phases'].map((phase) {
-          return phase.map(_deserializeTransformerOrGroup).toList();
+          return phase.map((transformer) => _deserializeTransformerOrGroup(
+              transformer, id)).toList();
         }).toList(),
         _toString = map['toString'];
 
@@ -457,10 +461,14 @@ class _ForeignGroup implements TransformerGroup {
 }
 
 /// Converts a serializable map into a [Transformer] or a [TransformerGroup].
-_deserializeTransformerOrGroup(Map map) {
-  if (map['type'] == 'Transformer') return new _ForeignTransformer(map);
+_deserializeTransformerOrGroup(Map map, TransformerId id) {
+  if (map['type'] == 'Transformer') {
+    var transformer = new _ForeignTransformer(map);
+    return ExcludingTransformer.wrap(transformer, id.includes, id.excludes);
+  }
+
   assert(map['type'] == 'TransformerGroup');
-  return new _ForeignGroup(map);
+  return new _ForeignGroup(id, map);
 }
 
 /// Converts [transform] into a serializable map.
diff --git a/lib/src/pubspec.dart b/lib/src/pubspec.dart
index b44bb555..27480617 100644
--- a/lib/src/pubspec.dart
+++ b/lib/src/pubspec.dart
@@ -162,18 +162,9 @@ class Pubspec {
             _error('"$field.$library" field must be a map, but was '
                 '"$configuration".');
           }
-
-          var reservedKeys = configuration.keys
-              .where((key) => key is String && key.startsWith(r'$'))
-              .map((key) => '"$key"');
-          if (reservedKeys.isNotEmpty) {
-            _error('"$field.$library" field cannot contain reserved '
-                '${pluralize('field', reservedKeys.length)} '
-                '${toSentence(reservedKeys)}.');
-          }
         }
 
-        var id = _wrapFormatException("transformer identifier",
+        var id = _wrapFormatException("transformer configuration",
             "$field.$library",
             () => new TransformerId.parse(library, configuration));
 
diff --git a/test/pubspec_test.dart b/test/pubspec_test.dart
index f451f1fb..a730a14b 100644
--- a/test/pubspec_test.dart
+++ b/test/pubspec_test.dart
@@ -241,13 +241,14 @@ dependencies:
           '"transformers.pkg" field must be a map');
     });
 
-    test("throws if a transformer's configuration contains a top-level key "
-        "beginning with a dollar sign", () {
+    test("throws if a transformer's configuration contains an unknown "
+        "reserved key at the top level", () {
       expectPubspecException('''
 name: pkg
 transformers: [{pkg: {\$key: "value"}}]''',
           (pubspec) => pubspec.transformers,
-          '"transformers.pkg" field cannot contain reserved field "\$key"');
+          'Invalid transformer configuration for "transformers.pkg": '
+          'Unknown reserved field "\$key"');
     });
 
     test("doesn't throw if a transformer's configuration contains a "
@@ -262,13 +263,55 @@ transformers:
       expect(pkg.configuration["outer"]["\$inner"], equals("value"));
     });
 
+    test("throws if the \$include value is not a string or list", () {
+      expectPubspecException('''
+name: pkg
+transformers:
+- pkg: {\$include: 123}''',
+          (pubspec) => pubspec.transformers,
+          'Invalid transformer configuration for "transformers.pkg": '
+          '"\$include" field must be a string or list, but was "123"');
+    });
+
+    test("throws if the \$include list contains a non-string", () {
+      expectPubspecException('''
+name: pkg
+transformers:
+- pkg: {\$include: ["ok", 123, "alright", null]}''',
+        (pubspec) => pubspec.transformers,
+        'Invalid transformer configuration for "transformers.pkg": '
+        '"\$include" list field may only contain strings, but contained '
+        '"123" and "null"');
+    });
+
+    test("throws if the \$exclude value is not a string or list", () {
+      expectPubspecException('''
+name: pkg
+transformers:
+- pkg: {\$exclude: 123}''',
+        (pubspec) => pubspec.transformers,
+        'Invalid transformer configuration for "transformers.pkg": '
+        '"\$exclude" field must be a string or list, but was "123"');
+    });
+
+    test("throws if the \$exclude list contains a non-string", () {
+      expectPubspecException('''
+name: pkg
+transformers:
+- pkg: {\$exclude: ["ok", 123, "alright", null]}''',
+        (pubspec) => pubspec.transformers,
+        'Invalid transformer configuration for "transformers.pkg": '
+        '"\$exclude" list field may only contain strings, but contained '
+        '"123" and "null"');
+    });
+
     test("throws if a transformer is not from a dependency", () {
       expectPubspecException('''
 name: pkg
 transformers: [foo]
 ''',
-          (pubspec) => pubspec.transformers,
-          '"transformers.foo" refers to a package that\'s not a dependency.');
+        (pubspec) => pubspec.transformers,
+        '"transformers.foo" refers to a package that\'s not a dependency.');
     });
 
     test("allows a transformer from a normal dependency", () {
diff --git a/test/transformer/exclusion/exclude_asset_list_test.dart b/test/transformer/exclusion/exclude_asset_list_test.dart
new file mode 100644
index 00000000..81fc9ca0
--- /dev/null
+++ b/test/transformer/exclusion/exclude_asset_list_test.dart
@@ -0,0 +1,48 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS d.file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library pub_tests;
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+import '../../serve/utils.dart';
+
+main() {
+  initConfig();
+  integration("excludes a list of assets", () {
+    d.dir(appPath, [
+      d.pubspec({
+        "name": "myapp",
+        "transformers": [
+          {
+            "myapp/src/transformer": {
+              "\$exclude": [
+                "web/foo.txt",
+                "web/sub/foo.txt"
+              ]
+            }
+          }
+        ]
+      }),
+      d.dir("lib", [d.dir("src", [
+        d.file("transformer.dart", REWRITE_TRANSFORMER)
+      ])]),
+      d.dir("web", [
+        d.file("foo.txt", "foo"),
+        d.file("bar.txt", "bar"),
+        d.dir("sub", [
+          d.file("foo.txt", "foo"),
+        ])
+      ])
+    ]).create();
+
+    createLockFile('myapp', pkg: ['barback']);
+
+    pubServe();
+    requestShould404("foo.out");
+    requestShould404("sub/foo.out");
+    requestShouldSucceed("bar.out", "bar.out");
+    endPubServe();
+  });
+}
diff --git a/test/transformer/exclusion/exclude_asset_string_test.dart b/test/transformer/exclusion/exclude_asset_string_test.dart
new file mode 100644
index 00000000..b5bf7141
--- /dev/null
+++ b/test/transformer/exclusion/exclude_asset_string_test.dart
@@ -0,0 +1,45 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS d.file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library pub_tests;
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+import '../../serve/utils.dart';
+
+main() {
+  initConfig();
+  integration("allows a single string as the asset to exclude", () {
+    d.dir(appPath, [
+      d.pubspec({
+        "name": "myapp",
+        "transformers": [
+          {
+            "myapp/src/transformer": {
+              "\$exclude": "web/foo.txt"
+            }
+          }
+        ]
+      }),
+      d.dir("lib", [d.dir("src", [
+        d.file("transformer.dart", REWRITE_TRANSFORMER)
+      ])]),
+      d.dir("web", [
+        d.file("foo.txt", "foo"),
+        d.file("bar.txt", "bar"),
+        d.dir("sub", [
+          d.file("foo.txt", "foo"),
+        ])
+      ])
+    ]).create();
+
+    createLockFile('myapp', pkg: ['barback']);
+
+    pubServe();
+    requestShould404("foo.out");
+    requestShouldSucceed("sub/foo.out", "foo.out");
+    requestShouldSucceed("bar.out", "bar.out");
+    endPubServe();
+  });
+}
diff --git a/test/transformer/exclusion/include_asset_list_test.dart b/test/transformer/exclusion/include_asset_list_test.dart
new file mode 100644
index 00000000..aac64323
--- /dev/null
+++ b/test/transformer/exclusion/include_asset_list_test.dart
@@ -0,0 +1,48 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS d.file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library pub_tests;
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+import '../../serve/utils.dart';
+
+main() {
+  initConfig();
+  integration("includes assets", () {
+    d.dir(appPath, [
+      d.pubspec({
+        "name": "myapp",
+        "transformers": [
+          {
+            "myapp/src/transformer": {
+              "\$include": [
+                "web/foo.txt",
+                "web/sub/foo.txt"
+              ]
+            }
+          }
+        ]
+      }),
+      d.dir("lib", [d.dir("src", [
+        d.file("transformer.dart", REWRITE_TRANSFORMER)
+      ])]),
+      d.dir("web", [
+        d.file("foo.txt", "foo"),
+        d.file("bar.txt", "bar"),
+        d.dir("sub", [
+          d.file("foo.txt", "foo"),
+        ])
+      ])
+    ]).create();
+
+    createLockFile('myapp', pkg: ['barback']);
+
+    pubServe();
+    requestShouldSucceed("foo.out", "foo.out");
+    requestShouldSucceed("sub/foo.out", "foo.out");
+    requestShould404("bar.out");
+    endPubServe();
+  });
+}
diff --git a/test/transformer/exclusion/include_asset_string_test.dart b/test/transformer/exclusion/include_asset_string_test.dart
new file mode 100644
index 00000000..02891267
--- /dev/null
+++ b/test/transformer/exclusion/include_asset_string_test.dart
@@ -0,0 +1,45 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS d.file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library pub_tests;
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+import '../../serve/utils.dart';
+
+main() {
+  initConfig();
+  integration("allows a single string as the asset to include", () {
+    d.dir(appPath, [
+      d.pubspec({
+        "name": "myapp",
+        "transformers": [
+          {
+            "myapp/src/transformer": {
+              "\$include": "web/foo.txt"
+            }
+          }
+        ]
+      }),
+      d.dir("lib", [d.dir("src", [
+        d.file("transformer.dart", REWRITE_TRANSFORMER)
+      ])]),
+      d.dir("web", [
+        d.file("foo.txt", "foo"),
+        d.file("bar.txt", "bar"),
+        d.dir("sub", [
+          d.file("foo.txt", "foo"),
+        ])
+      ])
+    ]).create();
+
+    createLockFile('myapp', pkg: ['barback']);
+
+    pubServe();
+    requestShouldSucceed("foo.out", "foo.out");
+    requestShould404("sub/foo.out");
+    requestShould404("bar.out");
+    endPubServe();
+  });
+}
diff --git a/test/transformer/exclusion/includes_before_excludes_test.dart b/test/transformer/exclusion/includes_before_excludes_test.dart
new file mode 100644
index 00000000..4d7f436d
--- /dev/null
+++ b/test/transformer/exclusion/includes_before_excludes_test.dart
@@ -0,0 +1,44 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS d.file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library pub_tests;
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+import '../../serve/utils.dart';
+
+main() {
+  initConfig();
+  integration("applies includes before excludes if both are present", () {
+    d.dir(appPath, [
+      d.pubspec({
+        "name": "myapp",
+        "transformers": [
+          {
+            "myapp/src/transformer": {
+              "\$include": ["web/a.txt", "web/b.txt"],
+              "\$exclude": "web/a.txt"
+            }
+          }
+        ]
+      }),
+      d.dir("lib", [d.dir("src", [
+        d.file("transformer.dart", REWRITE_TRANSFORMER)
+      ])]),
+      d.dir("web", [
+        d.file("a.txt", "a.txt"),
+        d.file("b.txt", "b.txt"),
+        d.file("c.txt", "c.txt")
+      ])
+    ]).create();
+
+    createLockFile('myapp', pkg: ['barback']);
+
+    pubServe();
+    requestShould404("a.out");
+    requestShouldSucceed("b.out", "b.txt.out");
+    requestShould404("c.out");
+    endPubServe();
+  });
+}
diff --git a/test/transformer/exclusion/works_on_dart2js_test.dart b/test/transformer/exclusion/works_on_dart2js_test.dart
new file mode 100644
index 00000000..aedfbf97
--- /dev/null
+++ b/test/transformer/exclusion/works_on_dart2js_test.dart
@@ -0,0 +1,43 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS d.file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library pub_tests;
+
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+import '../../serve/utils.dart';
+
+main() {
+  initConfig();
+  integration("works on the dart2js transformer", () {
+    d.dir(appPath, [
+      d.pubspec({
+        "name": "myapp",
+        "transformers": [
+          {
+            "\$dart2js": {
+              "\$include": ["web/a.dart", "web/b.dart"],
+              "\$exclude": "web/a.dart"
+            }
+          }
+        ]
+      }),
+      d.dir("web", [
+        d.file("a.dart", "void main() => print('hello');"),
+        d.file("b.dart", "void main() => print('hello');"),
+        d.file("c.dart", "void main() => print('hello');")
+      ])
+    ]).create();
+
+    createLockFile('myapp', pkg: ['barback']);
+
+    pubServe();
+    requestShould404("a.dart.js");
+    requestShouldSucceed("b.dart.js", isNot(isEmpty));
+    requestShould404("c.dart.js");
+    endPubServe();
+  });
+}
diff --git a/test/transformer/exclusion/works_on_transformer_group_test.dart b/test/transformer/exclusion/works_on_transformer_group_test.dart
new file mode 100644
index 00000000..d4164bd1
--- /dev/null
+++ b/test/transformer/exclusion/works_on_transformer_group_test.dart
@@ -0,0 +1,59 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS d.file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library pub_tests;
+
+import 'package:scheduled_test/scheduled_test.dart';
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+import '../../serve/utils.dart';
+
+const GROUP = """
+import 'package:barback/barback.dart';
+
+import 'transformer.dart';
+
+class RewriteGroup implements TransformerGroup {
+  RewriteGroup.asPlugin();
+
+  Iterable<Iterable> get phases => [[new RewriteTransformer.asPlugin()]];
+}
+""";
+
+main() {
+  initConfig();
+  integration("works on a transformer group", () {
+    d.dir(appPath, [
+      d.pubspec({
+        "name": "myapp",
+        "transformers": [
+          {
+            "myapp/src/group": {
+              "\$include": ["web/a.txt", "web/b.txt"],
+              "\$exclude": "web/a.txt"
+            }
+          }
+        ]
+      }),
+      d.dir("lib", [d.dir("src", [
+        d.file("transformer.dart", REWRITE_TRANSFORMER),
+        d.file("group.dart", GROUP)
+      ])]),
+      d.dir("web", [
+        d.file("a.txt", "a.txt"),
+        d.file("b.txt", "b.txt"),
+        d.file("c.txt", "c.txt")
+      ])
+    ]).create();
+
+    createLockFile('myapp', pkg: ['barback']);
+
+    pubServe();
+    requestShould404("a.out");
+    requestShouldSucceed("b.out", "b.txt.out");
+    requestShould404("c.out");
+    endPubServe();
+  });
+}
diff --git a/test/transformer/fails_to_load_a_pubspec_with_reserved_transformer_test.dart b/test/transformer/fails_to_load_a_pubspec_with_reserved_transformer_test.dart
index f004b236..0b00b037 100644
--- a/test/transformer/fails_to_load_a_pubspec_with_reserved_transformer_test.dart
+++ b/test/transformer/fails_to_load_a_pubspec_with_reserved_transformer_test.dart
@@ -29,7 +29,7 @@ main() {
     var pub = startPubServe();
     pub.stderr.expect(emitsLines(
         'Error in pubspec for package "myapp" loaded from pubspec.yaml:\n'
-        'Invalid transformer identifier for "transformers.\$nonexistent": '
+        'Invalid transformer configuration for "transformers.\$nonexistent": '
             'Unsupported built-in transformer \$nonexistent.'));
     pub.shouldExit(1);
   });
-- 
GitLab