artifact_updater_test.dart 20 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
11
import 'package:flutter_tools/src/base/platform.dart';
12
import 'package:flutter_tools/src/base/terminal.dart';
13
import 'package:flutter_tools/src/cache.dart';
14
import 'package:test/fake.dart';
15 16

import '../src/common.dart';
17
import '../src/fake_http_client.dart';
18
import '../src/fakes.dart';
19

20
final Platform testPlatform = FakePlatform();
21

22 23
void main() {
  testWithoutContext('ArtifactUpdater can download a zip archive', () async {
24
    final FakeOperatingSystemUtils operatingSystemUtils = FakeOperatingSystemUtils();
25 26 27 28 29 30
    final MemoryFileSystem fileSystem = MemoryFileSystem.test();
    final BufferLogger logger = BufferLogger.test();
    final ArtifactUpdater artifactUpdater = ArtifactUpdater(
      fileSystem: fileSystem,
      logger: logger,
      operatingSystemUtils: operatingSystemUtils,
31
      platform: testPlatform,
32
      httpClient: FakeHttpClient.any(),
33 34
      tempStorage: fileSystem.currentDirectory.childDirectory('temp')
        ..createSync(),
35
      allowedBaseUrls: <String>['http://test.zip'],
36 37 38 39
    );

    await artifactUpdater.downloadZipArchive(
      'test message',
40
      Uri.parse('http://test.zip'),
41 42 43 44 45 46
      fileSystem.currentDirectory.childDirectory('out'),
    );
    expect(logger.statusText, contains('test message'));
    expect(fileSystem.file('out/test'), exists);
  });

47
  testWithoutContext('ArtifactUpdater can download a zip archive and delete stale files', () async {
48
    final FakeOperatingSystemUtils operatingSystemUtils = FakeOperatingSystemUtils();
49 50 51 52 53 54 55
    final MemoryFileSystem fileSystem = MemoryFileSystem.test();
    final BufferLogger logger = BufferLogger.test();
    final ArtifactUpdater artifactUpdater = ArtifactUpdater(
      fileSystem: fileSystem,
      logger: logger,
      operatingSystemUtils: operatingSystemUtils,
      platform: testPlatform,
56
      httpClient: FakeHttpClient.any(),
57 58
      tempStorage: fileSystem.currentDirectory.childDirectory('temp')
        ..createSync(),
59
      allowedBaseUrls: <String>['http://test.zip'],
60 61 62 63 64 65 66 67
    );
    // Unrelated file from another cache.
    fileSystem.file('out/bar').createSync(recursive: true);
    // Stale file from current cache.
    fileSystem.file('out/test/foo.txt').createSync(recursive: true);

    await artifactUpdater.downloadZipArchive(
      'test message',
68
      Uri.parse('http://test.zip'),
69 70 71 72 73 74 75 76
      fileSystem.currentDirectory.childDirectory('out'),
    );
    expect(logger.statusText, contains('test message'));
    expect(fileSystem.file('out/test'), exists);
    expect(fileSystem.file('out/bar'), exists);
    expect(fileSystem.file('out/test/foo.txt'), isNot(exists));
  });

77 78
  testWithoutContext('ArtifactUpdater will not validate the md5 hash if the '
    'x-goog-hash header is present but missing an md5 entry', () async {
79
    final FakeOperatingSystemUtils operatingSystemUtils = FakeOperatingSystemUtils();
80 81 82 83 84 85 86 87
    final MemoryFileSystem fileSystem = MemoryFileSystem.test();
    final BufferLogger logger = BufferLogger.test();

    final ArtifactUpdater artifactUpdater = ArtifactUpdater(
      fileSystem: fileSystem,
      logger: logger,
      operatingSystemUtils: operatingSystemUtils,
      platform: testPlatform,
88
      httpClient: FakeHttpClient.list(<FakeRequest>[
89
        FakeRequest(Uri.parse('http://test.zip'), response: const FakeResponse(
90 91 92 93 94
          headers: <String, List<String>>{
            'x-goog-hash': <String>[],
          }
        )),
      ]),
95 96
      tempStorage: fileSystem.currentDirectory.childDirectory('temp')
        ..createSync(),
97
      allowedBaseUrls: <String>['http://test.zip'],
98 99 100 101
    );

    await artifactUpdater.downloadZipArchive(
      'test message',
102
      Uri.parse('http://test.zip'),
103 104 105 106 107 108 109 110
      fileSystem.currentDirectory.childDirectory('out'),
    );
    expect(logger.statusText, contains('test message'));
    expect(fileSystem.file('out/test'), exists);
  });

  testWithoutContext('ArtifactUpdater will validate the md5 hash if the '
    'x-goog-hash header is present', () async {
111
    final FakeOperatingSystemUtils operatingSystemUtils = FakeOperatingSystemUtils();
112 113 114 115 116 117 118 119
    final MemoryFileSystem fileSystem = MemoryFileSystem.test();
    final BufferLogger logger = BufferLogger.test();

    final ArtifactUpdater artifactUpdater = ArtifactUpdater(
      fileSystem: fileSystem,
      logger: logger,
      operatingSystemUtils: operatingSystemUtils,
      platform: testPlatform,
120
      httpClient: FakeHttpClient.list(<FakeRequest>[
121
        FakeRequest(Uri.parse('http://test.zip'), response: const FakeResponse(
122 123 124 125
          body: <int>[0],
          headers: <String, List<String>>{
            'x-goog-hash': <String>[
              'foo-bar-baz',
126
              'md5=k7iFrf4NoInN9jSQT9WfcQ==',
127 128 129 130
            ],
          }
        )),
      ]),
131 132
      tempStorage: fileSystem.currentDirectory.childDirectory('temp')
        ..createSync(),
133
      allowedBaseUrls: <String>['http://test.zip'],
134 135 136 137
    );

    await artifactUpdater.downloadZipArchive(
      'test message',
138
      Uri.parse('http://test.zip'),
139 140 141 142 143 144 145 146
      fileSystem.currentDirectory.childDirectory('out'),
    );
    expect(logger.statusText, contains('test message'));
    expect(fileSystem.file('out/test'), exists);
  });

  testWithoutContext('ArtifactUpdater will validate the md5 hash if the '
    'x-goog-hash header is present and throw if it does not match', () async {
147
    final FakeOperatingSystemUtils operatingSystemUtils = FakeOperatingSystemUtils();
148 149 150 151 152 153 154 155
    final MemoryFileSystem fileSystem = MemoryFileSystem.test();
    final BufferLogger logger = BufferLogger.test();

    final ArtifactUpdater artifactUpdater = ArtifactUpdater(
      fileSystem: fileSystem,
      logger: logger,
      operatingSystemUtils: operatingSystemUtils,
      platform: testPlatform,
156
      httpClient: FakeHttpClient.list(<FakeRequest>[
157
         FakeRequest(Uri.parse('http://test.zip'), response: const FakeResponse(
158 159 160 161
           body: <int>[0],
           headers: <String, List<String>>{
             'x-goog-hash': <String>[
              'foo-bar-baz',
162
              'md5=k7iFrf4SQT9WfcQ==',
163 164 165
            ],
          }
        )),
166
       FakeRequest(Uri.parse('http://test.zip'), response: const FakeResponse(
167 168 169
           headers: <String, List<String>>{
             'x-goog-hash': <String>[
              'foo-bar-baz',
170
              'md5=k7iFrf4SQT9WfcQ==',
171 172 173 174
            ],
          }
        )),
      ]),
175 176
      tempStorage: fileSystem.currentDirectory.childDirectory('temp')
        ..createSync(),
177
      allowedBaseUrls: <String>['http://test.zip'],
178 179
    );

180
    await expectLater(() async => artifactUpdater.downloadZipArchive(
181
      'test message',
182
      Uri.parse('http://test.zip'),
183 184 185 186
      fileSystem.currentDirectory.childDirectory('out'),
    ), throwsToolExit(message: 'k7iFrf4SQT9WfcQ==')); // validate that the hash mismatch message is included.
  });

187
  testWithoutContext('ArtifactUpdater will restart the status ticker if it needs to retry the download', () async {
188
    final FakeOperatingSystemUtils operatingSystemUtils = FakeOperatingSystemUtils();
189 190 191
    final MemoryFileSystem fileSystem = MemoryFileSystem.test();
    final Logger logger = StdoutLogger(
      terminal: Terminal.test(supportsColor: true),
192
      stdio: FakeStdio(),
193 194 195 196 197 198 199
      outputPreferences: OutputPreferences.test(),
    );
    final ArtifactUpdater artifactUpdater = ArtifactUpdater(
      fileSystem: fileSystem,
      logger: logger,
      operatingSystemUtils: operatingSystemUtils,
      platform: testPlatform,
200
      httpClient: FakeHttpClient.list(<FakeRequest>[
201 202
        FakeRequest(Uri.parse('http://test.zip'), responseError: const HttpException('')),
        FakeRequest(Uri.parse('http://test.zip')),
203
      ]),
204 205
      tempStorage: fileSystem.currentDirectory.childDirectory('temp')
        ..createSync(),
206
      allowedBaseUrls: <String>['http://test.zip'],
207 208 209 210
    );

    await artifactUpdater.downloadZipArchive(
      'test message',
211
      Uri.parse('http://test.zip'),
212 213 214 215 216 217
      fileSystem.currentDirectory.childDirectory('out'),
    );

    expect(fileSystem.file('out/test'), exists);
  });

218
  testWithoutContext('ArtifactUpdater will re-attempt on a non-200 response', () async {
219
    final FakeOperatingSystemUtils operatingSystemUtils = FakeOperatingSystemUtils();
220 221
    final MemoryFileSystem fileSystem = MemoryFileSystem.test();
    final BufferLogger logger = BufferLogger.test();
222

223 224 225 226 227
    final ArtifactUpdater artifactUpdater = ArtifactUpdater(
      fileSystem: fileSystem,
      logger: logger,
      operatingSystemUtils: operatingSystemUtils,
      platform: testPlatform,
228
      httpClient: FakeHttpClient.list(<FakeRequest>[
229 230
        FakeRequest(Uri.parse('http://test.zip'), response: const FakeResponse(statusCode: HttpStatus.preconditionFailed)),
        FakeRequest(Uri.parse('http://test.zip'), response: const FakeResponse(statusCode: HttpStatus.preconditionFailed)),
231
      ]),
232 233
      tempStorage: fileSystem.currentDirectory.childDirectory('temp')
        ..createSync(),
234
      allowedBaseUrls: <String>['http://test.zip'],
235 236
    );

237
    await expectLater(() async => artifactUpdater.downloadZipArchive(
238
      'test message',
239
      Uri.parse('http://test.zip'),
240 241 242 243 244 245 246 247
      fileSystem.currentDirectory.childDirectory('out'),
    ), throwsToolExit());

    expect(logger.statusText, contains('test message'));
    expect(fileSystem.file('out/test'), isNot(exists));
  });

  testWithoutContext('ArtifactUpdater will tool exit on an ArgumentError from http client with base url override', () async {
248
    final FakeOperatingSystemUtils operatingSystemUtils = FakeOperatingSystemUtils();
249 250 251 252 253 254 255 256
    final MemoryFileSystem fileSystem = MemoryFileSystem.test();
    final BufferLogger logger = BufferLogger.test();
    final ArtifactUpdater artifactUpdater = ArtifactUpdater(
      fileSystem: fileSystem,
      logger: logger,
      operatingSystemUtils: operatingSystemUtils,
      platform: FakePlatform(
        environment: <String, String>{
257
          'FLUTTER_STORAGE_BASE_URL': 'foo-bar',
258 259
        },
      ),
260
      httpClient: FakeHttpClient.list(<FakeRequest>[
261
        FakeRequest(Uri.parse('http://foo-bar/test.zip'), responseError: ArgumentError()),
262
      ]),
263 264
      tempStorage: fileSystem.currentDirectory.childDirectory('temp')
        ..createSync(),
265
      allowedBaseUrls: <String>['http://foo-bar/test.zip'],
266 267
    );

268
    await expectLater(() async => artifactUpdater.downloadZipArchive(
269
      'test message',
270
      Uri.parse('http://foo-bar/test.zip'),
271 272 273 274 275 276 277 278
      fileSystem.currentDirectory.childDirectory('out'),
    ), throwsToolExit());

    expect(logger.statusText, contains('test message'));
    expect(fileSystem.file('out/test'), isNot(exists));
  });

  testWithoutContext('ArtifactUpdater will rethrow on an ArgumentError from http client without base url override', () async {
279
    final FakeOperatingSystemUtils operatingSystemUtils = FakeOperatingSystemUtils();
280 281 282 283 284 285 286
    final MemoryFileSystem fileSystem = MemoryFileSystem.test();
    final BufferLogger logger = BufferLogger.test();
    final ArtifactUpdater artifactUpdater = ArtifactUpdater(
      fileSystem: fileSystem,
      logger: logger,
      operatingSystemUtils: operatingSystemUtils,
      platform: testPlatform,
287
      httpClient: FakeHttpClient.list(<FakeRequest>[
288
        FakeRequest(Uri.parse('http://test.zip'), responseError: ArgumentError()),
289
      ]),
290 291
      tempStorage: fileSystem.currentDirectory.childDirectory('temp')
        ..createSync(),
292
      allowedBaseUrls: <String>['http://test.zip'],
293 294
    );

295
    await expectLater(() async => artifactUpdater.downloadZipArchive(
296
      'test message',
297
      Uri.parse('http://test.zip'),
298
      fileSystem.currentDirectory.childDirectory('out'),
299
    ), throwsArgumentError);
300 301 302 303 304

    expect(logger.statusText, contains('test message'));
    expect(fileSystem.file('out/test'), isNot(exists));
  });

305
  testWithoutContext('ArtifactUpdater will re-download a file if unzipping fails', () async {
306
    final FakeOperatingSystemUtils operatingSystemUtils = FakeOperatingSystemUtils();
307 308 309 310 311 312
    final MemoryFileSystem fileSystem = MemoryFileSystem.test();
    final BufferLogger logger = BufferLogger.test();
    final ArtifactUpdater artifactUpdater = ArtifactUpdater(
      fileSystem: fileSystem,
      logger: logger,
      operatingSystemUtils: operatingSystemUtils,
313
      platform: testPlatform,
314
      httpClient: FakeHttpClient.any(),
315 316
      tempStorage: fileSystem.currentDirectory.childDirectory('temp')
        ..createSync(),
317
      allowedBaseUrls: <String>['http://test.zip'],
318 319 320 321 322
    );
    operatingSystemUtils.failures = 1;

    await artifactUpdater.downloadZipArchive(
      'test message',
323
      Uri.parse('http://test.zip'),
324 325 326 327 328 329
      fileSystem.currentDirectory.childDirectory('out'),
    );
    expect(logger.statusText, contains('test message'));
    expect(fileSystem.file('out/test'), exists);
  });

330
  testWithoutContext('ArtifactUpdater will de-download a file if unzipping fails on windows', () async {
331
    final FakeOperatingSystemUtils operatingSystemUtils = FakeOperatingSystemUtils(windows: true);
332 333 334 335 336 337 338
    final MemoryFileSystem fileSystem = MemoryFileSystem.test();
    final BufferLogger logger = BufferLogger.test();
    final ArtifactUpdater artifactUpdater = ArtifactUpdater(
      fileSystem: fileSystem,
      logger: logger,
      operatingSystemUtils: operatingSystemUtils,
      platform: testPlatform,
339
      httpClient: FakeHttpClient.any(),
340 341
      tempStorage: fileSystem.currentDirectory.childDirectory('temp')
        ..createSync(),
342
      allowedBaseUrls: <String>['http://test.zip'],
343 344 345 346 347
    );
    operatingSystemUtils.failures = 1;

    await artifactUpdater.downloadZipArchive(
      'test message',
348
      Uri.parse('http://test.zip'),
349 350 351 352 353 354
      fileSystem.currentDirectory.childDirectory('out'),
    );
    expect(logger.statusText, contains('test message'));
    expect(fileSystem.file('out/test'), exists);
  });

355
  testWithoutContext('ArtifactUpdater will bail with a tool exit if unzipping fails more than twice', () async {
356
    final FakeOperatingSystemUtils operatingSystemUtils = FakeOperatingSystemUtils();
357 358 359 360 361 362
    final MemoryFileSystem fileSystem = MemoryFileSystem.test();
    final BufferLogger logger = BufferLogger.test();
    final ArtifactUpdater artifactUpdater = ArtifactUpdater(
      fileSystem: fileSystem,
      logger: logger,
      operatingSystemUtils: operatingSystemUtils,
363
      platform: testPlatform,
364
      httpClient: FakeHttpClient.any(),
365 366
      tempStorage: fileSystem.currentDirectory.childDirectory('temp')
        ..createSync(),
367
      allowedBaseUrls: <String>['http://test.zip'],
368 369 370 371 372
    );
    operatingSystemUtils.failures = 2;

    expect(artifactUpdater.downloadZipArchive(
      'test message',
373
      Uri.parse('http://test.zip'),
374
      fileSystem.currentDirectory.childDirectory('out'),
375
    ), throwsToolExit());
376 377 378 379
    expect(fileSystem.file('te,[/test'), isNot(exists));
    expect(fileSystem.file('out/test'), isNot(exists));
  });

380
  testWithoutContext('ArtifactUpdater will bail if unzipping fails more than twice on Windows', () async {
381
    final FakeOperatingSystemUtils operatingSystemUtils = FakeOperatingSystemUtils(windows: true);
382 383 384 385 386 387 388
    final MemoryFileSystem fileSystem = MemoryFileSystem.test();
    final BufferLogger logger = BufferLogger.test();
    final ArtifactUpdater artifactUpdater = ArtifactUpdater(
      fileSystem: fileSystem,
      logger: logger,
      operatingSystemUtils: operatingSystemUtils,
      platform: testPlatform,
389
      httpClient: FakeHttpClient.any(),
390 391
      tempStorage: fileSystem.currentDirectory.childDirectory('temp')
        ..createSync(),
392
      allowedBaseUrls: <String>['http://test.zip'],
393 394 395 396 397
    );
    operatingSystemUtils.failures = 2;

    expect(artifactUpdater.downloadZipArchive(
      'test message',
398
      Uri.parse('http://test.zip'),
399
      fileSystem.currentDirectory.childDirectory('out'),
400
    ), throwsToolExit());
401 402 403 404
    expect(fileSystem.file('te,[/test'), isNot(exists));
    expect(fileSystem.file('out/test'), isNot(exists));
  });

405
  testWithoutContext('ArtifactUpdater can download a tar archive', () async {
406
    final FakeOperatingSystemUtils operatingSystemUtils = FakeOperatingSystemUtils();
407 408 409 410 411 412
    final MemoryFileSystem fileSystem = MemoryFileSystem.test();
    final BufferLogger logger = BufferLogger.test();
    final ArtifactUpdater artifactUpdater = ArtifactUpdater(
      fileSystem: fileSystem,
      logger: logger,
      operatingSystemUtils: operatingSystemUtils,
413
      platform: testPlatform,
414
      httpClient: FakeHttpClient.any(),
415 416
      tempStorage: fileSystem.currentDirectory.childDirectory('temp')
        ..createSync(),
417
      allowedBaseUrls: <String>['http://test.zip'],
418 419 420 421
    );

    await artifactUpdater.downloadZippedTarball(
      'test message',
422
      Uri.parse('http://test.zip'),
423 424 425 426 427 428
      fileSystem.currentDirectory.childDirectory('out'),
    );
    expect(fileSystem.file('out/test'), exists);
  });

  testWithoutContext('ArtifactUpdater will delete downloaded files if they exist.', () async {
429
    final FakeOperatingSystemUtils operatingSystemUtils = FakeOperatingSystemUtils();
430 431 432 433 434 435
    final MemoryFileSystem fileSystem = MemoryFileSystem.test();
    final BufferLogger logger = BufferLogger.test();
    final ArtifactUpdater artifactUpdater = ArtifactUpdater(
      fileSystem: fileSystem,
      logger: logger,
      operatingSystemUtils: operatingSystemUtils,
436
      platform: testPlatform,
437
      httpClient: FakeHttpClient.any(),
438 439
      tempStorage: fileSystem.currentDirectory.childDirectory('temp')
        ..createSync(),
440
      allowedBaseUrls: <String>['http://test.zip'],
441 442 443 444 445 446 447 448 449 450 451 452
    );

    artifactUpdater.downloadedFiles.addAll(<File>[
      fileSystem.file('a/b/c/d')..createSync(recursive: true),
      fileSystem.file('d/e/f'),
    ]);

    artifactUpdater.removeDownloadedFiles();

    expect(fileSystem.file('a/b/c/d'), isNot(exists));
    expect(logger.errorText, isEmpty);
  });
453 454 455 456 457 458 459 460 461 462 463 464 465 466 467

  testWithoutContext('ArtifactUpdater will tool exit if deleting the existing artifacts fails with 32 on windows', () async {
    const int kSharingViolation = 32;
    final FileExceptionHandler handler = FileExceptionHandler();
    final FakeOperatingSystemUtils operatingSystemUtils = FakeOperatingSystemUtils();
    final MemoryFileSystem fileSystem = MemoryFileSystem.test(opHandle: handler.opHandle);
    final BufferLogger logger = BufferLogger.test();
    final ArtifactUpdater artifactUpdater = ArtifactUpdater(
      fileSystem: fileSystem,
      logger: logger,
      operatingSystemUtils: operatingSystemUtils,
      platform: FakePlatform(operatingSystem: 'windows'),
      httpClient: FakeHttpClient.any(),
      tempStorage: fileSystem.currentDirectory.childDirectory('temp')
        ..createSync(),
468
      allowedBaseUrls: <String>['http://test.zip'],
469 470 471 472 473 474 475 476
    );

    final Directory errorDirectory = fileSystem.currentDirectory
      .childDirectory('out')
      .childDirectory('test')
      ..createSync(recursive: true);
    handler.addError(errorDirectory, FileSystemOp.delete, const FileSystemException('', '', OSError('', kSharingViolation)));

477
    await expectLater(() async => artifactUpdater.downloadZippedTarball(
478 479 480 481 482 483 484 485
      'test message',
      Uri.parse('http://test.zip'),
      fileSystem.currentDirectory.childDirectory('out'),
    ), throwsToolExit(
      message: 'Failed to delete /out/test because the local file/directory is in use by another process'
    ));
    expect(fileSystem.file('out/test'), isNot(exists));
  });
486 487
}

488 489
class FakeOperatingSystemUtils extends Fake implements OperatingSystemUtils {
  FakeOperatingSystemUtils({this.windows = false});
490

491
  int failures = 0;
492
  final bool windows;
493 494 495 496 497

  @override
  void unzip(File file, Directory targetDirectory) {
    if (failures > 0) {
      failures -= 1;
498
      throw Exception();
499 500 501 502 503 504 505 506 507
    }
    targetDirectory.childFile(file.fileSystem.path.basenameWithoutExtension(file.path))
      .createSync();
  }

  @override
  void unpack(File gzippedTarFile, Directory targetDirectory) {
    if (failures > 0) {
      failures -= 1;
508
      throw Exception();
509 510 511 512 513
    }
    targetDirectory.childFile(gzippedTarFile.fileSystem.path.basenameWithoutExtension(gzippedTarFile.path))
      .createSync();
  }
}