Add gradle wrapper to project template (#10928)

diff --git a/bin/internal/gradle_wrapper.version b/bin/internal/gradle_wrapper.version
new file mode 100644
index 0000000..97e3b0d
--- /dev/null
+++ b/bin/internal/gradle_wrapper.version
@@ -0,0 +1 @@
+0b5c1398d1d04ac245a310de98825cb7b3278e2a
diff --git a/packages/flutter_tools/lib/src/base/file_system.dart b/packages/flutter_tools/lib/src/base/file_system.dart
index 6d5f3bc..6baeecf 100644
--- a/packages/flutter_tools/lib/src/base/file_system.dart
+++ b/packages/flutter_tools/lib/src/base/file_system.dart
@@ -71,10 +71,11 @@
   }
 }
 
-/// Recursively copies `srcDir` to `destDir`.
+/// Recursively copies `srcDir` to `destDir`, invoking [onFileCopied] if
+/// specified for each source/destination file pair.
 ///
 /// Creates `destDir` if needed.
-void copyDirectorySync(Directory srcDir, Directory destDir) {
+void copyDirectorySync(Directory srcDir, Directory destDir, [void onFileCopied(File srcFile, File destFile)]) {
   if (!srcDir.existsSync())
     throw new Exception('Source directory "${srcDir.path}" does not exist, nothing to copy');
 
@@ -86,6 +87,7 @@
     if (entity is File) {
       final File newFile = destDir.fileSystem.file(newPath);
       newFile.writeAsBytesSync(entity.readAsBytesSync());
+      onFileCopied?.call(entity, newFile);
     } else if (entity is Directory) {
       copyDirectorySync(
         entity, destDir.fileSystem.directory(newPath));
diff --git a/packages/flutter_tools/lib/src/base/os.dart b/packages/flutter_tools/lib/src/base/os.dart
index 1cb5696..8dfcbb4 100644
--- a/packages/flutter_tools/lib/src/base/os.dart
+++ b/packages/flutter_tools/lib/src/base/os.dart
@@ -47,6 +47,8 @@
 
   void unzip(File file, Directory targetDirectory);
 
+  void unpack(File gzippedTarFile, Directory targetDirectory);
+
   /// Returns a pretty name string for the current operating system.
   ///
   /// If available, the detailed version of the OS is included.
@@ -97,6 +99,12 @@
     runSync(<String>['unzip', '-o', '-q', file.path, '-d', targetDirectory.path]);
   }
 
+  // tar -xzf tarball -C dest
+  @override
+  void unpack(File gzippedTarFile, Directory targetDirectory) {
+    runSync(<String>['tar', '-xzf', gzippedTarFile.path, '-C', targetDirectory.path]);
+  }
+
   @override
   File makePipe(String path) {
     runSync(<String>['mkfifo', path]);
@@ -167,7 +175,18 @@
   @override
   void unzip(File file, Directory targetDirectory) {
     final Archive archive = new ZipDecoder().decodeBytes(file.readAsBytesSync());
+    _unpackArchive(archive, targetDirectory);
+  }
 
+  @override
+  void unpack(File gzippedTarFile, Directory targetDirectory) {
+    final Archive archive = new TarDecoder().decodeBytes(
+      new GZipDecoder().decodeBytes(gzippedTarFile.readAsBytesSync()),
+    );
+    _unpackArchive(archive, targetDirectory);
+  }
+
+  void _unpackArchive(Archive archive, Directory targetDirectory) {
     for (ArchiveFile archiveFile in archive.files) {
       // The archive package doesn't correctly set isFile.
       if (!archiveFile.isFile || archiveFile.name.endsWith('/'))
diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart
index 5d10d2a..007e7a5 100644
--- a/packages/flutter_tools/lib/src/cache.dart
+++ b/packages/flutter_tools/lib/src/cache.dart
@@ -17,9 +17,19 @@
 /// A wrapper around the `bin/cache/` directory.
 class Cache {
   /// [rootOverride] is configurable for testing.
-  Cache({ Directory rootOverride }) : _rootOverride = rootOverride;
+  /// [artifacts] is configurable for testing.
+  Cache({ Directory rootOverride, List<CachedArtifact> artifacts }) : _rootOverride = rootOverride {
+    if (artifacts == null) {
+      _artifacts.add(new MaterialFonts(this));
+      _artifacts.add(new FlutterEngine(this));
+      _artifacts.add(new GradleWrapper(this));
+    } else {
+      _artifacts.addAll(artifacts);
+    }
+  }
 
   final Directory _rootOverride;
+  final List<CachedArtifact> _artifacts = <CachedArtifact>[];
 
   // Initialized by FlutterCommandRunner on startup.
   static String flutterRoot;
@@ -155,16 +165,9 @@
     return fs.file(fs.path.join(getRoot().path, '$artifactName.stamp'));
   }
 
-  bool isUpToDate() {
-    final MaterialFonts materialFonts = new MaterialFonts(cache);
-    final FlutterEngine engine = new FlutterEngine(cache);
+  bool isUpToDate() => _artifacts.every((CachedArtifact artifact) => artifact.isUpToDate());
 
-    return materialFonts.isUpToDate() && engine.isUpToDate();
-  }
-
-  Future<String> getThirdPartyFile(String urlStr, String serviceName, {
-    bool unzip: false
-  }) async {
+  Future<String> getThirdPartyFile(String urlStr, String serviceName) async {
     final Uri url = Uri.parse(urlStr);
     final Directory thirdPartyDir = getArtifactDirectory('third_party');
 
@@ -175,7 +178,7 @@
     final File cachedFile = fs.file(fs.path.join(serviceDir.path, url.pathSegments.last));
     if (!cachedFile.existsSync()) {
       try {
-        await _downloadFileToCache(url, cachedFile, unzip);
+        await _downloadFile(url, cachedFile);
       } catch (e) {
         printError('Failed to fetch third-party artifact $url: $e');
         rethrow;
@@ -188,77 +191,65 @@
   Future<Null> updateAll() async {
     if (!_lockEnabled)
       return null;
-    final MaterialFonts materialFonts = new MaterialFonts(cache);
-    if (!materialFonts.isUpToDate())
-      await materialFonts.download();
-
-    final FlutterEngine engine = new FlutterEngine(cache);
-    if (!engine.isUpToDate())
-      await engine.download();
-  }
-
-  /// Download a file from the given url and write it to the cache.
-  /// If [unzip] is true, treat the url as a zip file, and unzip it to the
-  /// directory given.
-  static Future<Null> _downloadFileToCache(Uri url, FileSystemEntity location, bool unzip) async {
-    if (!location.parent.existsSync())
-      location.parent.createSync(recursive: true);
-
-    final List<int> fileBytes = await fetchUrl(url);
-    if (unzip) {
-      if (location is Directory && !location.existsSync())
-        location.createSync(recursive: true);
-
-      final File tempFile = fs.file(fs.path.join(fs.systemTempDirectory.path, '${url.toString().hashCode}.zip'));
-      tempFile.writeAsBytesSync(fileBytes, flush: true);
-      os.unzip(tempFile, location);
-      tempFile.deleteSync();
-    } else {
-      final File file = location;
-      file.writeAsBytesSync(fileBytes, flush: true);
+    for (CachedArtifact artifact in _artifacts) {
+      if (!artifact.isUpToDate())
+        await artifact.update();
     }
   }
 }
 
-class MaterialFonts {
-  MaterialFonts(this.cache);
+/// An artifact managed by the cache.
+abstract class CachedArtifact {
+  CachedArtifact(this.name, this.cache);
 
-  static const String kName = 'material_fonts';
-
+  final String name;
   final Cache cache;
 
+  Directory get location => cache.getArtifactDirectory(name);
+  String get version => cache.getVersionFor(name);
+
   bool isUpToDate() {
-    if (!cache.getArtifactDirectory(kName).existsSync())
+    if (!location.existsSync())
       return false;
-    return cache.getVersionFor(kName) == cache.getStampFor(kName);
+    if (version != cache.getStampFor(name))
+      return false;
+    return isUpToDateInner();
   }
 
-  Future<Null> download() {
+  Future<Null> update() async {
+    if (location.existsSync())
+      location.deleteSync(recursive: true);
+    location.createSync(recursive: true);
+    return updateInner().then<Null>((_) {
+      cache.setStampFor(name, version);
+    });
+  }
+
+  /// Hook method for extra checks for being up-to-date.
+  bool isUpToDateInner() => true;
+
+  /// Template method to perform artifact update.
+  Future<Null> updateInner();
+}
+
+/// A cached artifact containing fonts used for Material Design.
+class MaterialFonts extends CachedArtifact {
+  MaterialFonts(Cache cache): super('material_fonts', cache);
+
+  @override
+  Future<Null> updateInner() {
     final Status status = logger.startProgress('Downloading Material fonts...', expectSlowOperation: true);
-
-    final Directory fontsDir = cache.getArtifactDirectory(kName);
-    if (fontsDir.existsSync())
-      fontsDir.deleteSync(recursive: true);
-
-    return Cache._downloadFileToCache(
-      Uri.parse(cache.getVersionFor(kName)), fontsDir, true
-    ).then<Null>((Null value) {
-      cache.setStampFor(kName, cache.getVersionFor(kName));
+    return _downloadZipArchive(Uri.parse(version), location).then<Null>((_) {
       status.stop();
     }).whenComplete(status.cancel);
   }
 }
 
-class FlutterEngine {
+/// A cached artifact containing the Flutter engine binaries.
+class FlutterEngine extends CachedArtifact {
+  FlutterEngine(Cache cache): super('engine', cache);
 
-  FlutterEngine(this.cache);
-
-  static const String kName = 'engine';
-  static const String kSkyEngine = 'sky_engine';
-
-  final Cache cache;
-
-  List<String> _getPackageDirs() => const <String>[kSkyEngine];
+  List<String> _getPackageDirs() => const <String>['sky_engine'];
 
   // Return a list of (cache directory path, download URL path) tuples.
   List<List<String>> _getBinaryDirs() {
@@ -320,7 +311,8 @@
     <String>['ios-release', 'ios-release/artifacts.zip'],
   ];
 
-  bool isUpToDate() {
+  @override
+  bool isUpToDateInner() {
     final Directory pkgDir = cache.getCacheDir('pkg');
     for (String pkgName in _getPackageDirs()) {
       final String pkgPath = fs.path.join(pkgDir.path, pkgName);
@@ -328,19 +320,17 @@
         return false;
     }
 
-    final Directory engineDir = cache.getArtifactDirectory(kName);
     for (List<String> toolsDir in _getBinaryDirs()) {
-      final Directory dir = fs.directory(fs.path.join(engineDir.path, toolsDir[0]));
+      final Directory dir = fs.directory(fs.path.join(location.path, toolsDir[0]));
       if (!dir.existsSync())
         return false;
     }
-
-    return cache.getVersionFor(kName) == cache.getStampFor(kName);
+    return true;
   }
 
-  Future<Null> download() async {
-    final String engineVersion = cache.getVersionFor(kName);
-    final String url = 'https://storage.googleapis.com/flutter_infra/flutter/$engineVersion/';
+  @override
+  Future<Null> updateInner() async {
+    final String url = 'https://storage.googleapis.com/flutter_infra/flutter/$version/';
 
     final Directory pkgDir = cache.getCacheDir('pkg');
     for (String pkgName in _getPackageDirs()) {
@@ -351,14 +341,10 @@
       await _downloadItem('Downloading package $pkgName...', url + pkgName + '.zip', pkgDir);
     }
 
-    final Directory engineDir = cache.getArtifactDirectory(kName);
-    if (engineDir.existsSync())
-      engineDir.deleteSync(recursive: true);
-
     for (List<String> toolsDir in _getBinaryDirs()) {
       final String cacheDir = toolsDir[0];
       final String urlPath = toolsDir[1];
-      final Directory dir = fs.directory(fs.path.join(engineDir.path, cacheDir));
+      final Directory dir = fs.directory(fs.path.join(location.path, cacheDir));
       await _downloadItem('Downloading $cacheDir tools...', url + urlPath, dir);
 
       _makeFilesExecutable(dir);
@@ -370,8 +356,6 @@
         os.unzip(frameworkZip, framework);
       }
     }
-
-    cache.setStampFor(kName, cache.getVersionFor(kName));
   }
 
   void _makeFilesExecutable(Directory dir) {
@@ -386,8 +370,68 @@
 
   Future<Null> _downloadItem(String message, String url, Directory dest) {
     final Status status = logger.startProgress(message, expectSlowOperation: true);
-    return Cache._downloadFileToCache(Uri.parse(url), dest, true).then<Null>((Null value) {
+    return _downloadZipArchive(Uri.parse(url), dest).then<Null>((_) {
       status.stop();
     }).whenComplete(status.cancel);
   }
 }
+
+/// A cached artifact containing Gradle Wrapper scripts and binaries.
+class GradleWrapper extends CachedArtifact {
+  GradleWrapper(Cache cache): super('gradle_wrapper', cache);
+
+  @override
+  Future<Null> updateInner() async {
+    final Status status = logger.startProgress('Downloading Gradle Wrapper...', expectSlowOperation: true);
+
+    final String url = 'https://android.googlesource.com'
+        '/platform/tools/base/+archive/$version/templates/gradle/wrapper.tgz';
+    await _downloadZippedTarball(Uri.parse(url), location).then<Null>((_) {
+      // Delete property file, allowing templates to provide it.
+      fs.file(fs.path.join(location.path, 'gradle', 'wrapper', 'gradle-wrapper.properties')).deleteSync();
+      status.stop();
+    }).whenComplete(status.cancel);
+  }
+}
+
+/// Download a file from the given [url] and write it to [location].
+Future<Null> _downloadFile(Uri url, File location) async {
+  _ensureExists(location.parent);
+  final List<int> fileBytes = await fetchUrl(url);
+  location.writeAsBytesSync(fileBytes, flush: true);
+}
+
+/// Download a zip archive from the given [url] and unzip it to [location].
+Future<Null> _downloadZipArchive(Uri url, Directory location) {
+  return _withTemporaryFile('download.zip', (File tempFile) async {
+    await _downloadFile(url, tempFile);
+    _ensureExists(location);
+    os.unzip(tempFile, location);
+  });
+}
+
+/// Download a gzipped tarball from the given [url] and unpack it to [location].
+Future<Null> _downloadZippedTarball(Uri url, Directory location) {
+  return _withTemporaryFile('download.tgz', (File tempFile) async {
+    await _downloadFile(url, tempFile);
+    _ensureExists(location);
+    os.unpack(tempFile, location);
+  });
+}
+
+/// Create a file with the given name in a new temporary directory, invoke
+/// [onTemporaryFile] with the file as argument, then delete the temporary
+/// directory.
+Future<Null> _withTemporaryFile(String name, Future<Null> onTemporaryFile(File file)) async {
+  final Directory tempDir = fs.systemTempDirectory.createTempSync();
+  final File tempFile = fs.file(fs.path.join(tempDir.path, name));
+  await onTemporaryFile(tempFile).whenComplete(() {
+    tempDir.delete(recursive: true);
+  });
+}
+
+/// Create the given [directory] and parents, as necessary.
+void _ensureExists(Directory directory) {
+  if (!directory.existsSync())
+    directory.createSync(recursive: true);
+}
diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart
index cb60d49..0b6f67c 100644
--- a/packages/flutter_tools/lib/src/commands/create.dart
+++ b/packages/flutter_tools/lib/src/commands/create.dart
@@ -11,6 +11,7 @@
 import '../android/gradle.dart' as gradle;
 import '../base/common.dart';
 import '../base/file_system.dart';
+import '../base/os.dart';
 import '../base/utils.dart';
 import '../build_info.dart';
 import '../cache.dart';
@@ -167,6 +168,10 @@
     }
 
     generatedCount += _renderTemplate('create', appPath, templateContext);
+    generatedCount += _injectGradleWrapper(appPath);
+    if (appPath != dirPath) {
+      generatedCount += _injectGradleWrapper(dirPath);
+    }
     if (argResults['with-driver-test']) {
       final String testPath = fs.path.join(appPath, 'test_driver');
       generatedCount += _renderTemplate('driver', testPath, templateContext);
@@ -272,6 +277,22 @@
     final Template template = new Template.fromName(templateName);
     return template.render(fs.directory(dirPath), context, overwriteExisting: false);
   }
+
+  int _injectGradleWrapper(String projectDir) {
+    int filesCreated = 0;
+    copyDirectorySync(
+      cache.getArtifactDirectory('gradle_wrapper'),
+      fs.directory(fs.path.join(projectDir, 'android')),
+      (File sourceFile, File destinationFile) {
+        filesCreated++;
+        final String modes = sourceFile.statSync().modeString();
+        if (modes != null && modes.contains('x')) {
+          os.makeExecutable(destinationFile);
+        }
+      },
+    );
+    return filesCreated;
+  }
 }
 
 String _createAndroidIdentifier(String organization, String name) {
diff --git a/packages/flutter_tools/lib/src/services.dart b/packages/flutter_tools/lib/src/services.dart
index 00bd107..1aaf0d2 100644
--- a/packages/flutter_tools/lib/src/services.dart
+++ b/packages/flutter_tools/lib/src/services.dart
@@ -69,20 +69,18 @@
 
     if (jars != null && serviceConfig['jars'] is Iterable) {
       for (String jar in serviceConfig['jars'])
-        jars.add(fs.file(await getServiceFromUrl(jar, serviceRoot, service, unzip: false)));
+        jars.add(fs.file(await getServiceFromUrl(jar, serviceRoot, service)));
     }
   }
 }
 
-Future<String> getServiceFromUrl(
-  String url, String rootDir, String serviceName, { bool unzip: false }
-) async {
+Future<String> getServiceFromUrl(String url, String rootDir, String serviceName) async {
   if (url.startsWith("android-sdk:") && androidSdk != null) {
     // It's something shipped in the standard android SDK.
     return url.replaceAll('android-sdk:', '${androidSdk.directory}/');
   } else if (url.startsWith("http")) {
     // It's a regular file to download.
-    return await cache.getThirdPartyFile(url, serviceName, unzip: unzip);
+    return await cache.getThirdPartyFile(url, serviceName);
   } else {
     // Assume url is a path relative to the service's root dir.
     return fs.path.join(rootDir, url);
diff --git a/packages/flutter_tools/templates/create/android-java.tmpl/build.gradle b/packages/flutter_tools/templates/create/android-java.tmpl/build.gradle
index f5004b9..879b8ca 100644
--- a/packages/flutter_tools/templates/create/android-java.tmpl/build.gradle
+++ b/packages/flutter_tools/templates/create/android-java.tmpl/build.gradle
@@ -4,7 +4,7 @@
     }
 
     dependencies {
-        classpath 'com.android.tools.build:gradle:2.2.3'
+        classpath 'com.android.tools.build:gradle:2.3.3'
     }
 }
 
@@ -26,7 +26,3 @@
 task clean(type: Delete) {
     delete rootProject.buildDir
 }
-
-task wrapper(type: Wrapper) {
-    gradleVersion = '2.14.1'
-}
diff --git a/packages/flutter_tools/templates/create/android-kotlin.tmpl/build.gradle b/packages/flutter_tools/templates/create/android-kotlin.tmpl/build.gradle
index b22b7b7..27a170c 100644
--- a/packages/flutter_tools/templates/create/android-kotlin.tmpl/build.gradle
+++ b/packages/flutter_tools/templates/create/android-kotlin.tmpl/build.gradle
@@ -4,7 +4,7 @@
     }
 
     dependencies {
-        classpath 'com.android.tools.build:gradle:2.2.3'
+        classpath 'com.android.tools.build:gradle:2.3.3'
         classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.1.2-4'
     }
 }
@@ -27,7 +27,3 @@
 task clean(type: Delete) {
     delete rootProject.buildDir
 }
-
-task wrapper(type: Wrapper) {
-    gradleVersion = '2.14.1'
-}
diff --git a/packages/flutter_tools/templates/create/android.tmpl/.gitignore b/packages/flutter_tools/templates/create/android.tmpl/.gitignore
index 1fd9325..1658458 100644
--- a/packages/flutter_tools/templates/create/android.tmpl/.gitignore
+++ b/packages/flutter_tools/templates/create/android.tmpl/.gitignore
@@ -7,7 +7,3 @@
 /build
 /captures
 GeneratedPluginRegistrant.java
-
-/gradle
-/gradlew
-/gradlew.bat
diff --git a/packages/flutter_tools/templates/create/android.tmpl/gradle/wrapper/gradle-wrapper.properties b/packages/flutter_tools/templates/create/android.tmpl/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..45e7f14
--- /dev/null
+++ b/packages/flutter_tools/templates/create/android.tmpl/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
diff --git a/packages/flutter_tools/templates/plugin/android-java.tmpl/build.gradle.tmpl b/packages/flutter_tools/templates/plugin/android-java.tmpl/build.gradle.tmpl
index a67e4e9..5ad2e82 100644
--- a/packages/flutter_tools/templates/plugin/android-java.tmpl/build.gradle.tmpl
+++ b/packages/flutter_tools/templates/plugin/android-java.tmpl/build.gradle.tmpl
@@ -7,7 +7,7 @@
     }
 
     dependencies {
-        classpath 'com.android.tools.build:gradle:2.3.0'
+        classpath 'com.android.tools.build:gradle:2.3.3'
     }
 }
 
diff --git a/packages/flutter_tools/templates/plugin/android-kotlin.tmpl/build.gradle.tmpl b/packages/flutter_tools/templates/plugin/android-kotlin.tmpl/build.gradle.tmpl
index 9d3eecd..02514fd 100644
--- a/packages/flutter_tools/templates/plugin/android-kotlin.tmpl/build.gradle.tmpl
+++ b/packages/flutter_tools/templates/plugin/android-kotlin.tmpl/build.gradle.tmpl
@@ -7,7 +7,7 @@
     }
 
     dependencies {
-        classpath 'com.android.tools.build:gradle:2.3.0'
+        classpath 'com.android.tools.build:gradle:2.3.3'
     }
 }
 
diff --git a/packages/flutter_tools/templates/plugin/android.tmpl/.gitignore b/packages/flutter_tools/templates/plugin/android.tmpl/.gitignore
index 5c4ef82..c6cbe56 100644
--- a/packages/flutter_tools/templates/plugin/android.tmpl/.gitignore
+++ b/packages/flutter_tools/templates/plugin/android.tmpl/.gitignore
@@ -6,7 +6,3 @@
 .DS_Store
 /build
 /captures
-
-/gradle
-/gradlew
-/gradlew.bat
diff --git a/packages/flutter_tools/templates/plugin/android.tmpl/gradle/wrapper/gradle-wrapper.properties b/packages/flutter_tools/templates/plugin/android.tmpl/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..45e7f14
--- /dev/null
+++ b/packages/flutter_tools/templates/plugin/android.tmpl/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
diff --git a/packages/flutter_tools/test/cache_test.dart b/packages/flutter_tools/test/cache_test.dart
index f4b4509..b921260 100644
--- a/packages/flutter_tools/test/cache_test.dart
+++ b/packages/flutter_tools/test/cache_test.dart
@@ -48,6 +48,34 @@
       Platform: () => new FakePlatform()..environment = <String, String>{'FLUTTER_ALREADY_LOCKED': 'true'},
     });
   });
+  group('Cache', () {
+    test('should not be up to date, if some cached artifact is not', () {
+      final CachedArtifact artifact1 = new MockCachedArtifact();
+      final CachedArtifact artifact2 = new MockCachedArtifact();
+      when(artifact1.isUpToDate()).thenReturn(true);
+      when(artifact2.isUpToDate()).thenReturn(false);
+      final Cache cache = new Cache(artifacts: <CachedArtifact>[artifact1, artifact2]);
+      expect(cache.isUpToDate(), isFalse);
+    });
+    test('should be up to date, if all cached artifacts are', () {
+      final CachedArtifact artifact1 = new MockCachedArtifact();
+      final CachedArtifact artifact2 = new MockCachedArtifact();
+      when(artifact1.isUpToDate()).thenReturn(true);
+      when(artifact2.isUpToDate()).thenReturn(true);
+      final Cache cache = new Cache(artifacts: <CachedArtifact>[artifact1, artifact2]);
+      expect(cache.isUpToDate(), isTrue);
+    });
+    test('should update cached artifacts which are not up to date', () async {
+      final CachedArtifact artifact1 = new MockCachedArtifact();
+      final CachedArtifact artifact2 = new MockCachedArtifact();
+      when(artifact1.isUpToDate()).thenReturn(true);
+      when(artifact2.isUpToDate()).thenReturn(false);
+      final Cache cache = new Cache(artifacts: <CachedArtifact>[artifact1, artifact2]);
+      await cache.updateAll();
+      verifyNever(artifact1.update());
+      verify(artifact2.update());
+    });
+  });
 }
 
 class MockFileSystem extends MemoryFileSystem {
@@ -65,3 +93,4 @@
 }
 
 class MockRandomAccessFile extends Mock implements RandomAccessFile {}
+class MockCachedArtifact extends Mock implements CachedArtifact {}