Reland #39157 (#39798)

diff --git a/packages/flutter_tools/test/general.shard/cache_test.dart b/packages/flutter_tools/test/general.shard/cache_test.dart
index aa9d940..5a15cb7 100644
--- a/packages/flutter_tools/test/general.shard/cache_test.dart
+++ b/packages/flutter_tools/test/general.shard/cache_test.dart
@@ -8,8 +8,11 @@
 import 'package:meta/meta.dart';
 import 'package:mockito/mockito.dart';
 import 'package:platform/platform.dart';
+import 'package:process/process.dart';
 
+import 'package:flutter_tools/src/android/gradle.dart';
 import 'package:flutter_tools/src/base/common.dart';
+import 'package:flutter_tools/src/base/io.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/io.dart' show InternetAddress, SocketException;
@@ -136,18 +139,30 @@
       when(artifact1.isUpToDate()).thenReturn(true);
       when(artifact2.isUpToDate()).thenReturn(false);
       final Cache cache = Cache(artifacts: <CachedArtifact>[artifact1, artifact2]);
-      await cache.updateAll(<DevelopmentArtifact>{});
-      verifyNever(artifact1.update(<DevelopmentArtifact>{}));
-      verify(artifact2.update(<DevelopmentArtifact>{}));
+      await cache.updateAll(<DevelopmentArtifact>{
+        null,
+      });
+      verifyNever(artifact1.update());
+      verify(artifact2.update());
     });
     testUsingContext('getter dyLdLibEntry concatenates the output of each artifact\'s dyLdLibEntry getter', () async {
-      final CachedArtifact artifact1 = MockCachedArtifact();
-      final CachedArtifact artifact2 = MockCachedArtifact();
-      final CachedArtifact artifact3 = MockCachedArtifact();
-      when(artifact1.dyLdLibPath).thenReturn('/path/to/alpha:/path/to/beta');
-      when(artifact2.dyLdLibPath).thenReturn('/path/to/gamma:/path/to/delta:/path/to/epsilon');
-      when(artifact3.dyLdLibPath).thenReturn(''); // Empty output
+      final IosUsbArtifacts artifact1 = MockIosUsbArtifacts();
+      final IosUsbArtifacts artifact2 = MockIosUsbArtifacts();
+      final IosUsbArtifacts artifact3 = MockIosUsbArtifacts();
+      when(artifact1.environment)
+          .thenReturn(<String, String>{
+            'DYLD_LIBRARY_PATH': '/path/to/alpha:/path/to/beta',
+          });
+      when(artifact2.environment)
+          .thenReturn(<String, String>{
+            'DYLD_LIBRARY_PATH': '/path/to/gamma:/path/to/delta:/path/to/epsilon',
+          });
+      when(artifact3.environment)
+          .thenReturn(<String, String>{
+            'DYLD_LIBRARY_PATH': '',
+          });
       final Cache cache = Cache(artifacts: <CachedArtifact>[artifact1, artifact2, artifact3]);
+
       expect(cache.dyLdLibEntry.key, 'DYLD_LIBRARY_PATH');
       expect(
         cache.dyLdLibEntry.value,
@@ -163,18 +178,20 @@
       when(artifact2.isUpToDate()).thenReturn(false);
       final MockInternetAddress address = MockInternetAddress();
       when(address.host).thenReturn('storage.googleapis.com');
-      when(artifact1.update(<DevelopmentArtifact>{})).thenThrow(SocketException(
+      when(artifact1.update()).thenThrow(SocketException(
         'Connection reset by peer',
         address: address,
       ));
       final Cache cache = Cache(artifacts: <CachedArtifact>[artifact1, artifact2]);
       try {
-        await cache.updateAll(<DevelopmentArtifact>{});
+        await cache.updateAll(<DevelopmentArtifact>{
+          null,
+        });
         fail('Mock thrown exception expected');
       } catch (e) {
-        verify(artifact1.update(<DevelopmentArtifact>{}));
+        verify(artifact1.update());
         // Don't continue when retrieval fails.
-        verifyNever(artifact2.update(<DevelopmentArtifact>{}));
+        verifyNever(artifact2.update());
         expect(
           testLogger.errorText,
           contains('https://flutter.dev/community/china'),
@@ -226,6 +243,7 @@
         binaryDirs: <List<String>>[
           <String>['bin_dir', 'unused_url_path'],
         ],
+        requiredArtifacts: DevelopmentArtifact.universal,
       );
       await artifact.updateInner();
       final Directory dir = memoryFileSystem.systemTempDirectory
@@ -244,25 +262,53 @@
     });
   });
 
-  testUsingContext('throws tool exit on fs exception', () async {
-    final FakeCachedArtifact fakeCachedArtifact = FakeCachedArtifact(
-      cache: MockCache(),
-      requiredArtifacts: <DevelopmentArtifact>{
-        DevelopmentArtifact.android,
-      }
-    );
-    final Directory mockDirectory = MockDirectory();
-    when(fakeCachedArtifact.cache.getArtifactDirectory(any))
-        .thenReturn(mockDirectory);
-    when(mockDirectory.existsSync()).thenReturn(false);
-    when(mockDirectory.createSync(recursive: true))
-        .thenThrow(const FileSystemException());
+  group('AndroidMavenArtifacts', () {
+    MemoryFileSystem memoryFileSystem;
+    MockProcessManager processManager;
+    MockCache mockCache;
 
-    expect(() => fakeCachedArtifact.update(<DevelopmentArtifact>{
-        DevelopmentArtifact.android,
-    }), throwsA(isInstanceOf<ToolExit>()));
-  }, overrides: <Type, Generator>{
-    FileSystem: () => MemoryFileSystem(),
+    setUp(() {
+      memoryFileSystem = MemoryFileSystem();
+      processManager = MockProcessManager();
+      mockCache = MockCache();
+    });
+
+    test('development artifact', () async {
+      final AndroidMavenArtifacts mavenArtifacts = AndroidMavenArtifacts();
+      expect(mavenArtifacts.developmentArtifact, DevelopmentArtifact.androidMaven);
+    });
+
+    testUsingContext('update', () async {
+      final AndroidMavenArtifacts mavenArtifacts = AndroidMavenArtifacts();
+      expect(mavenArtifacts.isUpToDate(), isFalse);
+
+      final Directory gradleWrapperDir = fs.systemTempDirectory.createTempSync('gradle_wrapper.');
+      when(mockCache.getArtifactDirectory('gradle_wrapper')).thenReturn(gradleWrapperDir);
+
+      fs.directory(gradleWrapperDir.childDirectory('gradle').childDirectory('wrapper'))
+          .createSync(recursive: true);
+      fs.file(fs.path.join(gradleWrapperDir.path, 'gradlew')).writeAsStringSync('irrelevant');
+      fs.file(fs.path.join(gradleWrapperDir.path, 'gradlew.bat')).writeAsStringSync('irrelevant');
+
+      when(processManager.run(any, environment: captureAnyNamed('environment')))
+        .thenAnswer((Invocation invocation) {
+          final List<String> args = invocation.positionalArguments[0];
+          expect(args.length, 6);
+          expect(args[1], '-b');
+          expect(args[2].endsWith('resolve_dependencies.gradle'), isTrue);
+          expect(args[5], 'resolveDependencies');
+          expect(invocation.namedArguments[#environment], gradleEnv);
+          return Future<ProcessResult>.value(ProcessResult(0, 0, '', ''));
+        });
+
+      await mavenArtifacts.update();
+
+      expect(mavenArtifacts.isUpToDate(), isFalse);
+    }, overrides: <Type, Generator>{
+      Cache: ()=> mockCache,
+      FileSystem: () => memoryFileSystem,
+      ProcessManager: () => processManager,
+    });
   });
 }
 
@@ -270,7 +316,7 @@
   FakeCachedArtifact({
     String stampName = 'STAMP',
     @required Cache cache,
-    Set<DevelopmentArtifact> requiredArtifacts = const <DevelopmentArtifact>{},
+    DevelopmentArtifact requiredArtifacts,
     this.binaryDirs = const <List<String>>[],
     this.licenseDirs = const <String>[],
     this.packageDirs = const <String>[],
@@ -290,12 +336,14 @@
   List<String> getPackageDirs() => packageDirs;
 }
 
+class MockProcessManager extends Mock implements ProcessManager {}
 class MockFileSystem extends Mock implements FileSystem {}
 class MockFile extends Mock implements File {}
 class MockDirectory extends Mock implements Directory {}
 
 class MockRandomAccessFile extends Mock implements RandomAccessFile {}
 class MockCachedArtifact extends Mock implements CachedArtifact {}
+class MockIosUsbArtifacts extends Mock implements IosUsbArtifacts {}
 class MockInternetAddress extends Mock implements InternetAddress {}
 class MockCache extends Mock implements Cache {}
 class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {}