// 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/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/bot_detector.dart'; import 'package:flutter_tools/src/base/common.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/platform.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/dart/pub.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:mockito/mockito.dart'; import 'package:process/process.dart'; import 'package:fake_async/fake_async.dart'; import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/mocks.dart' as mocks; void main() { setUpAll(() { Cache.flutterRoot = ''; }); tearDown(() { MockDirectory.findCache = false; }); testWithoutContext('checkUpToDate skips pub get if the package config is newer than the pubspec ' 'and the current framework version is the same as the last version', () async { final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[]); final BufferLogger logger = BufferLogger.test(); final MemoryFileSystem fileSystem = MemoryFileSystem.test(); fileSystem.file('pubspec.yaml').createSync(); fileSystem.file('pubspec.lock').createSync(); fileSystem.file('.dart_tool/package_config.json').createSync(recursive: true); fileSystem.file('.dart_tool/version').writeAsStringSync('a'); fileSystem.file('version').writeAsStringSync('a'); final Pub pub = Pub( fileSystem: fileSystem, logger: logger, processManager: processManager, usage: MockUsage(), platform: FakePlatform( environment: const <String, String>{}, ), botDetector: const BotDetectorAlwaysNo(), ); await pub.get( context: PubContext.pubGet, checkUpToDate: true, ); expect(logger.traceText, contains('Skipping pub get: version match.')); }); testWithoutContext('checkUpToDate does not skip pub get if the package config is newer than the pubspec ' 'but the current framework version is not the same as the last version', () async { final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ const FakeCommand(command: <String>[ 'bin/cache/dart-sdk/bin/pub', '--verbosity=warning', 'get', '--no-precompile', ]) ]); final BufferLogger logger = BufferLogger.test(); final MemoryFileSystem fileSystem = MemoryFileSystem.test(); fileSystem.file('pubspec.yaml').createSync(); fileSystem.file('pubspec.lock').createSync(); fileSystem.file('.dart_tool/package_config.json').createSync(recursive: true); fileSystem.file('.dart_tool/version').writeAsStringSync('a'); fileSystem.file('version').writeAsStringSync('b'); final Pub pub = Pub( fileSystem: fileSystem, logger: logger, processManager: processManager, usage: MockUsage(), platform: FakePlatform( environment: const <String, String>{}, ), botDetector: const BotDetectorAlwaysNo(), ); await pub.get( context: PubContext.pubGet, checkUpToDate: true, ); expect(processManager.hasRemainingExpectations, false); expect(fileSystem.file('.dart_tool/version').readAsStringSync(), 'b'); }); testWithoutContext('checkUpToDate does not skip pub get if the package config is newer than the pubspec ' 'but the current framework version does not exist yet', () async { final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ const FakeCommand(command: <String>[ 'bin/cache/dart-sdk/bin/pub', '--verbosity=warning', 'get', '--no-precompile', ]) ]); final BufferLogger logger = BufferLogger.test(); final MemoryFileSystem fileSystem = MemoryFileSystem.test(); fileSystem.file('pubspec.yaml').createSync(); fileSystem.file('pubspec.lock').createSync(); fileSystem.file('.dart_tool/package_config.json').createSync(recursive: true); fileSystem.file('version').writeAsStringSync('b'); final Pub pub = Pub( fileSystem: fileSystem, logger: logger, processManager: processManager, usage: MockUsage(), platform: FakePlatform( environment: const <String, String>{}, ), botDetector: const BotDetectorAlwaysNo(), ); await pub.get( context: PubContext.pubGet, checkUpToDate: true, ); expect(processManager.hasRemainingExpectations, false); expect(fileSystem.file('.dart_tool/version').readAsStringSync(), 'b'); }); testWithoutContext('checkUpToDate does not skip pub get if the package config does not exist', () async { final MemoryFileSystem fileSystem = MemoryFileSystem.test(); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ FakeCommand(command: const <String>[ 'bin/cache/dart-sdk/bin/pub', '--verbosity=warning', 'get', '--no-precompile', ], onRun: () { fileSystem.file('.dart_tool/package_config.json').createSync(recursive: true); }) ]); final BufferLogger logger = BufferLogger.test(); fileSystem.file('pubspec.yaml').createSync(); fileSystem.file('pubspec.lock').createSync(); fileSystem.file('version').writeAsStringSync('b'); final Pub pub = Pub( fileSystem: fileSystem, logger: logger, processManager: processManager, usage: MockUsage(), platform: FakePlatform( environment: const <String, String>{}, ), botDetector: const BotDetectorAlwaysNo(), ); await pub.get( context: PubContext.pubGet, checkUpToDate: true, ); expect(processManager.hasRemainingExpectations, false); expect(fileSystem.file('.dart_tool/version').readAsStringSync(), 'b'); }); testWithoutContext('checkUpToDate does not skip pub get if the pubspec.lock does not exist', () async { final MemoryFileSystem fileSystem = MemoryFileSystem.test(); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ const FakeCommand(command: <String>[ 'bin/cache/dart-sdk/bin/pub', '--verbosity=warning', 'get', '--no-precompile', ]), ]); final BufferLogger logger = BufferLogger.test(); fileSystem.file('pubspec.yaml').createSync(); fileSystem.file('version').writeAsStringSync('b'); fileSystem.file('.dart_tool/package_config.json').createSync(recursive: true); fileSystem.file('.dart_tool/version').writeAsStringSync('b'); final Pub pub = Pub( fileSystem: fileSystem, logger: logger, processManager: processManager, usage: MockUsage(), platform: FakePlatform( environment: const <String, String>{}, ), botDetector: const BotDetectorAlwaysNo(), ); await pub.get( context: PubContext.pubGet, checkUpToDate: true, ); expect(processManager.hasRemainingExpectations, false); expect(fileSystem.file('.dart_tool/version').readAsStringSync(), 'b'); }); testWithoutContext('checkUpToDate does not skip pub get if the package config is older that the pubspec', () async { final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ const FakeCommand(command: <String>[ 'bin/cache/dart-sdk/bin/pub', '--verbosity=warning', 'get', '--no-precompile', ]) ]); final BufferLogger logger = BufferLogger.test(); final MemoryFileSystem fileSystem = MemoryFileSystem.test(); fileSystem.file('pubspec.yaml').createSync(); fileSystem.file('pubspec.lock').createSync(); fileSystem.file('.dart_tool/package_config.json') ..createSync(recursive: true) ..setLastModifiedSync(DateTime(1991)); fileSystem.file('version').writeAsStringSync('b'); final Pub pub = Pub( fileSystem: fileSystem, logger: logger, processManager: processManager, usage: MockUsage(), platform: FakePlatform( environment: const <String, String>{}, ), botDetector: const BotDetectorAlwaysNo(), ); await pub.get( context: PubContext.pubGet, checkUpToDate: true, ); expect(processManager.hasRemainingExpectations, false); expect(fileSystem.file('.dart_tool/version').readAsStringSync(), 'b'); }); testWithoutContext('checkUpToDate does not skip pub get if the pubspec.lock is older that the pubspec', () async { final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ const FakeCommand(command: <String>[ 'bin/cache/dart-sdk/bin/pub', '--verbosity=warning', 'get', '--no-precompile', ]) ]); final BufferLogger logger = BufferLogger.test(); final MemoryFileSystem fileSystem = MemoryFileSystem.test(); fileSystem.file('pubspec.yaml').createSync(); fileSystem.file('pubspec.lock') ..createSync() ..setLastModifiedSync(DateTime(1991)); fileSystem.file('.dart_tool/package_config.json') .createSync(recursive: true); fileSystem.file('version').writeAsStringSync('b'); fileSystem.file('.dart_tool/version').writeAsStringSync('b'); final Pub pub = Pub( fileSystem: fileSystem, logger: logger, processManager: processManager, usage: MockUsage(), platform: FakePlatform( environment: const <String, String>{}, ), botDetector: const BotDetectorAlwaysNo(), ); await pub.get( context: PubContext.pubGet, checkUpToDate: true, ); expect(processManager.hasRemainingExpectations, false); expect(fileSystem.file('.dart_tool/version').readAsStringSync(), 'b'); }); testWithoutContext('pub get 69', () async { String error; final MockProcessManager processMock = MockProcessManager(69); final BufferLogger logger = BufferLogger.test(); final Pub pub = Pub( fileSystem: MockFileSystem(), logger: logger, processManager: processMock, usage: MockUsage(), platform: FakePlatform( environment: const <String, String>{}, ), botDetector: const BotDetectorAlwaysNo(), ); FakeAsync().run((FakeAsync time) { expect(processMock.lastPubEnvironment, isNull); expect(logger.statusText, ''); pub.get(context: PubContext.flutterTests).then((void value) { error = 'test completed unexpectedly'; }, onError: (dynamic thrownError) { error = 'test failed unexpectedly: $thrownError'; }); time.elapse(const Duration(milliseconds: 500)); expect(logger.statusText, 'Running "flutter pub get" in /...\n' 'pub get failed (server unavailable) -- attempting retry 1 in 1 second...\n', ); expect(processMock.lastPubEnvironment, contains('flutter_cli:flutter_tests')); expect(processMock.lastPubCache, isNull); time.elapse(const Duration(milliseconds: 500)); expect(logger.statusText, 'Running "flutter pub get" in /...\n' 'pub get failed (server unavailable) -- attempting retry 1 in 1 second...\n' 'pub get failed (server unavailable) -- attempting retry 2 in 2 seconds...\n', ); time.elapse(const Duration(seconds: 1)); expect(logger.statusText, 'Running "flutter pub get" in /...\n' 'pub get failed (server unavailable) -- attempting retry 1 in 1 second...\n' 'pub get failed (server unavailable) -- attempting retry 2 in 2 seconds...\n', ); time.elapse(const Duration(seconds: 100)); // from t=0 to t=100 expect(logger.statusText, 'Running "flutter pub get" in /...\n' 'pub get failed (server unavailable) -- attempting retry 1 in 1 second...\n' 'pub get failed (server unavailable) -- attempting retry 2 in 2 seconds...\n' 'pub get failed (server unavailable) -- attempting retry 3 in 4 seconds...\n' // at t=1 'pub get failed (server unavailable) -- attempting retry 4 in 8 seconds...\n' // at t=5 'pub get failed (server unavailable) -- attempting retry 5 in 16 seconds...\n' // at t=13 'pub get failed (server unavailable) -- attempting retry 6 in 32 seconds...\n' // at t=29 'pub get failed (server unavailable) -- attempting retry 7 in 64 seconds...\n', // at t=61 ); time.elapse(const Duration(seconds: 200)); // from t=0 to t=200 expect(logger.statusText, 'Running "flutter pub get" in /...\n' 'pub get failed (server unavailable) -- attempting retry 1 in 1 second...\n' 'pub get failed (server unavailable) -- attempting retry 2 in 2 seconds...\n' 'pub get failed (server unavailable) -- attempting retry 3 in 4 seconds...\n' 'pub get failed (server unavailable) -- attempting retry 4 in 8 seconds...\n' 'pub get failed (server unavailable) -- attempting retry 5 in 16 seconds...\n' 'pub get failed (server unavailable) -- attempting retry 6 in 32 seconds...\n' 'pub get failed (server unavailable) -- attempting retry 7 in 64 seconds...\n' 'pub get failed (server unavailable) -- attempting retry 8 in 64 seconds...\n' // at t=39 'pub get failed (server unavailable) -- attempting retry 9 in 64 seconds...\n' // at t=103 'pub get failed (server unavailable) -- attempting retry 10 in 64 seconds...\n', // at t=167 ); }); expect(logger.errorText, isEmpty); expect(error, isNull); }); testWithoutContext('pub get 66 shows message from pub', () async { final BufferLogger logger = BufferLogger.test(); final Pub pub = Pub( platform: FakePlatform(environment: const <String, String>{}), fileSystem: MockFileSystem(), logger: logger, usage: MockUsage(), botDetector: const BotDetectorAlwaysNo(), processManager: MockProcessManager(66, stderr: 'err1\nerr2\nerr3\n', stdout: 'out1\nout2\nout3\n'), ); try { await pub.get(context: PubContext.flutterTests); throw AssertionError('pubGet did not fail'); } on ToolExit catch (error) { expect(error.message, 'pub get failed (66; err3)'); } expect(logger.statusText, 'Running "flutter pub get" in /...\n' 'out1\n' 'out2\n' 'out3\n' ); expect(logger.errorText, 'err1\n' 'err2\n' 'err3\n' ); }); testWithoutContext('pub cache in root is used', () async { String error; final MockProcessManager processMock = MockProcessManager(69); final MockFileSystem fsMock = MockFileSystem(); final Pub pub = Pub( platform: FakePlatform(environment: const <String, String>{}), usage: MockUsage(), fileSystem: fsMock, logger: BufferLogger.test(), processManager: processMock, botDetector: const BotDetectorAlwaysNo(), ); FakeAsync().run((FakeAsync time) { MockDirectory.findCache = true; expect(processMock.lastPubEnvironment, isNull); expect(processMock.lastPubCache, isNull); pub.get(context: PubContext.flutterTests).then((void value) { error = 'test completed unexpectedly'; }, onError: (dynamic thrownError) { error = 'test failed unexpectedly: $thrownError'; }); time.elapse(const Duration(milliseconds: 500)); expect(processMock.lastPubCache, equals(fsMock.path.join(Cache.flutterRoot, '.pub-cache'))); expect(error, isNull); }); }); testWithoutContext('pub cache in environment is used', () async { final MockProcessManager processMock = MockProcessManager(69); final Pub pub = Pub( fileSystem: MockFileSystem(), logger: BufferLogger.test(), processManager: processMock, usage: MockUsage(), botDetector: const BotDetectorAlwaysNo(), platform: FakePlatform( environment: const <String, String>{ 'PUB_CACHE': 'custom/pub-cache/path', }, ), ); FakeAsync().run((FakeAsync time) { MockDirectory.findCache = true; expect(processMock.lastPubEnvironment, isNull); expect(processMock.lastPubCache, isNull); String error; pub.get(context: PubContext.flutterTests).then((void value) { error = 'test completed unexpectedly'; }, onError: (dynamic thrownError) { error = 'test failed unexpectedly: $thrownError'; }); time.elapse(const Duration(milliseconds: 500)); expect(processMock.lastPubCache, equals('custom/pub-cache/path')); expect(error, isNull); }); }); testWithoutContext('analytics sent on success', () async { final FileSystem fileSystem = MemoryFileSystem.test(); final MockUsage usage = MockUsage(); final Pub pub = Pub( fileSystem: fileSystem, logger: BufferLogger.test(), processManager: MockProcessManager(0), botDetector: const BotDetectorAlwaysNo(), usage: usage, platform: FakePlatform( environment: const <String, String>{ 'PUB_CACHE': 'custom/pub-cache/path', } ), ); fileSystem.file('version').createSync(); fileSystem.file('pubspec.yaml').createSync(); fileSystem.file('.dart_tool/package_config.json') ..createSync(recursive: true) ..writeAsStringSync('{"configVersion": 2,"packages": []}'); await pub.get( context: PubContext.flutterTests, generateSyntheticPackage: true, ); verify(usage.sendEvent('pub-result', 'flutter-tests', label: 'success')).called(1); }); testWithoutContext('package_config_subset file is generated from packages and not timestamp', () async { final FileSystem fileSystem = MemoryFileSystem.test(); final MockUsage usage = MockUsage(); final Pub pub = Pub( fileSystem: fileSystem, logger: BufferLogger.test(), processManager: MockProcessManager(0), botDetector: const BotDetectorAlwaysNo(), usage: usage, platform: FakePlatform( environment: const <String, String>{ 'PUB_CACHE': 'custom/pub-cache/path', } ), ); fileSystem.file('version').createSync(); fileSystem.file('pubspec.yaml').createSync(); fileSystem.file('.dart_tool/package_config.json') ..createSync(recursive: true) ..writeAsStringSync(''' {"configVersion": 2,"packages": [ { "name": "flutter_tools", "rootUri": "../", "packageUri": "lib/", "languageVersion": "2.7" } ],"generated":"some-time"} '''); await pub.get( context: PubContext.flutterTests, generateSyntheticPackage: true, ); expect( fileSystem.file('.dart_tool/package_config_subset').readAsStringSync(), 'flutter_tools\n' '2.7\n' 'file:///\n' 'file:///lib/\n' '2\n', ); }); testWithoutContext('analytics sent on failure', () async { MockDirectory.findCache = true; final MockUsage usage = MockUsage(); final Pub pub = Pub( usage: usage, fileSystem: MockFileSystem(), logger: BufferLogger.test(), processManager: MockProcessManager(1), botDetector: const BotDetectorAlwaysNo(), platform: FakePlatform( environment: const <String, String>{ 'PUB_CACHE': 'custom/pub-cache/path', }, ), ); try { await pub.get(context: PubContext.flutterTests); } on ToolExit { // Ignore. } verify(usage.sendEvent('pub-result', 'flutter-tests', label: 'failure')).called(1); }); testWithoutContext('analytics sent on failed version solve', () async { final MockUsage usage = MockUsage(); final FileSystem fileSystem = MemoryFileSystem.test(); final Pub pub = Pub( fileSystem: fileSystem, logger: BufferLogger.test(), processManager: MockProcessManager( 1, stderr: 'version solving failed', ), platform: FakePlatform( environment: <String, String>{ 'PUB_CACHE': 'custom/pub-cache/path', }, ), usage: usage, botDetector: const BotDetectorAlwaysNo(), ); fileSystem.file('pubspec.yaml').writeAsStringSync('name: foo'); try { await pub.get(context: PubContext.flutterTests); } on ToolExit { // Ignore. } verify(usage.sendEvent('pub-result', 'flutter-tests', label: 'version-solving-failed')).called(1); }); testWithoutContext('Pub error handling', () async { final BufferLogger logger = BufferLogger.test(); final MemoryFileSystem fileSystem = MemoryFileSystem.test(); final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ FakeCommand( command: const <String>[ 'bin/cache/dart-sdk/bin/pub', '--verbosity=warning', 'get', '--no-precompile', ], onRun: () { fileSystem.file('.dart_tool/package_config.json') .setLastModifiedSync(DateTime(2002)); } ), const FakeCommand( command: <String>[ 'bin/cache/dart-sdk/bin/pub', '--verbosity=warning', 'get', '--no-precompile', ], ), FakeCommand( command: const <String>[ 'bin/cache/dart-sdk/bin/pub', '--verbosity=warning', 'get', '--no-precompile', ], onRun: () { fileSystem.file('pubspec.yaml') .setLastModifiedSync(DateTime(2002)); } ), const FakeCommand( command: <String>[ 'bin/cache/dart-sdk/bin/pub', '--verbosity=warning', 'get', '--no-precompile', ], ), ]); final Pub pub = Pub( usage: MockUsage(), fileSystem: fileSystem, logger: logger, processManager: processManager, platform: FakePlatform( operatingSystem: 'linux', // so that the command executed is consistent environment: <String, String>{}, ), botDetector: const BotDetectorAlwaysNo() ); fileSystem.file('version').createSync(); // the good scenario: .packages is old, pub updates the file. fileSystem.file('.dart_tool/package_config.json') ..createSync(recursive: true) ..setLastModifiedSync(DateTime(2000)); fileSystem.file('pubspec.yaml') ..createSync() ..setLastModifiedSync(DateTime(2001)); await pub.get(context: PubContext.flutterTests); // pub sets date of .packages to 2002 expect(logger.statusText, 'Running "flutter pub get" in /...\n'); expect(logger.errorText, isEmpty); expect(fileSystem.file('pubspec.yaml').lastModifiedSync(), DateTime(2001)); // because nothing should touch it logger.clear(); // bad scenario 1: pub doesn't update file; doesn't matter, because we do instead fileSystem.file('.dart_tool/package_config.json') .setLastModifiedSync(DateTime(2000)); fileSystem.file('pubspec.yaml') .setLastModifiedSync(DateTime(2001)); await pub.get(context: PubContext.flutterTests); // pub does nothing expect(logger.statusText, 'Running "flutter pub get" in /...\n'); expect(logger.errorText, isEmpty); expect(fileSystem.file('pubspec.yaml').lastModifiedSync(), DateTime(2001)); // because nothing should touch it logger.clear(); }); } class BotDetectorAlwaysNo implements BotDetector { const BotDetectorAlwaysNo(); @override Future<bool> get isRunningOnBot async => false; } typedef StartCallback = void Function(List<dynamic> command); class MockProcessManager implements ProcessManager { MockProcessManager(this.fakeExitCode, { this.stdout = '', this.stderr = '', }); final int fakeExitCode; final String stdout; final String stderr; String lastPubEnvironment; String lastPubCache; @override Future<Process> start( List<dynamic> command, { String workingDirectory, Map<String, String> environment, bool includeParentEnvironment = true, bool runInShell = false, ProcessStartMode mode = ProcessStartMode.normal, }) { lastPubEnvironment = environment['PUB_ENVIRONMENT']; lastPubCache = environment['PUB_CACHE']; return Future<Process>.value(mocks.createMockProcess( exitCode: fakeExitCode, stdout: stdout, stderr: stderr, )); } @override dynamic noSuchMethod(Invocation invocation) => null; } class MockFileSystem extends ForwardingFileSystem { MockFileSystem() : super(MemoryFileSystem.test()); @override File file(dynamic path) { return MockFile(); } @override Directory directory(dynamic path) { return MockDirectory(path as String); } } class MockFile implements File { @override Future<RandomAccessFile> open({ FileMode mode = FileMode.read }) async { return MockRandomAccessFile(); } @override bool existsSync() => true; @override DateTime lastModifiedSync() => DateTime(0); @override dynamic noSuchMethod(Invocation invocation) => null; } class MockDirectory implements Directory { MockDirectory(this.path); @override final String path; static bool findCache = false; @override bool existsSync() => findCache && path.endsWith('.pub-cache'); @override dynamic noSuchMethod(Invocation invocation) => null; } class MockRandomAccessFile extends Mock implements RandomAccessFile {} class MockUsage extends Mock implements Usage {}