Version 0.4.3.5

svn merge -c 20575 https://dart.googlecode.com/svn/branches/bleeding_edge/dart dart
svn merge -c 20576 https://dart.googlecode.com/svn/branches/bleeding_edge/dart dart
svn merge -c 20577 https://dart.googlecode.com/svn/branches/bleeding_edge/dart dart
Review URL: https://codereview.chromium.org//13132002

git-svn-id: http://dart.googlecode.com/svn/trunk@20602 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/runtime/vm/class_finalizer.cc b/runtime/vm/class_finalizer.cc
index 400f1fa..d4173cc 100644
--- a/runtime/vm/class_finalizer.cc
+++ b/runtime/vm/class_finalizer.cc
@@ -627,7 +627,8 @@
   const intptr_t offset = cls.NumTypeArguments() - num_type_params;
   AbstractType& type_arg = AbstractType::Handle();
   AbstractType& cls_type_param = AbstractType::Handle();
-  AbstractType& bound = AbstractType::Handle();
+  AbstractType& declared_bound = AbstractType::Handle();
+  AbstractType& instantiated_bound = AbstractType::Handle();
   const TypeArguments& cls_type_params =
       TypeArguments::Handle(cls.type_parameters());
   ASSERT((cls_type_params.IsNull() && (num_type_params == 0)) ||
@@ -640,33 +641,41 @@
     cls_type_param = cls_type_params.TypeAt(i);
     const TypeParameter& type_param = TypeParameter::Cast(cls_type_param);
     ASSERT(type_param.IsFinalized());
-    bound = type_param.bound();
-    if (!bound.IsObjectType() && !bound.IsDynamicType()) {
+    declared_bound = type_param.bound();
+    if (!declared_bound.IsObjectType() && !declared_bound.IsDynamicType()) {
       Error& malformed_error = Error::Handle();
       // Note that the bound may be malformed, in which case the bound check
       // will return an error and the bound check will be postponed to run time.
       // Note also that the bound may still be unfinalized.
-      if (!bound.IsFinalized()) {
-        ASSERT(bound.IsBeingFinalized());
+      if (!declared_bound.IsFinalized()) {
+        ASSERT(declared_bound.IsBeingFinalized());
         // The bound refers to type parameters, creating a cycle; postpone
         // bound check to run time, when the bound will be finalized.
         // TODO(regis): Do we need to instantiate an uninstantiated bound here?
-        type_arg = BoundedType::New(type_arg, bound, type_param);
+        type_arg = BoundedType::New(type_arg, declared_bound, type_param);
         arguments.SetTypeAt(offset + i, type_arg);
         continue;
       }
-      if (!bound.IsInstantiated()) {
-        bound = bound.InstantiateFrom(arguments, &malformed_error);
+      if (declared_bound.IsInstantiated()) {
+        instantiated_bound = declared_bound.raw();
+      } else {
+        instantiated_bound =
+            declared_bound.InstantiateFrom(arguments, &malformed_error);
       }
       // TODO(regis): We could simplify this code if we could differentiate
       // between a failed bound check and a bound check that is undecidable at
       // compile time.
-      if (malformed_error.IsNull()) {
-        type_param.CheckBound(type_arg, bound, &malformed_error);
+      // Shortcut the special case where we check a type parameter against its
+      // declared upper bound.
+      if (malformed_error.IsNull() &&
+          (!type_arg.Equals(type_param) ||
+           !instantiated_bound.Equals(declared_bound))) {
+        type_param.CheckBound(type_arg, instantiated_bound, &malformed_error);
       }
       if (!malformed_error.IsNull()) {
-        if (!type_arg.IsInstantiated() || !bound.IsInstantiated()) {
-          type_arg = BoundedType::New(type_arg, bound, type_param);
+        if (!type_arg.IsInstantiated() ||
+            !instantiated_bound.IsInstantiated()) {
+          type_arg = BoundedType::New(type_arg, instantiated_bound, type_param);
           arguments.SetTypeAt(offset + i, type_arg);
         } else if (bound_error->IsNull()) {
           *bound_error = malformed_error.raw();
@@ -1021,7 +1030,8 @@
   for (intptr_t i = 0; i < num_type_params; i++) {
     type_param ^= type_params.TypeAt(i);
     bound = type_param.bound();
-    if (bound.IsFinalized()) {
+    if (bound.IsFinalized() || bound.IsBeingFinalized()) {
+      // A bound involved in F-bounded quantification may form a cycle.
       continue;
     }
     ResolveType(cls, bound, kCanonicalize);
diff --git a/runtime/vm/object.cc b/runtime/vm/object.cc
index 121788b..3c1643c 100644
--- a/runtime/vm/object.cc
+++ b/runtime/vm/object.cc
@@ -9179,7 +9179,13 @@
         num_type_params = 0;
       }
     } else {
-      first_type_param_index = num_args - num_type_params;
+      // The actual type argument vector can be longer than necessary, because
+      // of type optimizations.
+      if (IsFinalized() && cls.is_finalized()) {
+        first_type_param_index = cls.NumTypeArguments() - num_type_params;
+      } else {
+        first_type_param_index = num_args - num_type_params;
+      }
     }
     if (cls.IsSignatureClass()) {
       // We may be reporting an error about a malformed function type. In that
@@ -9192,8 +9198,10 @@
       if (cls.IsCanonicalSignatureClass()) {
         const Function& signature_function = Function::Handle(
             cls.signature_function());
-        // Signature classes have no super type.
-        ASSERT(first_type_param_index == 0);
+        // Signature classes have no super type, however, they take as many
+        // type arguments as the owner class of their signature function (if it
+        // is non static and generic, see Class::NumTypeArguments()). Therefore,
+        // first_type_param_index may be greater than 0 here.
         return signature_function.InstantiatedSignatureFrom(args,
                                                             name_visibility);
       }
@@ -9548,8 +9556,9 @@
       AbstractTypeArguments::Handle(arguments());
   type_arguments = type_arguments.InstantiateFrom(instantiator_type_arguments,
                                                   malformed_error);
+  // Note that the type class has to be resolved at this time, but not
+  // necessarily finalized yet. We may be checking bounds at compile time.
   const Class& cls = Class::Handle(type_class());
-  ASSERT(cls.is_finalized());
   // This uninstantiated type is not modified, as it can be instantiated
   // with different instantiators.
   Type& instantiated_type = Type::Handle(
@@ -9734,7 +9743,6 @@
 
 
 bool TypeParameter::Equals(const Instance& other) const {
-  ASSERT(IsFinalized());
   if (raw() == other.raw()) {
     return true;
   }
@@ -9746,6 +9754,9 @@
   if (parameterized_class() != other_type_param.parameterized_class()) {
     return false;
   }
+  if (IsFinalized() != other_type_param.IsFinalized()) {
+    return false;
+  }
   if (index() != other_type_param.index()) {
     return false;
   }
diff --git a/tests/language/f_bounded_quantification2_test.dart b/tests/language/f_bounded_quantification2_test.dart
new file mode 100644
index 0000000..64db786
--- /dev/null
+++ b/tests/language/f_bounded_quantification2_test.dart
@@ -0,0 +1,20 @@
+// Copyright (c) 2013, 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.
+
+// Test for F-Bounded Quantification. Regression test for issue 9291.
+
+class Entities<T extends ConceptEntity<T>> implements EntitiesApi<T> { }
+
+class ConceptEntity<T extends ConceptEntity<T>> implements EntityApi { }
+
+abstract class EntityApi<T extends EntityApi<T>> { }
+
+abstract class EntitiesApi<T extends EntityApi<T>> { }
+
+class Concept extends ConceptEntity<Concept> { }
+
+main() {
+  new ConceptEntity<Concept>();
+  new ConceptEntity<ConceptEntity>();
+}
diff --git a/tools/VERSION b/tools/VERSION
index beb3099..5c9f716 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -1,4 +1,4 @@
 MAJOR 0
 MINOR 4
 BUILD 3
-PATCH 4
+PATCH 5
diff --git a/utils/pub/command_lish.dart b/utils/pub/command_lish.dart
index 84afdc0..2de62a8 100644
--- a/utils/pub/command_lish.dart
+++ b/utils/pub/command_lish.dart
@@ -147,7 +147,7 @@
 
       return listDir(rootDir, recursive: true).then((entries) {
         return entries
-            .where(fileExists) // Skip directories.
+            .where(fileExists) // Skip directories and broken symlinks.
             .map((entry) => path.relative(entry, from: rootDir));
       });
     }).then((files) => files.where(_shouldPublish).toList());
diff --git a/utils/pub/entrypoint.dart b/utils/pub/entrypoint.dart
index f689416..2b02f02 100644
--- a/utils/pub/entrypoint.dart
+++ b/utils/pub/entrypoint.dart
@@ -74,11 +74,11 @@
     var future = defer(() {
       ensureDir(path.dirname(packageDir));
 
-      if (dirExists(packageDir)) {
+      if (entryExists(packageDir)) {
         // TODO(nweiz): figure out when to actually delete the directory, and
         // when we can just re-use the existing symlink.
         log.fine("Deleting package directory for ${id.name} before install.");
-        deleteDir(packageDir);
+        deleteEntry(packageDir);
       }
 
       if (id.source.shouldCache) {
@@ -210,7 +210,7 @@
   /// exists. If it doesn't, this completes to an empty [LockFile].
   LockFile loadLockFile() {
     var lockFilePath = path.join(root.dir, 'pubspec.lock');
-    if (!fileExists(lockFilePath)) return new LockFile.empty();
+    if (!entryExists(lockFilePath)) return new LockFile.empty();
     return new LockFile.load(lockFilePath, cache.sources);
   }
 
@@ -298,17 +298,7 @@
   Future _linkSecondaryPackageDir(String dir) {
     return defer(() {
       var symlink = path.join(dir, 'packages');
-      // The order of if tests is significant here. fileExists() will return
-      // true for a symlink (broken or not) but deleteFile() cannot be used
-      // to delete a broken symlink on Windows. So we test for the directory
-      // first since deleteDir() does work on symlinks.
-      // TODO(rnystrom): Make deleteFile() work for symlinks on Windows so this
-      // doesn't matter.
-      if (dirExists(symlink)) {
-        deleteDir(symlink);
-      } else if (fileExists(symlink)) {
-        deleteFile(symlink);
-      }
+      if (entryExists(symlink)) deleteEntry(symlink);
       return createSymlink(packagesDir, symlink, relative: true);
     });
   }
diff --git a/utils/pub/io.dart b/utils/pub/io.dart
index f4bb6de..59172c8 100644
--- a/utils/pub/io.dart
+++ b/utils/pub/io.dart
@@ -28,12 +28,16 @@
 }
 
 /// Determines if a file or directory exists at [path].
-bool entryExists(String path) => dirExists(path) || fileExists(path);
+bool entryExists(String path) =>
+  dirExists(path) || fileExists(path) || linkExists(path);
 
-/// Determines if [file] exists on the file system. Will also return `true` if
-/// [file] points to a symlink, even a directory symlink.
-bool fileExists(String file) =>
-    new File(file).existsSync() || new Link(file).existsSync();
+/// Returns whether [link] exists on the file system. This will return `true`
+/// for any symlink, regardless of what it points at or whether it's broken.
+bool linkExists(String path) => new Link(path).existsSync();
+
+/// Returns whether [file] exists on the file system. This will return `true`
+/// for a symlink only if that symlink is unbroken and points to a file.
+bool fileExists(String file) => new File(file).existsSync();
 
 /// Reads the contents of the text file [file].
 String readTextFile(String file) =>
@@ -61,11 +65,6 @@
   return file;
 }
 
-/// Deletes [file].
-void deleteFile(String file) {
-  new File(file).deleteSync();
-}
-
 /// Creates [file] and writes [contents] to it.
 String writeBinaryFile(String file, List<int> contents) {
   log.io("Writing ${contents.length} bytes to binary file $file.");
@@ -126,12 +125,6 @@
   return tempDir.path;
 }
 
-/// Recursively deletes [dir].
-void deleteDir(String dir) {
-  log.io("Deleting directory $dir.");
-  new Directory(dir).deleteSync(recursive: true);
-}
-
 /// Asynchronously lists the contents of [dir]. If [recursive] is `true`, lists
 /// subdirectory contents (defaults to `false`). If [includeHiddenFiles] is
 /// `true`, includes files and directories beginning with `.` (defaults to
@@ -199,21 +192,34 @@
   return doList(dir, new Set<String>());
 }
 
-/// Determines if [dir] exists on the file system.
+/// Returns whether [dir] exists on the file system. This will return `true` for
+/// a symlink only if that symlink is unbroken and points to a directory.
 bool dirExists(String dir) => new Directory(dir).existsSync();
 
+/// Deletes whatever's at [path], whether it's a file, directory, or symlink. If
+/// it's a directory, it will be deleted recursively.
+void deleteEntry(String path) {
+  if (linkExists(path)) {
+    log.io("Deleting link $path.");
+    if (Platform.operatingSystem == 'windows') {
+      // TODO(nweiz): remove this when issue 9278 is fixed.
+      new Directory(path).deleteSync();
+    } else {
+      new Link(path).deleteSync();
+    }
+  } else if (dirExists(path)) {
+    log.io("Deleting directory $path.");
+    new Directory(path).deleteSync(recursive: true);
+  } else {
+    log.io("Deleting file $path.");
+    new File(path).deleteSync();
+  }
+}
+
 /// "Cleans" [dir]. If that directory already exists, it will be deleted. Then a
 /// new empty directory will be created.
 void cleanDir(String dir) {
-  if (dirExists(dir)) {
-    // Delete it first.
-    deleteDir(dir);
-  } else if (fileExists(dir)) {
-    // If there is a non-directory there (file or symlink), delete it.
-    deleteFile(dir);
-  }
-
-  // Just create it.
+  if (entryExists(dir)) deleteEntry(dir);
   createDir(dir);
 }
 
@@ -582,7 +588,7 @@
   return defer(() {
     var tempDir = createTempDir();
     return new Future.of(() => fn(tempDir))
-        .whenComplete(() => deleteDir(tempDir));
+        .whenComplete(() => deleteEntry(tempDir));
   });
 }
 
diff --git a/utils/pub/oauth2.dart b/utils/pub/oauth2.dart
index 55245f3..69364d3 100644
--- a/utils/pub/oauth2.dart
+++ b/utils/pub/oauth2.dart
@@ -66,9 +66,7 @@
 void clearCredentials(SystemCache cache) {
   _credentials = null;
   var credentialsFile = _credentialsFile(cache);
-  if (!fileExists(credentialsFile)) return;
-
-  deleteFile(credentialsFile);
+  if (entryExists(credentialsFile)) deleteEntry(credentialsFile);
 }
 
 /// Asynchronously passes an OAuth2 [Client] to [fn], and closes the client when
diff --git a/utils/pub/path_source.dart b/utils/pub/path_source.dart
index 6cdc070..8b3b7a5 100644
--- a/utils/pub/path_source.dart
+++ b/utils/pub/path_source.dart
@@ -108,8 +108,6 @@
 
     if (dirExists(dir)) return;
 
-    // Check this after dirExists() so that symlinks to directories don't get
-    // confused as files.
     if (fileExists(dir)) {
       throw new FormatException(
           "Path dependency for package '$name' must refer to a "
diff --git a/utils/pub/pub.dart b/utils/pub/pub.dart
index 1c05c45..c2fb8f9 100644
--- a/utils/pub/pub.dart
+++ b/utils/pub/pub.dart
@@ -258,6 +258,8 @@
       if (globalOptions['trace'] && trace != null) {
         log.error(trace);
         log.dumpTranscript();
+      } else {
+        log.fine(trace);
       }
 
       exit(_chooseExitCode(error));
diff --git a/utils/pub/source.dart b/utils/pub/source.dart
index 60d455b..1180967 100644
--- a/utils/pub/source.dart
+++ b/utils/pub/source.dart
@@ -119,7 +119,7 @@
         if (!isCorrupted) return true;
 
         // Busted, so wipe out the package and reinstall.
-        deleteDir(packageDir);
+        deleteEntry(packageDir);
         return false;
       });
     }).then((isInstalled) {
diff --git a/utils/pub/system_cache.dart b/utils/pub/system_cache.dart
index b6467b5..1b30ea8 100644
--- a/utils/pub/system_cache.dart
+++ b/utils/pub/system_cache.dart
@@ -111,7 +111,6 @@
   /// Deletes the system cache's internal temp directory.
   void deleteTempDir() {
     log.fine('Clean up system cache temp directory $tempDir.');
-    if (!dirExists(tempDir)) return;
-    deleteDir(tempDir);
+    if (dirExists(tempDir)) deleteEntry(tempDir);
   }
 }
diff --git a/utils/tests/pub/install/git/lock_version_test.dart b/utils/tests/pub/install/git/lock_version_test.dart
index 7c3824b..ad6c92a 100644
--- a/utils/tests/pub/install/git/lock_version_test.dart
+++ b/utils/tests/pub/install/git/lock_version_test.dart
@@ -35,7 +35,7 @@
     ]).validate();
 
     // Delete the packages path to simulate a new checkout of the application.
-    schedule(() => deleteDir(path.join(sandboxDir, packagesPath)));
+    schedule(() => deleteEntry(path.join(sandboxDir, packagesPath)));
 
     d.git('foo.git', [
       d.libDir('foo', 'foo 2'),
diff --git a/utils/tests/pub/install/hosted/stay_locked_test.dart b/utils/tests/pub/install/hosted/stay_locked_test.dart
index dc8f72a..df7cf87 100644
--- a/utils/tests/pub/install/hosted/stay_locked_test.dart
+++ b/utils/tests/pub/install/hosted/stay_locked_test.dart
@@ -27,7 +27,7 @@
     d.packagesDir({"foo": "1.0.0"}).validate();
 
     // Delete the packages path to simulate a new checkout of the application.
-    schedule(() => deleteDir(path.join(sandboxDir, packagesPath)));
+    schedule(() => deleteEntry(path.join(sandboxDir, packagesPath)));
 
     // Start serving a newer package as well.
     servePackages([packageMap("foo", "1.0.1")]);
diff --git a/utils/tests/pub/install/pub_install_test.dart b/utils/tests/pub/install/pub_install_test.dart
index bdaa25f..f927b54 100644
--- a/utils/tests/pub/install/pub_install_test.dart
+++ b/utils/tests/pub/install/pub_install_test.dart
@@ -6,8 +6,10 @@
 
 import 'dart:io';
 
+import '../../../../pkg/pathos/lib/path.dart' as path;
 import '../../../../pkg/scheduled_test/lib/scheduled_test.dart';
 
+import '../../../pub/io.dart';
 import '../descriptor.dart' as d;
 import '../test_pub.dart';
 
@@ -114,6 +116,29 @@
     ]).validate();
   });
 
+  integration('overwrites a broken packages directory symlink', () {
+    d.dir(appPath, [
+      d.appPubspec([]),
+      d.dir('packages'),
+      d.libDir('myapp'),
+      d.dir('bin')
+    ]).create();
+
+    scheduleSymlink(
+        path.join(appPath, 'packages'),
+        path.join(appPath, 'bin', 'packages'));
+
+    schedule(() => deleteEntry(path.join(sandboxDir, appPath, 'packages')));
+
+    schedulePub(args: ['install'],
+        output: new RegExp(r"Dependencies installed!$"));
+
+    d.dir(packagesPath, [
+      d.nothing('foo'),
+      d.dir('myapp', [d.file('myapp.dart', 'main() => "myapp";')])
+    ]).validate();
+  });
+
   group('creates a packages directory in', () {
     integration('"test/" and its subdirectories', () {
       d.dir(appPath, [
diff --git a/utils/tests/pub/io_test.dart b/utils/tests/pub/io_test.dart
index 526283d..2ebf55c 100644
--- a/utils/tests/pub/io_test.dart
+++ b/utils/tests/pub/io_test.dart
@@ -4,6 +4,8 @@
 
 library io_test;
 
+import 'dart:io';
+
 import '../../../pkg/pathos/lib/path.dart' as path;
 import '../../../pkg/unittest/lib/unittest.dart';
 
@@ -134,4 +136,156 @@
       }), completes);
     });
   });
+
+  testExistencePredicate("entryExists", entryExists,
+      forFile: true,
+      forFileSymlink: true,
+      forMultiLevelFileSymlink: true,
+      forDirectory: true,
+      forDirectorySymlink: true,
+      forMultiLevelDirectorySymlink: true,
+      forBrokenSymlink: true,
+      forMultiLevelBrokenSymlink: true);
+
+  testExistencePredicate("linkExists", linkExists,
+      forFile: false,
+      forFileSymlink: true,
+      forMultiLevelFileSymlink: true,
+      forDirectory: false,
+      forDirectorySymlink: true,
+      forMultiLevelDirectorySymlink: true,
+      forBrokenSymlink: true,
+      forMultiLevelBrokenSymlink: true);
+
+  testExistencePredicate("fileExists", fileExists,
+      forFile: true,
+      forFileSymlink: true,
+      forMultiLevelFileSymlink: true,
+      forDirectory: false,
+      forDirectorySymlink: false,
+      forMultiLevelDirectorySymlink: false,
+      forBrokenSymlink: false,
+      forMultiLevelBrokenSymlink: false);
+
+  testExistencePredicate("dirExists", dirExists,
+      forFile: false,
+      forFileSymlink: false,
+      forMultiLevelFileSymlink: false,
+      forDirectory: true,
+      forDirectorySymlink: true,
+      forMultiLevelDirectorySymlink: true,
+      forBrokenSymlink: false,
+      forMultiLevelBrokenSymlink: false);
+}
+
+void testExistencePredicate(String name, bool predicate(String path),
+    {bool forFile,
+     bool forFileSymlink,
+     bool forMultiLevelFileSymlink,
+     bool forDirectory,
+     bool forDirectorySymlink,
+     bool forMultiLevelDirectorySymlink,
+     bool forBrokenSymlink,
+     bool forMultiLevelBrokenSymlink}) {
+  group(name, () {
+    test('returns $forFile for a file', () {
+      expect(withTempDir((temp) {
+        var path = path.join(temp, "test.txt");
+        writeTextFile(path, "contents");
+        expect(predicate(path), equals(forFile));
+      }), completes);
+    });
+
+    test('returns $forDirectory for a directory', () {
+      expect(withTempDir((temp) {
+        var path = path.join(temp, "dir");
+        createDir(path);
+        expect(predicate(path), equals(forDirectory));
+      }), completes);
+    });
+
+    test('returns $forDirectorySymlink for a symlink to a directory', () {
+      expect(withTempDir((temp) {
+        var targetPath = path.join(temp, "dir");
+        var symlinkPath = path.join(temp, "linkdir");
+        createDir(targetPath);
+        return createSymlink(targetPath, symlinkPath).then((_) {
+          expect(predicate(symlinkPath), equals(forDirectorySymlink));
+        });
+      }), completes);
+    });
+
+    test('returns $forMultiLevelDirectorySymlink for a multi-level symlink to '
+        'a directory', () {
+      expect(withTempDir((temp) {
+        var targetPath = path.join(temp, "dir");
+        var symlink1Path = path.join(temp, "link1dir");
+        var symlink2Path = path.join(temp, "link2dir");
+        createDir(targetPath);
+        return createSymlink(targetPath, symlink1Path)
+            .then((_) => createSymlink(symlink1Path, symlink2Path))
+            .then((_) {
+          expect(predicate(symlink2Path),
+              equals(forMultiLevelDirectorySymlink));
+        });
+      }), completes);
+    });
+
+    test('returns $forBrokenSymlink for a broken symlink', () {
+      expect(withTempDir((temp) {
+        var targetPath = path.join(temp, "dir");
+        var symlinkPath = path.join(temp, "linkdir");
+        createDir(targetPath);
+        return createSymlink(targetPath, symlinkPath).then((_) {
+          deleteEntry(targetPath);
+          expect(predicate(symlinkPath), equals(forBrokenSymlink));
+        });
+      }), completes);
+    });
+
+    test('returns $forMultiLevelBrokenSymlink for a multi-level broken symlink',
+        () {
+      expect(withTempDir((temp) {
+        var targetPath = path.join(temp, "dir");
+        var symlink1Path = path.join(temp, "link1dir");
+        var symlink2Path = path.join(temp, "link2dir");
+        createDir(targetPath);
+        return createSymlink(targetPath, symlink1Path)
+            .then((_) => createSymlink(symlink1Path, symlink2Path))
+            .then((_) {
+          deleteEntry(targetPath);
+          expect(predicate(symlink2Path), equals(forMultiLevelBrokenSymlink));
+        });
+      }), completes);
+    });
+
+    // Windows doesn't support symlinking to files.
+    if (Platform.operatingSystem != 'windows') {
+      test('returns $forFileSymlink for a symlink to a file', () {
+        expect(withTempDir((temp) {
+          var targetPath = path.join(temp, "test.txt");
+          var symlinkPath = path.join(temp, "link.txt");
+          writeTextFile(targetPath, "contents");
+          return createSymlink(targetPath, symlinkPath).then((_) {
+            expect(predicate(symlinkPath), equals(forFileSymlink));
+          });
+        }), completes);
+      });
+
+      test('returns $forMultiLevelFileSymlink for a multi-level symlink to a '
+          'file', () {
+        expect(withTempDir((temp) {
+          var targetPath = path.join(temp, "test.txt");
+          var symlink1Path = path.join(temp, "link1.txt");
+          var symlink2Path = path.join(temp, "link2.txt");
+          writeTextFile(targetPath, "contents");
+          return createSymlink(targetPath, symlink1Path)
+              .then((_) => createSymlink(symlink1Path, symlink2Path))
+              .then((_) {
+            expect(predicate(symlink2Path), equals(forMultiLevelFileSymlink));
+          });
+        }), completes);
+      });
+    }
+  });
 }
diff --git a/utils/tests/pub/test_pub.dart b/utils/tests/pub/test_pub.dart
index 2073f42..bf3722b 100644
--- a/utils/tests/pub/test_pub.dart
+++ b/utils/tests/pub/test_pub.dart
@@ -235,7 +235,7 @@
 
     _sandboxDir = createTempDir();
     d.defaultRoot = sandboxDir;
-    currentSchedule.onComplete.schedule(() => deleteDir(_sandboxDir),
+    currentSchedule.onComplete.schedule(() => deleteEntry(_sandboxDir),
         'deleting the sandbox directory');
 
     // Schedule the test.
diff --git a/utils/tests/pub/validator_test.dart b/utils/tests/pub/validator_test.dart
index 12dbd82..9b3b583 100644
--- a/utils/tests/pub/validator_test.dart
+++ b/utils/tests/pub/validator_test.dart
@@ -121,19 +121,19 @@
     });
 
     integration('has a COPYING file', () {
-      schedule(() => deleteFile(path.join(sandboxDir, appPath, 'LICENSE')));
+      schedule(() => deleteEntry(path.join(sandboxDir, appPath, 'LICENSE')));
       d.file(path.join(appPath, 'COPYING'), '').create();
       expectNoValidationError(license);
     });
 
     integration('has a prefixed LICENSE file', () {
-      schedule(() => deleteFile(path.join(sandboxDir, appPath, 'LICENSE')));
+      schedule(() => deleteEntry(path.join(sandboxDir, appPath, 'LICENSE')));
       d.file(path.join(appPath, 'MIT_LICENSE'), '').create();
       expectNoValidationError(license);
     });
 
     integration('has a suffixed LICENSE file', () {
-      schedule(() => deleteFile(path.join(sandboxDir, appPath, 'LICENSE')));
+      schedule(() => deleteEntry(path.join(sandboxDir, appPath, 'LICENSE')));
       d.file(path.join(appPath, 'LICENSE.md'), '').create();
       expectNoValidationError(license);
     });
@@ -269,7 +269,7 @@
     });
 
     integration('has no LICENSE file', () {
-      schedule(() => deleteFile(path.join(sandboxDir, appPath, 'LICENSE')));
+      schedule(() => deleteEntry(path.join(sandboxDir, appPath, 'LICENSE')));
       expectValidationError(license);
     });
 
