prepare_package_test.dart 26.6 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:convert';
import 'dart:io' hide Platform;
7
import 'dart:typed_data';
8 9

import 'package:path/path.dart' as path;
10
import 'package:platform/platform.dart' show FakePlatform;
11

12
import '../../../packages/flutter_tools/test/src/fake_process_manager.dart';
13
import '../prepare_package.dart';
14
import 'common.dart';
15 16

void main() {
17
  const String testRef = 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef';
18 19 20
  test('Throws on missing executable', () async {
    // Uses a *real* process manager, since we want to know what happens if
    // it can't find an executable.
21
    final ProcessRunner processRunner = ProcessRunner(subprocessOutput: false);
22 23 24 25
    expect(
        expectAsync1((List<String> commandLine) async {
          return processRunner.runProcess(commandLine);
        })(<String>['this_executable_better_not_exist_2857632534321']),
26
        throwsA(isA<PreparePackageException>()));
27 28 29 30 31 32

    await expectLater(
      () => processRunner.runProcess(<String>['this_executable_better_not_exist_2857632534321']),
      throwsA(isA<PreparePackageException>().having(
        (PreparePackageException error) => error.message,
        'message',
33
        contains('ProcessException: Failed to find "this_executable_better_not_exist_2857632534321" in the search path'),
34 35
      )),
    );
36
  });
37
  for (final String platformName in <String>['macos', 'linux', 'windows']) {
38
    final FakePlatform platform = FakePlatform(
39
      operatingSystem: platformName,
40 41 42
      environment: <String, String>{
        'DEPOT_TOOLS': path.join('D:', 'depot_tools'),
      },
43
    );
44 45
    group('ProcessRunner for $platform', () {
      test('Returns stdout', () async {
46 47 48 49 50 51 52
        final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[
          const FakeCommand(
            command: <String>['echo', 'test',],
            stdout: 'output',
            stderr: 'error',
          )
        ]);
53
        final ProcessRunner processRunner = ProcessRunner(
54 55
            subprocessOutput: false, platform: platform, processManager: fakeProcessManager);
        final String output = await processRunner.runProcess(<String>['echo', 'test']);
56 57 58
        expect(output, equals('output'));
      });
      test('Throws on process failure', () async {
59 60 61 62 63 64 65 66
        final FakeProcessManager fakeProcessManager = FakeProcessManager.list(<FakeCommand>[
          const FakeCommand(
            command: <String>['echo', 'test',],
            stdout: 'output',
            stderr: 'error',
            exitCode: -1,
          )
        ]);
67
        final ProcessRunner processRunner = ProcessRunner(
68
            subprocessOutput: false, platform: platform, processManager: fakeProcessManager);
69 70 71 72
        expect(
            expectAsync1((List<String> commandLine) async {
              return processRunner.runProcess(commandLine);
            })(<String>['echo', 'test']),
73
            throwsA(isA<PreparePackageException>()));
74 75
      });
    });
76
    group('ArchiveCreator for $platformName', () {
77 78
      late ArchiveCreator creator;
      late Directory tempDir;
79
      Directory flutterDir;
80
      Directory cacheDir;
81
      late FakeProcessManager processManager;
82 83
      final List<List<String>> args = <List<String>>[];
      final List<Map<Symbol, dynamic>> namedArgs = <Map<Symbol, dynamic>>[];
84
      late String flutter;
85

86
      Future<Uint8List> fakeHttpReader(Uri url, {Map<String, String>? headers}) {
87
        return Future<Uint8List>.value(Uint8List(0));
88 89
      }

90
      setUp(() async {
91
        processManager = FakeProcessManager.list(<FakeCommand>[]);
92 93
        args.clear();
        namedArgs.clear();
94
        tempDir = Directory.systemTemp.createTempSync('flutter_prepage_package_test.');
95
        flutterDir = Directory(path.join(tempDir.path, 'flutter'));
96
        flutterDir.createSync(recursive: true);
97 98
        cacheDir = Directory(path.join(flutterDir.path, 'bin', 'cache'));
        cacheDir.createSync(recursive: true);
99
        creator = ArchiveCreator(
100 101
          tempDir,
          tempDir,
102 103 104 105 106
          testRef,
          Branch.dev,
          processManager: processManager,
          subprocessOutput: false,
          platform: platform,
107
          httpReader: fakeHttpReader,
108 109 110 111 112
        );
        flutter = path.join(creator.flutterRoot.absolute.path, 'bin', 'flutter');
      });

      tearDown(() async {
113
        tryToDelete(tempDir);
114 115 116
      });

      test('sets PUB_CACHE properly', () async {
117
        final String createBase = path.join(tempDir.absolute.path, 'create_');
118 119
        final String archiveName = path.join(tempDir.absolute.path,
            'flutter_${platformName}_v1.2.3-dev${platform.isLinux ? '.tar.xz' : '.zip'}');
120

121
        processManager.addCommands(convertResults(<String, List<ProcessResult>?>{
122
          'git clone -b dev https://chromium.googlesource.com/external/github.com/flutter/flutter': null,
123
          'git reset --hard $testRef': null,
124
          'git remote set-url origin https://github.com/flutter/flutter.git': null,
125
          'git describe --tags --exact-match $testRef': <ProcessResult>[ProcessResult(0, 0, 'v1.2.3', '')],
126
          if (platform.isWindows) '7za x ${path.join(tempDir.path, 'mingit.zip')}': null,
127 128 129 130 131 132 133
          '$flutter doctor': null,
          '$flutter update-packages': null,
          '$flutter precache': null,
          '$flutter ide-config': null,
          '$flutter create --template=app ${createBase}app': null,
          '$flutter create --template=package ${createBase}package': null,
          '$flutter create --template=plugin ${createBase}plugin': null,
134 135
          'git clean -f -x -- **/.packages': null,
          'git clean -f -x -- **/.dart_tool/': null,
136
          if (platform.isMacOS) 'codesign -vvvv --check-notarization ${path.join(tempDir.path, 'flutter', 'bin', 'cache', 'dart-sdk', 'bin', 'dart')}': null,
137
          if (platform.isWindows) 'attrib -h .git': null,
138
          if (platform.isWindows) '7za a -tzip -mx=9 $archiveName flutter': null
139
          else if (platform.isMacOS) 'zip -r -9 --symlinks $archiveName flutter': null
140
          else if (platform.isLinux) 'tar cJf $archiveName flutter': null,
141
        }));
142 143 144 145 146
        await creator.initializeRepo();
        await creator.createArchive();
      });

      test('calls the right commands for archive output', () async {
147
        final String createBase = path.join(tempDir.absolute.path, 'create_');
148 149
        final String archiveName = path.join(tempDir.absolute.path,
            'flutter_${platformName}_v1.2.3-dev${platform.isLinux ? '.tar.xz' : '.zip'}');
150
        final Map<String, List<ProcessResult>?> calls = <String, List<ProcessResult>?>{
151
          'git clone -b dev https://chromium.googlesource.com/external/github.com/flutter/flutter': null,
152
          'git reset --hard $testRef': null,
153
          'git remote set-url origin https://github.com/flutter/flutter.git': null,
154
          'git describe --tags --exact-match $testRef': <ProcessResult>[ProcessResult(0, 0, 'v1.2.3', '')],
155
          if (platform.isWindows) '7za x ${path.join(tempDir.path, 'mingit.zip')}': null,
156 157 158 159 160
          '$flutter doctor': null,
          '$flutter update-packages': null,
          '$flutter precache': null,
          '$flutter ide-config': null,
          '$flutter create --template=app ${createBase}app': null,
161
          '$flutter create --template=package ${createBase}package': null,
162
          '$flutter create --template=plugin ${createBase}plugin': null,
163 164
          'git clean -f -x -- **/.packages': null,
          'git clean -f -x -- **/.dart_tool/': null,
165
          if (platform.isMacOS) 'codesign -vvvv --check-notarization ${path.join(tempDir.path, 'flutter', 'bin', 'cache', 'dart-sdk', 'bin', 'dart')}': null,
166
          if (platform.isWindows) 'attrib -h .git': null,
167
          if (platform.isWindows) '7za a -tzip -mx=9 $archiveName flutter': null
168
          else if (platform.isMacOS) 'zip -r -9 --symlinks $archiveName flutter': null
169 170
          else if (platform.isLinux) 'tar cJf $archiveName flutter': null,
        };
171
        processManager.addCommands(convertResults(calls));
172
        creator = ArchiveCreator(
173 174
          tempDir,
          tempDir,
175 176 177 178 179
          testRef,
          Branch.dev,
          processManager: processManager,
          subprocessOutput: false,
          platform: platform,
180
          httpReader: fakeHttpReader,
181 182 183 184 185 186 187 188
        );
        await creator.initializeRepo();
        await creator.createArchive();
      });

      test('throws when a command errors out', () async {
        final Map<String, List<ProcessResult>> calls = <String, List<ProcessResult>>{
          'git clone -b dev https://chromium.googlesource.com/external/github.com/flutter/flutter':
189 190
              <ProcessResult>[ProcessResult(0, 0, 'output1', '')],
          'git reset --hard $testRef': <ProcessResult>[ProcessResult(0, -1, 'output2', '')],
191
        };
192
        processManager.addCommands(convertResults(calls));
193
        expect(expectAsync0(creator.initializeRepo), throwsA(isA<PreparePackageException>()));
194 195 196 197
      });

      test('non-strict mode calls the right commands', () async {
        final String createBase = path.join(tempDir.absolute.path, 'create_');
198 199
        final String archiveName = path.join(tempDir.absolute.path,
            'flutter_${platformName}_v1.2.3-dev${platform.isLinux ? '.tar.xz' : '.zip'}');
200
        final Map<String, List<ProcessResult>?> calls = <String, List<ProcessResult>?>{
201 202 203 204
          'git clone -b dev https://chromium.googlesource.com/external/github.com/flutter/flutter': null,
          'git reset --hard $testRef': null,
          'git remote set-url origin https://github.com/flutter/flutter.git': null,
          'git describe --tags --abbrev=0 $testRef': <ProcessResult>[ProcessResult(0, 0, 'v1.2.3', '')],
205
          if (platform.isWindows) '7za x ${path.join(tempDir.path, 'mingit.zip')}': null,
206 207 208 209 210 211 212
          '$flutter doctor': null,
          '$flutter update-packages': null,
          '$flutter precache': null,
          '$flutter ide-config': null,
          '$flutter create --template=app ${createBase}app': null,
          '$flutter create --template=package ${createBase}package': null,
          '$flutter create --template=plugin ${createBase}plugin': null,
213 214
          'git clean -f -x -- **/.packages': null,
          'git clean -f -x -- **/.dart_tool/': null,
215
          if (platform.isWindows) 'attrib -h .git': null,
216
          if (platform.isWindows) '7za a -tzip -mx=9 $archiveName flutter': null
217
          else if (platform.isMacOS) 'zip -r -9 --symlinks $archiveName flutter': null
218 219
          else if (platform.isLinux) 'tar cJf $archiveName flutter': null,
        };
220
        processManager.addCommands(convertResults(calls));
221 222 223 224 225 226 227 228 229 230 231 232 233
        creator = ArchiveCreator(
          tempDir,
          tempDir,
          testRef,
          Branch.dev,
          strict: false,
          processManager: processManager,
          subprocessOutput: false,
          platform: platform,
          httpReader: fakeHttpReader,
        );
        await creator.initializeRepo();
        await creator.createArchive();
234
      });
235 236 237 238 239 240 241

      test('fails if binary is not codesigned', () async {
        final String createBase = path.join(tempDir.absolute.path, 'create_');
        final String archiveName = path.join(tempDir.absolute.path,
            'flutter_${platformName}_v1.2.3-dev${platform.isLinux ? '.tar.xz' : '.zip'}');
        final ProcessResult codesignFailure = ProcessResult(1, 1, '', 'code object is not signed at all');
        final String binPath = path.join(tempDir.path, 'flutter', 'bin', 'cache', 'dart-sdk', 'bin', 'dart');
242
        final Map<String, List<ProcessResult>?> calls = <String, List<ProcessResult>?>{
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
          'git clone -b dev https://chromium.googlesource.com/external/github.com/flutter/flutter': null,
          'git reset --hard $testRef': null,
          'git remote set-url origin https://github.com/flutter/flutter.git': null,
          'git describe --tags --exact-match $testRef': <ProcessResult>[ProcessResult(0, 0, 'v1.2.3', '')],
          if (platform.isWindows) '7za x ${path.join(tempDir.path, 'mingit.zip')}': null,
          '$flutter doctor': null,
          '$flutter update-packages': null,
          '$flutter precache': null,
          '$flutter ide-config': null,
          '$flutter create --template=app ${createBase}app': null,
          '$flutter create --template=package ${createBase}package': null,
          '$flutter create --template=plugin ${createBase}plugin': null,
          'git clean -f -x -- **/.packages': null,
          'git clean -f -x -- **/.dart_tool/': null,
          if (platform.isMacOS) 'codesign -vvvv --check-notarization $binPath': <ProcessResult>[codesignFailure],
          if (platform.isWindows) 'attrib -h .git': null,
          if (platform.isWindows) '7za a -tzip -mx=9 $archiveName flutter': null
          else if (platform.isMacOS) 'zip -r -9 --symlinks $archiveName flutter': null
          else if (platform.isLinux) 'tar cJf $archiveName flutter': null,
        };
        processManager.addCommands(convertResults(calls));
        creator = ArchiveCreator(
          tempDir,
          tempDir,
          testRef,
          Branch.dev,
          processManager: processManager,
          subprocessOutput: false,
          platform: platform,
          httpReader: fakeHttpReader,
        );
        await creator.initializeRepo();

276 277 278 279 280 281 282 283
        await expectLater(
          () => creator.createArchive(),
          throwsA(isA<PreparePackageException>().having(
            (PreparePackageException exception) => exception.message,
            'message',
            contains('The binary $binPath was not codesigned!'),
          )),
        );
284
      }, skip: !platform.isMacOS); // [intended] codesign is only available on macOS
285 286
    });

287
    group('ArchivePublisher for $platformName', () {
288 289
      late FakeProcessManager processManager;
      late Directory tempDir;
290
      final String gsutilCall = platform.isWindows
291
          ? 'python3 ${path.join("D:", "depot_tools", "gsutil.py")}'
292 293 294 295
          : 'gsutil.py';
      final String releasesName = 'releases_$platformName.json';
      final String archiveName = platform.isLinux ? 'archive.tar.xz' : 'archive.zip';
      final String archiveMime = platform.isLinux ? 'application/x-gtar' : 'application/zip';
296
      final String gsArchivePath = 'gs://flutter_infra_release/releases/stable/$platformName/$archiveName';
297 298

      setUp(() async {
299
        processManager = FakeProcessManager.list(<FakeCommand>[]);
300
        tempDir = Directory.systemTemp.createTempSync('flutter_prepage_package_test.');
301 302 303
      });

      tearDown(() async {
304
        tryToDelete(tempDir);
305 306 307
      });

      test('calls the right processes', () async {
308
        final String archivePath = path.join(tempDir.absolute.path, archiveName);
309
        final String jsonPath = path.join(tempDir.absolute.path, releasesName);
310
        final String gsJsonPath = 'gs://flutter_infra_release/releases/$releasesName';
311 312
        final String releasesJson = '''
{
313
  "base_url": "https://storage.googleapis.com/flutter_infra_release/releases",
314 315 316 317 318 319 320 321 322 323
  "current_release": {
    "beta": "3ea4d06340a97a1e9d7cae97567c64e0569dcaa2",
    "dev": "5a58b36e36b8d7aace89d3950e6deb307956a6a0"
  },
  "releases": [
    {
      "hash": "5a58b36e36b8d7aace89d3950e6deb307956a6a0",
      "channel": "dev",
      "version": "v0.2.3",
      "release_date": "2018-03-20T01:47:02.851729Z",
324 325
      "archive": "dev/$platformName/flutter_${platformName}_v0.2.3-dev.zip",
      "sha256": "4fe85a822093e81cb5a66c7fc263f68de39b5797b294191b6d75e7afcc86aff8"
326
    },
327 328 329 330 331
    {
      "hash": "b9bd51cc36b706215915711e580851901faebb40",
      "channel": "beta",
      "version": "v0.2.2",
      "release_date": "2018-03-16T18:48:13.375013Z",
332 333
      "archive": "dev/$platformName/flutter_${platformName}_v0.2.2-dev.zip",
      "sha256": "6073331168cdb37a4637a5dc073d6a7ef4e466321effa2c529fa27d2253a4d4b"
334 335 336
    },
    {
      "hash": "$testRef",
337
      "channel": "stable",
338 339
      "version": "v0.0.0",
      "release_date": "2018-03-20T01:47:02.851729Z",
340 341
      "archive": "stable/$platformName/flutter_${platformName}_v0.0.0-dev.zip",
      "sha256": "5dd34873b3a3e214a32fd30c2c319a0f46e608afb72f0d450b2d621a6d02aebd"
342
    }
343
  ]
344
}
345
''';
346
        File(jsonPath).writeAsStringSync(releasesJson);
347
        File(archivePath).writeAsStringSync('archive contents');
348
        final Map<String, List<ProcessResult>?> calls = <String, List<ProcessResult>?>{
349 350
          // This process fails because the file does NOT already exist
          '$gsutilCall -- stat $gsArchivePath': <ProcessResult>[ProcessResult(0, 1, '', '')],
351 352 353 354
          '$gsutilCall -- rm $gsArchivePath': null,
          '$gsutilCall -- -h Content-Type:$archiveMime cp $archivePath $gsArchivePath': null,
          '$gsutilCall -- cp $gsJsonPath $jsonPath': null,
          '$gsutilCall -- rm $gsJsonPath': null,
355
          '$gsutilCall -- -h Content-Type:application/json -h Cache-Control:max-age=60 cp $jsonPath $gsJsonPath': null,
356
        };
357
        processManager.addCommands(convertResults(calls));
358
        final File outputFile = File(path.join(tempDir.absolute.path, archiveName));
359
        outputFile.createSync();
360
        assert(tempDir.existsSync());
361
        final ArchivePublisher publisher = ArchivePublisher(
362 363
          tempDir,
          testRef,
364
          Branch.stable,
365
          'v1.2.3',
366
          outputFile,
367
          false,
368 369 370 371 372 373
          processManager: processManager,
          subprocessOutput: false,
          platform: platform,
        );
        assert(tempDir.existsSync());
        await publisher.publishArchive();
374

375
        final File releaseFile = File(jsonPath);
376 377 378
        expect(releaseFile.existsSync(), isTrue);
        final String contents = releaseFile.readAsStringSync();
        // Make sure new data is added.
379
        expect(contents, contains('"hash": "$testRef"'));
380 381
        expect(contents, contains('"channel": "stable"'));
        expect(contents, contains('"archive": "stable/$platformName/$archiveName"'));
382
        expect(contents, contains('"sha256": "f69f4865f861193a91d1c5544a894167a7137b788d10bac8edbf5d095f45cb4d"'));
383
        // Make sure existing entries are preserved.
384 385 386 387 388 389
        expect(contents, contains('"hash": "5a58b36e36b8d7aace89d3950e6deb307956a6a0"'));
        expect(contents, contains('"hash": "b9bd51cc36b706215915711e580851901faebb40"'));
        expect(contents, contains('"channel": "beta"'));
        expect(contents, contains('"channel": "dev"'));
        // Make sure old matching entries are removed.
        expect(contents, isNot(contains('v0.0.0')));
390 391
        final Map<String, dynamic> jsonData = json.decode(contents) as Map<String, dynamic>;
        final List<dynamic> releases = jsonData['releases'] as List<dynamic>;
392 393 394 395
        expect(releases.length, equals(3));
        // Make sure the new entry is first (and hopefully it takes less than a
        // minute to go from publishArchive above to this line!).
        expect(
396
          DateTime.now().difference(DateTime.parse((releases[0] as Map<String, dynamic>)['release_date'] as String)),
397 398
          lessThan(const Duration(minutes: 1)),
        );
399
        const JsonEncoder encoder = JsonEncoder.withIndent('  ');
400 401
        expect(contents, equals(encoder.convert(jsonData)));
      });
402

403 404 405
      test('updates base_url from old bucket to new bucket', () async {
        final String archivePath = path.join(tempDir.absolute.path, archiveName);
        final String jsonPath = path.join(tempDir.absolute.path, releasesName);
406
        final String gsJsonPath = 'gs://flutter_infra_release/releases/$releasesName';
407 408
        final String releasesJson = '''
{
409
  "base_url": "https://storage.googleapis.com/flutter_infra_release/releases",
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443
  "current_release": {
    "beta": "3ea4d06340a97a1e9d7cae97567c64e0569dcaa2",
    "dev": "5a58b36e36b8d7aace89d3950e6deb307956a6a0"
  },
  "releases": [
    {
      "hash": "5a58b36e36b8d7aace89d3950e6deb307956a6a0",
      "channel": "dev",
      "version": "v0.2.3",
      "release_date": "2018-03-20T01:47:02.851729Z",
      "archive": "dev/$platformName/flutter_${platformName}_v0.2.3-dev.zip",
      "sha256": "4fe85a822093e81cb5a66c7fc263f68de39b5797b294191b6d75e7afcc86aff8"
    },
    {
      "hash": "b9bd51cc36b706215915711e580851901faebb40",
      "channel": "beta",
      "version": "v0.2.2",
      "release_date": "2018-03-16T18:48:13.375013Z",
      "archive": "dev/$platformName/flutter_${platformName}_v0.2.2-dev.zip",
      "sha256": "6073331168cdb37a4637a5dc073d6a7ef4e466321effa2c529fa27d2253a4d4b"
    },
    {
      "hash": "$testRef",
      "channel": "stable",
      "version": "v0.0.0",
      "release_date": "2018-03-20T01:47:02.851729Z",
      "archive": "stable/$platformName/flutter_${platformName}_v0.0.0-dev.zip",
      "sha256": "5dd34873b3a3e214a32fd30c2c319a0f46e608afb72f0d450b2d621a6d02aebd"
    }
  ]
}
''';
        File(jsonPath).writeAsStringSync(releasesJson);
        File(archivePath).writeAsStringSync('archive contents');
444
        final Map<String, List<ProcessResult>?> calls = <String, List<ProcessResult>?>{
445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477
          // This process fails because the file does NOT already exist
          '$gsutilCall -- stat $gsArchivePath': <ProcessResult>[ProcessResult(0, 1, '', '')],
          '$gsutilCall -- rm $gsArchivePath': null,
          '$gsutilCall -- -h Content-Type:$archiveMime cp $archivePath $gsArchivePath': null,
          '$gsutilCall -- cp $gsJsonPath $jsonPath': null,
          '$gsutilCall -- rm $gsJsonPath': null,
          '$gsutilCall -- -h Content-Type:application/json -h Cache-Control:max-age=60 cp $jsonPath $gsJsonPath': null,
        };
        processManager.addCommands(convertResults(calls));
        final File outputFile = File(path.join(tempDir.absolute.path, archiveName));
        outputFile.createSync();
        assert(tempDir.existsSync());
        final ArchivePublisher publisher = ArchivePublisher(
          tempDir,
          testRef,
          Branch.stable,
          'v1.2.3',
          outputFile,
          false,
          processManager: processManager,
          subprocessOutput: false,
          platform: platform,
        );
        assert(tempDir.existsSync());
        await publisher.publishArchive();

        final File releaseFile = File(jsonPath);
        expect(releaseFile.existsSync(), isTrue);
        final String contents = releaseFile.readAsStringSync();
        final Map<String, dynamic> jsonData = json.decode(contents) as Map<String, dynamic>;
        expect(jsonData['base_url'], 'https://storage.googleapis.com/flutter_infra_release/releases');
      });

478 479 480 481 482 483 484 485 486
      test('publishArchive throws if forceUpload is false and artifact already exists on cloud storage', () async {
        final String archiveName = platform.isLinux ? 'archive.tar.xz' : 'archive.zip';
        final File outputFile = File(path.join(tempDir.absolute.path, archiveName));
        final ArchivePublisher publisher = ArchivePublisher(
          tempDir,
          testRef,
          Branch.stable,
          'v1.2.3',
          outputFile,
487
          false,
488 489 490 491 492 493 494 495
          processManager: processManager,
          subprocessOutput: false,
          platform: platform,
        );
        final Map<String, List<ProcessResult>> calls = <String, List<ProcessResult>>{
          // This process returns 0 because file already exists
          '$gsutilCall -- stat $gsArchivePath': <ProcessResult>[ProcessResult(0, 0, '', '')],
        };
496
        processManager.addCommands(convertResults(calls));
497
        expect(() async => publisher.publishArchive(), throwsException);
498 499 500 501 502 503 504 505 506 507 508
      });

      test('publishArchive does not throw if forceUpload is true and artifact already exists on cloud storage', () async {
        final String archiveName = platform.isLinux ? 'archive.tar.xz' : 'archive.zip';
        final File outputFile = File(path.join(tempDir.absolute.path, archiveName));
        final ArchivePublisher publisher = ArchivePublisher(
          tempDir,
          testRef,
          Branch.stable,
          'v1.2.3',
          outputFile,
509
          false,
510 511 512 513 514 515
          processManager: processManager,
          subprocessOutput: false,
          platform: platform,
        );
        final String archivePath = path.join(tempDir.absolute.path, archiveName);
        final String jsonPath = path.join(tempDir.absolute.path, releasesName);
516
        final String gsJsonPath = 'gs://flutter_infra_release/releases/$releasesName';
517 518
        final String releasesJson = '''
{
519
  "base_url": "https://storage.googleapis.com/flutter_infra_release/releases",
520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553
  "current_release": {
    "beta": "3ea4d06340a97a1e9d7cae97567c64e0569dcaa2",
    "dev": "5a58b36e36b8d7aace89d3950e6deb307956a6a0"
  },
  "releases": [
    {
      "hash": "5a58b36e36b8d7aace89d3950e6deb307956a6a0",
      "channel": "dev",
      "version": "v0.2.3",
      "release_date": "2018-03-20T01:47:02.851729Z",
      "archive": "dev/$platformName/flutter_${platformName}_v0.2.3-dev.zip",
      "sha256": "4fe85a822093e81cb5a66c7fc263f68de39b5797b294191b6d75e7afcc86aff8"
    },
    {
      "hash": "b9bd51cc36b706215915711e580851901faebb40",
      "channel": "beta",
      "version": "v0.2.2",
      "release_date": "2018-03-16T18:48:13.375013Z",
      "archive": "dev/$platformName/flutter_${platformName}_v0.2.2-dev.zip",
      "sha256": "6073331168cdb37a4637a5dc073d6a7ef4e466321effa2c529fa27d2253a4d4b"
    },
    {
      "hash": "$testRef",
      "channel": "stable",
      "version": "v0.0.0",
      "release_date": "2018-03-20T01:47:02.851729Z",
      "archive": "stable/$platformName/flutter_${platformName}_v0.0.0-dev.zip",
      "sha256": "5dd34873b3a3e214a32fd30c2c319a0f46e608afb72f0d450b2d621a6d02aebd"
    }
  ]
}
''';
        File(jsonPath).writeAsStringSync(releasesJson);
        File(archivePath).writeAsStringSync('archive contents');
554
        final Map<String, List<ProcessResult>?> calls = <String, List<ProcessResult>?>{
555 556 557 558
          '$gsutilCall -- rm $gsArchivePath': null,
          '$gsutilCall -- -h Content-Type:$archiveMime cp $archivePath $gsArchivePath': null,
          '$gsutilCall -- cp $gsJsonPath $jsonPath': null,
          '$gsutilCall -- rm $gsJsonPath': null,
559
          '$gsutilCall -- -h Content-Type:application/json -h Cache-Control:max-age=60 cp $jsonPath $gsJsonPath': null,
560
        };
561
        processManager.addCommands(convertResults(calls));
562 563 564
        assert(tempDir.existsSync());
        await publisher.publishArchive(true);
      });
565 566
    });
  }
567
}
568

569
List<FakeCommand> convertResults(Map<String, List<ProcessResult>?> results) {
570 571
  final List<FakeCommand> commands = <FakeCommand>[];
  for (final String key in results.keys) {
572
    final List<ProcessResult>? candidates = results[key];
573 574 575 576 577 578 579 580 581 582
    final List<String> args = key.split(' ');
    if (candidates == null) {
      commands.add(FakeCommand(
        command: args,
      ));
    } else {
      for (final ProcessResult result in candidates) {
        commands.add(FakeCommand(
          command: args,
          exitCode: result.exitCode,
583 584
          stderr: result.stderr.toString(),
          stdout: result.stdout.toString(),
585 586 587 588 589 590
        ));
      }
    }
  }
  return commands;
}