flutter_goldens_test.dart 21.9 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
// @dart = 2.8
6

7 8 9
import 'dart:async';
import 'dart:core';
import 'dart:io';
10 11 12 13 14 15 16 17 18 19
import 'dart:typed_data';

import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_goldens/flutter_goldens.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';

20 21
import 'json_templates.dart';

22
const String _kFlutterRoot = '/flutter';
23 24 25 26 27 28 29 30 31 32 33 34 35 36

// 1x1 transparent pixel
const List<int> _kTestPngBytes =
<int>[137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0,
  1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 11, 73, 68, 65, 84,
  120, 1, 99, 97, 0, 2, 0, 0, 25, 0, 5, 144, 240, 54, 245, 0, 0, 0, 0, 73, 69,
  78, 68, 174, 66, 96, 130];

// 1x1 colored pixel
const List<int> _kFailPngBytes =
<int>[137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0,
  1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 13, 73, 68, 65, 84,
  120, 1, 99, 249, 207, 240, 255, 63, 0, 7, 18, 3, 2, 164, 147, 160, 197, 0,
  0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130];
37

38 39 40 41 42 43 44 45 46 47 48
Future<void> testWithOutput(String name, Future<void> body(), String expectedOutput) async {
  test(name, () async {
    final StringBuffer output = StringBuffer();
    void _recordPrint(Zone self, ZoneDelegate parent, Zone zone, String line) {
      output.write(line);
    }
    await runZoned<Future<void>>(body, zoneSpecification: ZoneSpecification(print: _recordPrint));
    expect(output.toString(), expectedOutput);
  });
}