@@ -332,7 +332,7 @@
 
     integration('has a single library named differently than the package', () {
       schedule(() =>
-          deleteFile(path.join(sandboxDir, appPath, "lib", "test_pkg.dart")));
+          deleteEntry(path.join(sandboxDir, appPath, "lib", "test_pkg.dart")));
       d.dir(appPath, [
         d.dir("lib", [d.file("best_pkg.dart", "int i = 0;")])
       ]).create();
@@ -340,19 +340,19 @@
     });
 
     integration('has no lib directory', () {
-      schedule(() => deleteDir(path.join(sandboxDir, appPath, "lib")));
+      schedule(() => deleteEntry(path.join(sandboxDir, appPath, "lib")));
       expectValidationError(lib);
     });
 
     integration('has an empty lib directory', () {
       schedule(() =>
-          deleteFile(path.join(sandboxDir, appPath, "lib", "test_pkg.dart")));
+          deleteEntry(path.join(sandboxDir, appPath, "lib", "test_pkg.dart")));
       expectValidationError(lib);
     });
 
     integration('has a lib directory containing only src', () {
       schedule(() =>
-          deleteFile(path.join(sandboxDir, appPath, "lib", "test_pkg.dart")));
+          deleteEntry(path.join(sandboxDir, appPath, "lib", "test_pkg.dart")));
       d.dir(appPath, [
         d.dir("lib", [
           d.dir("src", [d.file("test_pkg.dart", "int i = 0;")])