49 50 51 52
void main() {
  MemoryFileSystem fs;
  FakePlatform platform;
  MockProcessManager process;
53
  MockHttpClient mockHttpClient;
54

55
  setUp(() {
56
    fs = MemoryFileSystem();
57 58 59 60
    platform = FakePlatform(
      environment: <String, String>{'FLUTTER_ROOT': _kFlutterRoot},
      operatingSystem: 'macos'
    );
61
    process = MockProcessManager();
62
    mockHttpClient = MockHttpClient();
63
    fs.directory(_kFlutterRoot).createSync(recursive: true);
64 65
  });

66 67
  group('SkiaGoldClient', () {
    SkiaGoldClient skiaClient;
68
    Directory workDirectory;
69 70

    setUp(() {
71
      workDirectory = fs.directory('/workDirectory')
72 73 74
        ..createSync(recursive: true);
      skiaClient = SkiaGoldClient(
        workDirectory,
75
        fs: fs,
76
        process: process,
77
        platform: platform,
78
        httpClient: mockHttpClient,
79 80 81
      );
    });

82
    test('auth performs minimal work if already authorized', () async {
83
      final File authFile = fs.file('/workDirectory/temp/auth_opt.json')
84
        ..createSync(recursive: true);
85
      authFile.writeAsStringSync(authTemplate());
86 87 88 89 90 91 92 93 94 95 96
      when(process.run(any))
        .thenAnswer((_) => Future<ProcessResult>
        .value(ProcessResult(123, 0, '', '')));
      await skiaClient.auth();

      verifyNever(process.run(
        captureAny,
        workingDirectory: captureAnyNamed('workingDirectory'),
      ));
    });

97 98 99 100 101 102 103 104 105 106
    test('gsutil is checked when authorization file is present', () async {
      final File authFile = fs.file('/workDirectory/temp/auth_opt.json')
        ..createSync(recursive: true);
      authFile.writeAsStringSync(authTemplate(gsutil: true));
      expect(
        await skiaClient.clientIsAuthorized(),
        isFalse,
      );
    });

107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
    test('throws for error state from auth', () async {
      platform = FakePlatform(
        environment: <String, String>{
          'FLUTTER_ROOT': _kFlutterRoot,
          'GOLD_SERVICE_ACCOUNT' : 'Service Account',
          'GOLDCTL' : 'goldctl',
        },
        operatingSystem: 'macos'
      );

      skiaClient = SkiaGoldClient(
        workDirectory,
        fs: fs,
        process: process,
        platform: platform,
        httpClient: mockHttpClient,
      );

      when(process.run(any))
        .thenAnswer((_) => Future<ProcessResult>
        .value(ProcessResult(123, 1, 'fail', 'fail')));
      final Future<void> test = skiaClient.auth();

      expect(
        test,
        throwsException,
      );
    });

136
    test('throws for error state from init', () {
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
      platform = FakePlatform(
        environment: <String, String>{
          'FLUTTER_ROOT': _kFlutterRoot,
          'GOLDCTL' : 'goldctl',
        },
        operatingSystem: 'macos'
      );

      skiaClient = SkiaGoldClient(
        workDirectory,
        fs: fs,
        process: process,
        platform: platform,
        httpClient: mockHttpClient,
      );

      when(process.run(
        <String>['git', 'rev-parse', 'HEAD'],
        workingDirectory: '/flutter',
      )).thenAnswer((_) => Future<ProcessResult>
        .value(ProcessResult(12345678, 0, '12345678', '')));

      when(process.run(
        <String>[
          'goldctl',
          'imgtest', 'init',
          '--instance', 'flutter',
          '--work-dir', '/workDirectory/temp',
          '--commit', '12345678',
          '--keys-file', '/workDirectory/keys.json',
          '--failure-file', '/workDirectory/failures.json',
          '--passfail',
        ],
      )).thenAnswer((_) => Future<ProcessResult>
        .value(ProcessResult(123, 1, 'fail', 'fail')));
      final Future<void> test =  skiaClient.imgtestInit();

      expect(
        test,
        throwsException,
      );
178 179
    });

180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
    test('correctly inits tryjob for luci', () async {
      platform = FakePlatform(
        environment: <String, String>{
          'FLUTTER_ROOT': _kFlutterRoot,
          'GOLDCTL' : 'goldctl',
          'SWARMING_TASK_ID' : '4ae997b50dfd4d11',
          'LOGDOG_STREAM_PREFIX' : 'buildbucket/cr-buildbucket.appspot.com/8885996262141582672',
          'GOLD_TRYJOB' : 'refs/pull/49815/head',
        },
        operatingSystem: 'macos'
      );

      skiaClient = SkiaGoldClient(
        workDirectory,
        fs: fs,
        process: process,
        platform: platform,
        httpClient: mockHttpClient,
      );

      final List<String> ciArguments = skiaClient.getCIArguments();

      expect(
        ciArguments,
        equals(
          <String>[
            '--changelist', '49815',
            '--cis', 'buildbucket',
            '--jobid', '8885996262141582672',
          ],
        ),
      );
    });

214 215 216 217 218 219 220 221 222 223 224 225
    test('Creates traceID correctly', () {
      String traceID;
      platform = FakePlatform(
        environment: <String, String>{
          'FLUTTER_ROOT': _kFlutterRoot,
          'GOLDCTL' : 'goldctl',
          'SWARMING_TASK_ID' : '4ae997b50dfd4d11',
          'LOGDOG_STREAM_PREFIX' : 'buildbucket/cr-buildbucket.appspot.com/8885996262141582672',
          'GOLD_TRYJOB' : 'refs/pull/49815/head',
        },
        operatingSystem: 'linux'
      );
226

227 228 229 230 231 232 233
      skiaClient = SkiaGoldClient(
        workDirectory,
        fs: fs,
        process: process,
        platform: platform,
        httpClient: mockHttpClient,
      );
234

235
      traceID = skiaClient.getTraceID('flutter.golden.1');
236

237 238 239 240
      expect(
        traceID,
        equals(',CI=luci,Platform=linux,name=flutter.golden.1,source_type=flutter,'),
      );
241

242 243 244 245 246 247 248 249 250 251 252 253
      // Browser
      platform = FakePlatform(
        environment: <String, String>{
          'FLUTTER_ROOT': _kFlutterRoot,
          'GOLDCTL' : 'goldctl',
          'SWARMING_TASK_ID' : '4ae997b50dfd4d11',
          'LOGDOG_STREAM_PREFIX' : 'buildbucket/cr-buildbucket.appspot.com/8885996262141582672',
          'GOLD_TRYJOB' : 'refs/pull/49815/head',
          'FLUTTER_TEST_BROWSER' : 'chrome',
        },
        operatingSystem: 'linux'
      );
254

255 256 257 258 259 260 261 262 263 264 265 266 267 268
      skiaClient = SkiaGoldClient(
        workDirectory,
        fs: fs,
        process: process,
        platform: platform,
        httpClient: mockHttpClient,
      );

      traceID = skiaClient.getTraceID('flutter.golden.1');

      expect(
        traceID,
        equals(',Browser=chrome,CI=luci,Platform=linux,name=flutter.golden.1,source_type=flutter,'),
      );
269

270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
      // Locally - should defer to luci traceID
      platform = FakePlatform(
        environment: <String, String>{
          'FLUTTER_ROOT': _kFlutterRoot,
        },
        operatingSystem: 'macos'
      );

      skiaClient = SkiaGoldClient(
        workDirectory,
        fs: fs,
        process: process,
        platform: platform,
        httpClient: mockHttpClient,
      );

      traceID = skiaClient.getTraceID('flutter.golden.1');

      expect(
        traceID,
        equals(',CI=luci,Platform=macos,name=flutter.golden.1,source_type=flutter,'),
      );
    });

    group('Request Handling', () {
      String expectation;

      setUp(() {
        expectation = '55109a4bed52acc780530f7a9aeff6c0';
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
      });

      test('image bytes are processed properly', () async {
        final Uri imageUrl = Uri.parse(
          'https://flutter-gold.skia.org/img/images/$expectation.png'
        );
        final MockHttpClientRequest mockImageRequest = MockHttpClientRequest();
        final MockHttpImageResponse mockImageResponse = MockHttpImageResponse(
          imageResponseTemplate()
        );
        when(mockHttpClient.getUrl(imageUrl))
          .thenAnswer((_) => Future<MockHttpClientRequest>.value(mockImageRequest));
        when(mockImageRequest.close())
          .thenAnswer((_) => Future<MockHttpImageResponse>.value(mockImageResponse));

        final List<int> masterBytes = await skiaClient.getImageBytes(expectation);

        expect(masterBytes, equals(_kTestPngBytes));
      });
318 319
    });
  });
320

321
  group('FlutterGoldenFileComparator', () {
322
    FlutterPostSubmitFileComparator comparator;
323 324

    setUp(() {
325 326
      final Directory basedir = fs.directory('flutter/test/library/')
        ..createSync(recursive: true);
327
      comparator = FlutterPostSubmitFileComparator(
328 329
        basedir.uri,
        MockSkiaGoldClient(),
330 331
        fs: fs,
        platform: platform,
332 333 334
      );
    });

335
    test('calculates the basedir correctly from defaultComparator for local testing', () async {
336 337 338 339 340 341 342 343
      final MockLocalFileComparator defaultComparator = MockLocalFileComparator();
      final Directory flutterRoot = fs.directory(platform.environment['FLUTTER_ROOT'])
        ..createSync(recursive: true);
      when(defaultComparator.basedir).thenReturn(flutterRoot.childDirectory('baz').uri);

      final Directory basedir = FlutterGoldenFileComparator.getBaseDirectory(
        defaultComparator,
        platform,
344
        local: true,
345 346 347 348 349 350 351 352 353 354 355
      );
      expect(
        basedir.uri,
        fs.directory('/flutter/bin/cache/pkg/skia_goldens/baz').uri,
      );
    });

    test('ignores version number', () {
      final Uri key = comparator.getTestUri(Uri.parse('foo.png'), 1);
      expect(key, Uri.parse('foo.png'));
    });
356

357 358 359 360 361
    group('Post-Submit', () {
      final MockSkiaGoldClient mockSkiaClient = MockSkiaGoldClient();

      setUp(() {
        final Directory basedir = fs.directory('flutter/test/library/')
362
          ..createSync(recursive: true);
363
        comparator = FlutterPostSubmitFileComparator(
364 365 366 367 368
          basedir.uri,
          mockSkiaClient,
          fs: fs,
          platform: platform,
        );
369
      });
370

371
      group('correctly determines testing environment', () {
372
        test('returns true for configured Luci', () {
373 374 375 376 377 378 379 380 381 382 383 384 385 386
          platform = FakePlatform(
            environment: <String, String>{
              'FLUTTER_ROOT': _kFlutterRoot,
              'SWARMING_TASK_ID' : '12345678990',
              'GOLDCTL' : 'goldctl',
            },
            operatingSystem: 'macos'
          );
          expect(
            FlutterPostSubmitFileComparator.isAvailableForEnvironment(platform),
            isTrue,
          );
        });

387
        test('returns false - GOLDCTL not present', () {
388 389 390
          platform = FakePlatform(
            environment: <String, String>{
              'FLUTTER_ROOT': _kFlutterRoot,
391
              'SWARMING_TASK_ID' : '12345678990',
392 393 394 395
            },
            operatingSystem: 'macos'
          );
          expect(
396
            FlutterPostSubmitFileComparator.isAvailableForEnvironment(platform),
397 398 399 400
            isFalse,
          );
        });

401
        test('returns false - GOLD_TRYJOB active', () {
402 403 404
          platform = FakePlatform(
            environment: <String, String>{
              'FLUTTER_ROOT': _kFlutterRoot,
405 406 407
              'SWARMING_TASK_ID' : '12345678990',
              'GOLDCTL' : 'goldctl',
              'GOLD_TRYJOB' : 'git/ref/12345/head'
408 409 410 411
            },
            operatingSystem: 'macos'
          );
          expect(
412
            FlutterPostSubmitFileComparator.isAvailableForEnvironment(platform),
413 414 415 416
            isFalse,
          );
        });

417
        test('returns false - on Cirrus', () {
418 419 420 421 422
          platform = FakePlatform(
            environment: <String, String>{
              'FLUTTER_ROOT': _kFlutterRoot,
              'CIRRUS_CI': 'true',
              'CIRRUS_PR': '',
423
              'CIRRUS_BRANCH': 'master',
424 425 426 427 428
              'GOLD_SERVICE_ACCOUNT': 'service account...'
            },
            operatingSystem: 'macos'
          );
          expect(
429
            FlutterPostSubmitFileComparator.isAvailableForEnvironment(platform),
430 431 432
            isFalse,
          );
        });
433
      });
434
    });
435

436
    group('Pre-Submit', () {
437
      group('correctly determines testing environment', () {
438
        test('returns true for Luci', () {
439 440 441
          platform = FakePlatform(
            environment: <String, String>{
              'FLUTTER_ROOT': _kFlutterRoot,
442 443 444
              'SWARMING_TASK_ID' : '12345678990',
              'GOLDCTL' : 'goldctl',
              'GOLD_TRYJOB' : 'git/ref/12345/head'
445 446 447 448 449 450 451 452
            },
            operatingSystem: 'macos'
          );
          expect(
            FlutterPreSubmitFileComparator.isAvailableForEnvironment(platform),
            isTrue,
          );
        });
453

454
        test('returns false - not on Luci', () {
455 456 457 458 459 460 461 462
          platform = FakePlatform(
            environment: <String, String>{
              'FLUTTER_ROOT': _kFlutterRoot,
            },
            operatingSystem: 'macos'
          );
          expect(
            FlutterPreSubmitFileComparator.isAvailableForEnvironment(platform),
463
            isFalse,
464 465 466
          );
        });

467
        test('returns false - GOLDCTL missing', () {
468 469 470
          platform = FakePlatform(
            environment: <String, String>{
              'FLUTTER_ROOT': _kFlutterRoot,
471 472
              'SWARMING_TASK_ID' : '12345678990',
              'GOLD_TRYJOB' : 'git/ref/12345/head'
473 474 475 476 477 478 479 480 481
            },
            operatingSystem: 'macos'
          );
          expect(
            FlutterPreSubmitFileComparator.isAvailableForEnvironment(platform),
            isFalse,
          );
        });

482
        test('returns false - GOLD_TRYJOB missing', () {
483 484 485
          platform = FakePlatform(
            environment: <String, String>{
              'FLUTTER_ROOT': _kFlutterRoot,
486 487
              'SWARMING_TASK_ID' : '12345678990',
              'GOLDCTL' : 'goldctl',
488 489 490 491 492 493 494 495 496
            },
            operatingSystem: 'macos'
          );
          expect(
            FlutterPreSubmitFileComparator.isAvailableForEnvironment(platform),
            isFalse,
          );
        });

497
        test('returns false - on Cirrus', () {
498 499 500
          platform = FakePlatform(
            environment: <String, String>{
              'FLUTTER_ROOT': _kFlutterRoot,
501 502 503 504
              'CIRRUS_CI': 'true',
              'CIRRUS_PR': '',
              'CIRRUS_BRANCH': 'master',
              'GOLD_SERVICE_ACCOUNT': 'service account...'
505 506 507 508
            },
            operatingSystem: 'macos'
          );
          expect(
509
            FlutterPostSubmitFileComparator.isAvailableForEnvironment(platform),
510 511 512
            isFalse,
          );
        });
513
      });
514
    });
515

516 517 518 519 520 521 522 523 524
    group('Skipping', () {
      group('correctly determines testing environment', () {
        test('returns true on Cirrus builds', () {
          platform = FakePlatform(
            environment: <String, String>{
              'FLUTTER_ROOT': _kFlutterRoot,
              'CIRRUS_CI' : 'yep',
            },
            operatingSystem: 'macos'
525
          );
526
          expect(
527
            FlutterSkippingFileComparator.isAvailableForEnvironment(platform),
528
            isTrue,
529
          );
530 531
        });

532
        test('returns true on irrelevant LUCI builds', () {
533 534 535
          platform = FakePlatform(
            environment: <String, String>{
              'FLUTTER_ROOT': _kFlutterRoot,
536
              'SWARMING_TASK_ID' : '1234567890',
537 538 539 540
            },
            operatingSystem: 'macos'
          );
          expect(
541
            FlutterSkippingFileComparator.isAvailableForEnvironment(platform),
542 543 544
            isTrue,
          );
        });
545

546 547 548 549 550 551 552 553
        test('returns false - no CI', () {
          platform = FakePlatform(
            environment: <String, String>{
              'FLUTTER_ROOT': _kFlutterRoot,
            },
            operatingSystem: 'macos'
          );
          expect(
554
            FlutterSkippingFileComparator.isAvailableForEnvironment(
555 556 557 558
              platform),
            isFalse,
          );
        });
559 560 561
      });
    });

562 563 564
    group('Local', () {
      FlutterLocalFileComparator comparator;
      final MockSkiaGoldClient mockSkiaClient = MockSkiaGoldClient();
565

566 567 568 569 570 571 572 573 574 575 576 577 578
      setUp(() async {
        final Directory basedir = fs.directory('flutter/test/library/')
          ..createSync(recursive: true);
        comparator = FlutterLocalFileComparator(
          basedir.uri,
          mockSkiaClient,
          fs: fs,
          platform: FakePlatform(
            environment: <String, String>{'FLUTTER_ROOT': _kFlutterRoot},
            operatingSystem: 'macos'
          ),
        );

579 580
        when(mockSkiaClient.getExpectationForTest('flutter.golden_test.1'))
          .thenAnswer((_) => Future<String>.value('55109a4bed52acc780530f7a9aeff6c0'));
581 582
        when(mockSkiaClient.getExpectationForTest('flutter.new_golden_test.2'))
          .thenAnswer((_) => Future<String>.value(''));
583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598
        when(mockSkiaClient.getImageBytes('55109a4bed52acc780530f7a9aeff6c0'))
          .thenAnswer((_) => Future<List<int>>.value(_kTestPngBytes));
        when(mockSkiaClient.cleanTestName('library.flutter.golden_test.1.png'))
          .thenReturn('flutter.golden_test.1');
      });

      test('passes when bytes match', () async {
        expect(
          await comparator.compare(
            Uint8List.fromList(_kTestPngBytes),
            Uri.parse('flutter.golden_test.1.png'),
          ),
          isTrue,
        );
      });

599
      testWithOutput('passes non-existent baseline for new test, null expectation', () async {
600 601 602 603 604 605 606
        expect(
          await comparator.compare(
            Uint8List.fromList(_kFailPngBytes),
            Uri.parse('flutter.new_golden_test.1'),
          ),
          isTrue,
        );
607 608 609 610
      }, 'No expectations provided by Skia Gold for test: library.flutter.new_golden_test.1. '
         'This may be a new test. If this is an unexpected result, check https://flutter-gold.skia.org.\n'
         'Validate image output found at flutter/test/library/'
      );
611

612 613 614 615 616 617 618 619 620 621 622 623 624
      testWithOutput('passes non-existent baseline for new test, empty expectation', () async {
        expect(
          await comparator.compare(
            Uint8List.fromList(_kFailPngBytes),
            Uri.parse('flutter.new_golden_test.2'),
          ),
          isTrue,
        );
      }, 'No expectations provided by Skia Gold for test: library.flutter.new_golden_test.2. '
        'This may be a new test. If this is an unexpected result, check https://flutter-gold.skia.org.\n'
        'Validate image output found at flutter/test/library/'
      );

625 626 627 628 629 630 631 632 633 634 635 636 637 638 639
      test('compare properly awaits validation & output before failing.', () async {
        final Completer<bool> completer = Completer<bool>();
        final Future<bool> result = comparator.compare(
          Uint8List.fromList(_kFailPngBytes),
          Uri.parse('flutter.golden_test.1.png'),
        );
        bool shouldThrow = true;
        result.then((_) {
          if (shouldThrow)
            fail('Compare completed before validation completed!');
        });
        await Future<void>.value();
        shouldThrow = false;
        completer.complete(Future<bool>.value(false));
      });
640 641 642 643 644

      test('returns FlutterSkippingGoldenFileComparator when network connection is unavailable', () async {
        final MockDirectory mockDirectory = MockDirectory();
        when(mockDirectory.existsSync()).thenReturn(true);
        when(mockDirectory.uri).thenReturn(Uri.parse('/flutter'));
645

646
        when(mockSkiaClient.getExpectationForTest(any))
647
          .thenAnswer((_) => throw const OSError("Can't reach Gold"));
648 649 650 651 652
        FlutterGoldenFileComparator comparator = await FlutterLocalFileComparator.fromDefaultComparator(
          platform,
          goldens: mockSkiaClient,
          baseDirectory: mockDirectory,
        );
653
        expect(comparator.runtimeType, FlutterSkippingFileComparator);
654

655
        when(mockSkiaClient.getExpectationForTest(any))
656
          .thenAnswer((_) => throw const SocketException("Can't reach Gold"));
657
        comparator = await FlutterLocalFileComparator.fromDefaultComparator(
658 659 660 661
          platform,
          goldens: mockSkiaClient,
          baseDirectory: mockDirectory,
        );
662
        expect(comparator.runtimeType, FlutterSkippingFileComparator);
663
      });
664
    });
665 666 667 668
  });
}

class MockProcessManager extends Mock implements ProcessManager {}
669

670
class MockSkiaGoldClient extends Mock implements SkiaGoldClient {}
671

672
class MockLocalFileComparator extends Mock implements LocalFileComparator {}
673

674 675
class MockDirectory extends Mock implements Directory {}

676 677 678 679 680 681 682
class MockHttpClient extends Mock implements HttpClient {}

class MockHttpClientRequest extends Mock implements HttpClientRequest {}

class MockHttpClientResponse extends Mock implements HttpClientResponse {
  MockHttpClientResponse(this.response);

683
  final List<int> response;
684 685

  @override
686 687
  StreamSubscription<List<int>> listen(
    void onData(List<int> event), {
688 689 690 691
      Function onError,
      void onDone(),
      bool cancelOnError,
    }) {
692
    return Stream<List<int>>.fromFuture(Future<List<int>>.value(response))
693 694 695 696 697 698 699 700 701 702 703 704 705 706
      .listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
  }
}

class MockHttpImageResponse extends Mock implements HttpClientResponse {
  MockHttpImageResponse(this.response);

  final List<List<int>> response;

  @override
  Future<void> forEach(void action(List<int> element)) async {
    response.forEach(action);
  }
}