[flutter_tools] Delete system temp entries on fatal signals (#55513)
This commit is contained in:
parent
ea0c73c1dc
commit
533cd7a6d1
@ -3,13 +3,19 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart' as local_fs;
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'common.dart' show throwToolExit;
|
||||
import 'io.dart';
|
||||
import 'platform.dart';
|
||||
import 'process.dart';
|
||||
import 'signals.dart';
|
||||
|
||||
// package:file/local.dart must not be exported. This exposes LocalFileSystem,
|
||||
// which we override to ensure that temporary directories are cleaned up when
|
||||
// the tool is killed by a signal.
|
||||
export 'package:file/file.dart';
|
||||
export 'package:file/local.dart';
|
||||
|
||||
/// Exception indicating that a file that was expected to exist was not found.
|
||||
class FileNotFoundException implements IOException {
|
||||
@ -136,11 +142,94 @@ class FileSystemUtils {
|
||||
/// Return the absolute path of the user's home directory
|
||||
String get homeDirPath {
|
||||
String path = _platform.isWindows
|
||||
? _platform.environment['USERPROFILE']
|
||||
: _platform.environment['HOME'];
|
||||
? _platform.environment['USERPROFILE']
|
||||
: _platform.environment['HOME'];
|
||||
if (path != null) {
|
||||
path = _fileSystem.path.absolute(path);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
/// This class extends [local_fs.LocalFileSystem] in order to clean up
|
||||
/// directories and files that the tool creates under the system temporary
|
||||
/// directory when the tool exits either normally or when killed by a signal.
|
||||
class LocalFileSystem extends local_fs.LocalFileSystem {
|
||||
LocalFileSystem._(Signals signals, List<ProcessSignal> fatalSignals) :
|
||||
_signals = signals, _fatalSignals = fatalSignals;
|
||||
|
||||
@visibleForTesting
|
||||
LocalFileSystem.test({
|
||||
@required Signals signals,
|
||||
List<ProcessSignal> fatalSignals = Signals.defaultExitSignals,
|
||||
}) : this._(signals, fatalSignals);
|
||||
|
||||
// Unless we're in a test of this class's signal hanlding features, we must
|
||||
// have only one instance created with the singleton LocalSignals instance
|
||||
// and the catchable signals it considers to be fatal.
|
||||
static LocalFileSystem _instance;
|
||||
static LocalFileSystem get instance => _instance ??= LocalFileSystem._(
|
||||
LocalSignals.instance,
|
||||
Signals.defaultExitSignals,
|
||||
);
|
||||
|
||||
Directory _systemTemp;
|
||||
final Map<ProcessSignal, Object> _signalTokens = <ProcessSignal, Object>{};
|
||||
|
||||
@visibleForTesting
|
||||
static Future<void> dispose() => LocalFileSystem.instance?._dispose();
|
||||
|
||||
Future<void> _dispose() async {
|
||||
_tryToDeleteTemp();
|
||||
for (final MapEntry<ProcessSignal, Object> signalToken in _signalTokens.entries) {
|
||||
await _signals.removeHandler(signalToken.key, signalToken.value);
|
||||
}
|
||||
_signalTokens.clear();
|
||||
}
|
||||
|
||||
final Signals _signals;
|
||||
final List<ProcessSignal> _fatalSignals;
|
||||
|
||||
void _tryToDeleteTemp() {
|
||||
try {
|
||||
if (_systemTemp?.existsSync() ?? false) {
|
||||
_systemTemp.deleteSync(recursive: true);
|
||||
}
|
||||
} on FileSystemException {
|
||||
// ignore.
|
||||
}
|
||||
_systemTemp = null;
|
||||
}
|
||||
|
||||
// This getter returns a fresh entry under /tmp, like
|
||||
// /tmp/flutter_tools.abcxyz, then the rest of the tool creates /tmp entries
|
||||
// under that, like /tmp/flutter_tools.abcxyz/flutter_build_stuff.123456.
|
||||
// Right before exiting because of a signal or otherwise, we delete
|
||||
// /tmp/flutter_tools.abcxyz, not the whole of /tmp.
|
||||
@override
|
||||
Directory get systemTempDirectory {
|
||||
if (_systemTemp == null) {
|
||||
_systemTemp = super.systemTempDirectory.createTempSync(
|
||||
'flutter_tools.',
|
||||
)..createSync(recursive: true);
|
||||
// Make sure that the temporary directory is cleaned up if the tool is
|
||||
// killed by a signal.
|
||||
for (final ProcessSignal signal in _fatalSignals) {
|
||||
final Object token = _signals.addHandler(
|
||||
signal,
|
||||
(ProcessSignal _) {
|
||||
_tryToDeleteTemp();
|
||||
},
|
||||
);
|
||||
_signalTokens[signal] = token;
|
||||
}
|
||||
// Make sure that the temporary directory is cleaned up when the tool
|
||||
// exits normally.
|
||||
shutdownHooks?.addShutdownHook(
|
||||
_tryToDeleteTemp,
|
||||
ShutdownStage.CLEANUP,
|
||||
);
|
||||
}
|
||||
return _systemTemp;
|
||||
}
|
||||
}
|
||||
|
@ -90,6 +90,7 @@ export 'dart:io'
|
||||
ProcessStartMode,
|
||||
// RandomAccessFile NO! Use `file_system.dart`
|
||||
ServerSocket,
|
||||
SignalException,
|
||||
// stderr, NO! Use `io.dart`
|
||||
// stdin, NO! Use `io.dart`
|
||||
Stdin,
|
||||
|
@ -4,30 +4,27 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'async_guard.dart';
|
||||
import 'context.dart';
|
||||
import 'io.dart';
|
||||
|
||||
typedef SignalHandler = FutureOr<void> Function(ProcessSignal signal);
|
||||
|
||||
Signals get signals => Signals.instance;
|
||||
|
||||
// The default list of signals that should cause the process to exit.
|
||||
const List<ProcessSignal> _defaultExitSignals = <ProcessSignal>[
|
||||
ProcessSignal.SIGTERM,
|
||||
ProcessSignal.SIGINT,
|
||||
ProcessSignal.SIGKILL,
|
||||
];
|
||||
|
||||
/// A class that manages signal handlers
|
||||
///
|
||||
/// Signal handlers are run in the order that they were added.
|
||||
abstract class Signals {
|
||||
factory Signals({
|
||||
List<ProcessSignal> exitSignals = _defaultExitSignals,
|
||||
}) => _DefaultSignals._(exitSignals);
|
||||
@visibleForTesting
|
||||
factory Signals.test({
|
||||
List<ProcessSignal> exitSignals = defaultExitSignals,
|
||||
}) => LocalSignals._(exitSignals);
|
||||
|
||||
static Signals get instance => context.get<Signals>();
|
||||
// The default list of signals that should cause the process to exit.
|
||||
static const List<ProcessSignal> defaultExitSignals = <ProcessSignal>[
|
||||
ProcessSignal.SIGTERM,
|
||||
ProcessSignal.SIGINT,
|
||||
];
|
||||
|
||||
/// Adds a signal handler to run on receipt of signal.
|
||||
///
|
||||
@ -48,8 +45,17 @@ abstract class Signals {
|
||||
Stream<Object> get errors;
|
||||
}
|
||||
|
||||
class _DefaultSignals implements Signals {
|
||||
_DefaultSignals._(this.exitSignals);
|
||||
/// A class that manages the real dart:io signal handlers.
|
||||
///
|
||||
/// We use a singleton instance of this class to ensure that all handlers for
|
||||
/// fatal signals run before this class calls exit().
|
||||
class LocalSignals implements Signals {
|
||||
LocalSignals._(this.exitSignals);
|
||||
|
||||
static LocalSignals _instance;
|
||||
static LocalSignals get instance => _instance ??= LocalSignals._(
|
||||
Signals.defaultExitSignals,
|
||||
);
|
||||
|
||||
final List<ProcessSignal> exitSignals;
|
||||
|
||||
@ -84,7 +90,13 @@ class _DefaultSignals implements Signals {
|
||||
// If we added the first one, then call signal.watch(), listen, and cache
|
||||
// the stream controller.
|
||||
if (_handlersList[signal].length == 1) {
|
||||
_streamSubscriptions[signal] = signal.watch().listen(_handleSignal);
|
||||
_streamSubscriptions[signal] = signal.watch().listen(
|
||||
_handleSignal,
|
||||
onError: (Object e) {
|
||||
_handlersTable[signal].remove(token);
|
||||
_handlersList[signal].remove(handler);
|
||||
},
|
||||
);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
@ -99,7 +111,10 @@ class _DefaultSignals implements Signals {
|
||||
if (!_handlersTable[signal].containsKey(token)) {
|
||||
return false;
|
||||
}
|
||||
final SignalHandler handler = _handlersTable[signal][token];
|
||||
final SignalHandler handler = _handlersTable[signal].remove(token);
|
||||
if (handler == null) {
|
||||
return false;
|
||||
}
|
||||
final bool removed = _handlersList[signal].remove(handler);
|
||||
if (!removed) {
|
||||
return false;
|
||||
|
@ -103,41 +103,50 @@ class AnalyzeOnce extends AnalyzeBase {
|
||||
experiments: experiments,
|
||||
);
|
||||
|
||||
StreamSubscription<bool> subscription;
|
||||
subscription = server.onAnalyzing.listen((bool isAnalyzing) {
|
||||
if (!isAnalyzing) {
|
||||
analysisCompleter.complete();
|
||||
subscription?.cancel();
|
||||
subscription = null;
|
||||
}
|
||||
});
|
||||
server.onErrors.listen((FileAnalysisErrors fileErrors) {
|
||||
// Record the issues found (but filter out to do comments).
|
||||
errors.addAll(fileErrors.errors.where((AnalysisError error) => error.type != 'TODO'));
|
||||
});
|
||||
Stopwatch timer;
|
||||
Status progress;
|
||||
try {
|
||||
StreamSubscription<bool> subscription;
|
||||
subscription = server.onAnalyzing.listen((bool isAnalyzing) {
|
||||
if (!isAnalyzing) {
|
||||
analysisCompleter.complete();
|
||||
subscription?.cancel();
|
||||
subscription = null;
|
||||
}
|
||||
});
|
||||
server.onErrors.listen((FileAnalysisErrors fileErrors) {
|
||||
// Record the issues found (but filter out to do comments).
|
||||
errors.addAll(fileErrors.errors.where((AnalysisError error) => error.type != 'TODO'));
|
||||
});
|
||||
|
||||
await server.start();
|
||||
// Completing the future in the callback can't fail.
|
||||
unawaited(server.onExit.then<void>((int exitCode) {
|
||||
if (!analysisCompleter.isCompleted) {
|
||||
analysisCompleter.completeError('analysis server exited: $exitCode');
|
||||
}
|
||||
}));
|
||||
await server.start();
|
||||
// Completing the future in the callback can't fail.
|
||||
unawaited(server.onExit.then<void>((int exitCode) {
|
||||
if (!analysisCompleter.isCompleted) {
|
||||
analysisCompleter.completeError('analysis server exited: $exitCode');
|
||||
}
|
||||
}));
|
||||
|
||||
Cache.releaseLockEarly();
|
||||
Cache.releaseLockEarly();
|
||||
|
||||
// collect results
|
||||
final Stopwatch timer = Stopwatch()..start();
|
||||
final String message = directories.length > 1
|
||||
? '${directories.length} ${directories.length == 1 ? 'directory' : 'directories'}'
|
||||
: fileSystem.path.basename(directories.first);
|
||||
final Status progress = argResults['preamble'] as bool
|
||||
? logger.startProgress('Analyzing $message...', timeout: timeoutConfiguration.slowOperation)
|
||||
: null;
|
||||
// collect results
|
||||
timer = Stopwatch()..start();
|
||||
final String message = directories.length > 1
|
||||
? '${directories.length} ${directories.length == 1 ? 'directory' : 'directories'}'
|
||||
: fileSystem.path.basename(directories.first);
|
||||
progress = argResults['preamble'] as bool
|
||||
? logger.startProgress(
|
||||
'Analyzing $message...',
|
||||
timeout: timeoutConfiguration.slowOperation,
|
||||
)
|
||||
: null;
|
||||
|
||||
await analysisCompleter.future;
|
||||
progress?.cancel();
|
||||
timer.stop();
|
||||
await analysisCompleter.future;
|
||||
} finally {
|
||||
await server.dispose();
|
||||
progress?.cancel();
|
||||
timer?.stop();
|
||||
}
|
||||
|
||||
// count missing dartdocs
|
||||
final int undocumentedMembers = errors.where((AnalysisError error) {
|
||||
|
@ -17,7 +17,6 @@ import 'base/io.dart';
|
||||
import 'base/logger.dart';
|
||||
import 'base/os.dart';
|
||||
import 'base/process.dart';
|
||||
import 'base/signals.dart';
|
||||
import 'base/time.dart';
|
||||
import 'base/user_messages.dart';
|
||||
import 'build_system/build_system.dart';
|
||||
@ -182,7 +181,6 @@ Future<T> runInContext<T>(
|
||||
usage: globals.flutterUsage,
|
||||
),
|
||||
ShutdownHooks: () => ShutdownHooks(logger: globals.logger),
|
||||
Signals: () => Signals(),
|
||||
Stdio: () => Stdio(),
|
||||
SystemClock: () => const SystemClock(),
|
||||
TimeoutConfiguration: () => const TimeoutConfiguration(),
|
||||
|
@ -17,6 +17,7 @@ import 'base/logger.dart';
|
||||
import 'base/net.dart';
|
||||
import 'base/os.dart';
|
||||
import 'base/platform.dart';
|
||||
import 'base/signals.dart';
|
||||
import 'base/template.dart';
|
||||
import 'base/terminal.dart';
|
||||
import 'base/user_messages.dart';
|
||||
@ -45,20 +46,22 @@ HttpClientFactory get httpClientFactory => context.get<HttpClientFactory>();
|
||||
Logger get logger => context.get<Logger>();
|
||||
OperatingSystemUtils get os => context.get<OperatingSystemUtils>();
|
||||
PersistentToolState get persistentToolState => PersistentToolState.instance;
|
||||
Signals get signals => context.get<Signals>() ?? LocalSignals.instance;
|
||||
Usage get flutterUsage => context.get<Usage>();
|
||||
FlutterProjectFactory get projectFactory => context.get<FlutterProjectFactory>() ?? FlutterProjectFactory(
|
||||
logger: logger,
|
||||
fileSystem: fs,
|
||||
);
|
||||
|
||||
const FileSystem _kLocalFs = LocalFileSystem();
|
||||
FlutterProjectFactory get projectFactory {
|
||||
return context.get<FlutterProjectFactory>() ?? FlutterProjectFactory(
|
||||
logger: logger,
|
||||
fileSystem: fs,
|
||||
);
|
||||
}
|
||||
|
||||
/// Currently active implementation of the file system.
|
||||
///
|
||||
/// By default it uses local disk-based implementation. Override this in tests
|
||||
/// with [MemoryFileSystem].
|
||||
FileSystem get fs => ErrorHandlingFileSystem(
|
||||
delegate: context.get<FileSystem>() ?? _kLocalFs,
|
||||
delegate: context.get<FileSystem>() ?? LocalFileSystem.instance,
|
||||
platform: platform,
|
||||
);
|
||||
|
||||
|
@ -1295,7 +1295,7 @@ class TerminalHandler {
|
||||
final Map<io.ProcessSignal, Object> _signalTokens = <io.ProcessSignal, Object>{};
|
||||
|
||||
void _addSignalHandler(io.ProcessSignal signal, SignalHandler handler) {
|
||||
_signalTokens[signal] = signals.addHandler(signal, handler);
|
||||
_signalTokens[signal] = globals.signals.addHandler(signal, handler);
|
||||
}
|
||||
|
||||
void registerSignalHandlers() {
|
||||
@ -1314,7 +1314,7 @@ class TerminalHandler {
|
||||
void stop() {
|
||||
assert(residentRunner.stayResident);
|
||||
for (final MapEntry<io.ProcessSignal, Object> entry in _signalTokens.entries) {
|
||||
signals.removeHandler(entry.key, entry.value);
|
||||
globals.signals.removeHandler(entry.key, entry.value);
|
||||
}
|
||||
_signalTokens.clear();
|
||||
subscription.cancel();
|
||||
|
@ -712,8 +712,8 @@ abstract class FlutterCommand extends Command<void> {
|
||||
systemClock.now(),
|
||||
);
|
||||
};
|
||||
signals.addHandler(io.ProcessSignal.SIGTERM, handler);
|
||||
signals.addHandler(io.ProcessSignal.SIGINT, handler);
|
||||
globals.signals.addHandler(io.ProcessSignal.SIGTERM, handler);
|
||||
globals.signals.addHandler(io.ProcessSignal.SIGINT, handler);
|
||||
}
|
||||
|
||||
/// Logs data about this command.
|
||||
|
@ -31,7 +31,7 @@ void main() {
|
||||
|
||||
setUp(() {
|
||||
platform = const LocalPlatform();
|
||||
fileSystem = const LocalFileSystem();
|
||||
fileSystem = LocalFileSystem.instance;
|
||||
platform = const LocalPlatform();
|
||||
processManager = const LocalProcessManager();
|
||||
terminal = AnsiTerminal(platform: platform, stdio: Stdio());
|
||||
|
@ -76,37 +76,39 @@ sky_engine:$flutterRootUri/bin/cache/pkg/sky_engine/lib/
|
||||
flutter_project:lib/
|
||||
''';
|
||||
fileSystem.file(fileSystem.path.join(projectPath, '.packages'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(dotPackagesSrc);
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(dotPackagesSrc);
|
||||
}
|
||||
|
||||
setUpAll(() {
|
||||
Cache.disableLocking();
|
||||
Cache.flutterRoot = FlutterCommandRunner.defaultFlutterRoot;
|
||||
processManager = const LocalProcessManager();
|
||||
terminal = AnsiTerminal(platform: platform, stdio: Stdio());
|
||||
fileSystem = const LocalFileSystem();
|
||||
platform = const LocalPlatform();
|
||||
terminal = AnsiTerminal(platform: platform, stdio: Stdio());
|
||||
fileSystem = LocalFileSystem.instance;
|
||||
logger = BufferLogger(
|
||||
outputPreferences: OutputPreferences.test(),
|
||||
terminal: terminal,
|
||||
);
|
||||
analyzerSeparator = platform.isWindows ? '-' : '•';
|
||||
tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_analyze_once_test_1.').absolute;
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
tempDir = fileSystem.systemTempDirectory.createTempSync(
|
||||
'flutter_analyze_once_test_1.',
|
||||
).absolute;
|
||||
projectPath = fileSystem.path.join(tempDir.path, 'flutter_project');
|
||||
fileSystem.file(fileSystem.path.join(projectPath, 'pubspec.yaml'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(pubspecYamlSrc);
|
||||
_createDotPackages(projectPath);
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
libMain = fileSystem.file(fileSystem.path.join(projectPath, 'lib', 'main.dart'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(mainDartSrc);
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
tearDown(() {
|
||||
tryToDelete(tempDir);
|
||||
});
|
||||
|
||||
|
@ -226,7 +226,7 @@ void main() {
|
||||
|
||||
testWithoutContext('Throws type error if Directory type is set to curentDirectory with LocalFileSystem', () {
|
||||
final FileSystem fs = ErrorHandlingFileSystem(
|
||||
delegate: const LocalFileSystem(),
|
||||
delegate: LocalFileSystem.instance,
|
||||
platform: const LocalPlatform(),
|
||||
);
|
||||
final MockDirectory directory = MockDirectory();
|
||||
|
@ -2,12 +2,18 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io' as io;
|
||||
|
||||
import 'package:file/memory.dart';
|
||||
import 'package:flutter_tools/src/base/file_system.dart';
|
||||
import 'package:flutter_tools/src/base/io.dart';
|
||||
import 'package:flutter_tools/src/base/platform.dart';
|
||||
import 'package:flutter_tools/src/base/signals.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
import '../../src/common.dart';
|
||||
import '../../src/context.dart';
|
||||
|
||||
class MockPlatform extends Mock implements Platform {}
|
||||
|
||||
@ -121,4 +127,40 @@ void main() {
|
||||
expect(fsUtils.escapePath(r'foo\cool.dart'), r'foo\cool.dart');
|
||||
});
|
||||
});
|
||||
|
||||
group('LocalFileSystem', () {
|
||||
MockIoProcessSignal mockSignal;
|
||||
ProcessSignal signalUnderTest;
|
||||
StreamController<io.ProcessSignal> controller;
|
||||
|
||||
setUp(() {
|
||||
mockSignal = MockIoProcessSignal();
|
||||
signalUnderTest = ProcessSignal(mockSignal);
|
||||
controller = StreamController<io.ProcessSignal>();
|
||||
when(mockSignal.watch()).thenAnswer((Invocation invocation) => controller.stream);
|
||||
});
|
||||
|
||||
testUsingContext('deletes system temp entry on a fatal signal', () async {
|
||||
final Completer<void> completer = Completer<void>();
|
||||
final Signals signals = Signals.test();
|
||||
final LocalFileSystem localFileSystem = LocalFileSystem.test(
|
||||
signals: signals,
|
||||
fatalSignals: <ProcessSignal>[signalUnderTest],
|
||||
);
|
||||
final Directory temp = localFileSystem.systemTempDirectory;
|
||||
|
||||
signals.addHandler(signalUnderTest, (ProcessSignal s) {
|
||||
completer.complete();
|
||||
});
|
||||
|
||||
expect(temp.existsSync(), isTrue);
|
||||
|
||||
controller.add(mockSignal);
|
||||
await completer.future;
|
||||
|
||||
expect(temp.existsSync(), isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class MockIoProcessSignal extends Mock implements io.ProcessSignal {}
|
||||
|
@ -10,22 +10,23 @@ import 'package:flutter_tools/src/base/signals.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
import '../../src/common.dart';
|
||||
import '../../src/context.dart';
|
||||
|
||||
void main() {
|
||||
group('Signals', () {
|
||||
Signals signals;
|
||||
MockIoProcessSignal mockSignal;
|
||||
ProcessSignal signalUnderTest;
|
||||
StreamController<io.ProcessSignal> controller;
|
||||
|
||||
setUp(() {
|
||||
signals = Signals.test();
|
||||
mockSignal = MockIoProcessSignal();
|
||||
signalUnderTest = ProcessSignal(mockSignal);
|
||||
controller = StreamController<io.ProcessSignal>();
|
||||
when(mockSignal.watch()).thenAnswer((Invocation invocation) => controller.stream);
|
||||
});
|
||||
|
||||
testUsingContext('signal handler runs', () async {
|
||||
testWithoutContext('signal handler runs', () async {
|
||||
final Completer<void> completer = Completer<void>();
|
||||
signals.addHandler(signalUnderTest, (ProcessSignal s) {
|
||||
expect(s, signalUnderTest);
|
||||
@ -34,11 +35,9 @@ void main() {
|
||||
|
||||
controller.add(mockSignal);
|
||||
await completer.future;
|
||||
}, overrides: <Type, Generator>{
|
||||
Signals: () => Signals(),
|
||||
});
|
||||
|
||||
testUsingContext('signal handlers run in order', () async {
|
||||
testWithoutContext('signal handlers run in order', () async {
|
||||
final Completer<void> completer = Completer<void>();
|
||||
|
||||
bool first = false;
|
||||
@ -56,11 +55,9 @@ void main() {
|
||||
|
||||
controller.add(mockSignal);
|
||||
await completer.future;
|
||||
}, overrides: <Type, Generator>{
|
||||
Signals: () => Signals(),
|
||||
});
|
||||
|
||||
testUsingContext('signal handler error goes on error stream', () async {
|
||||
testWithoutContext('signal handler error goes on error stream', () async {
|
||||
final Exception exn = Exception('Error');
|
||||
signals.addHandler(signalUnderTest, (ProcessSignal s) {
|
||||
throw exn;
|
||||
@ -68,65 +65,71 @@ void main() {
|
||||
|
||||
final Completer<void> completer = Completer<void>();
|
||||
final List<Object> errList = <Object>[];
|
||||
final StreamSubscription<Object> errSub = signals.errors.listen((Object err) {
|
||||
errList.add(err);
|
||||
completer.complete();
|
||||
});
|
||||
final StreamSubscription<Object> errSub = signals.errors.listen(
|
||||
(Object err) {
|
||||
errList.add(err);
|
||||
completer.complete();
|
||||
},
|
||||
);
|
||||
|
||||
controller.add(mockSignal);
|
||||
await completer.future;
|
||||
await errSub.cancel();
|
||||
expect(errList, contains(exn));
|
||||
}, overrides: <Type, Generator>{
|
||||
Signals: () => Signals(),
|
||||
});
|
||||
|
||||
testUsingContext('removed signal handler does not run', () async {
|
||||
final Object token = signals.addHandler(signalUnderTest, (ProcessSignal s) {
|
||||
fail('Signal handler should have been removed.');
|
||||
});
|
||||
testWithoutContext('removed signal handler does not run', () async {
|
||||
final Object token = signals.addHandler(
|
||||
signalUnderTest,
|
||||
(ProcessSignal s) {
|
||||
fail('Signal handler should have been removed.');
|
||||
},
|
||||
);
|
||||
|
||||
await signals.removeHandler(signalUnderTest, token);
|
||||
|
||||
final List<Object> errList = <Object>[];
|
||||
final StreamSubscription<Object> errSub = signals.errors.listen((Object err) {
|
||||
errList.add(err);
|
||||
});
|
||||
final StreamSubscription<Object> errSub = signals.errors.listen(
|
||||
(Object err) {
|
||||
errList.add(err);
|
||||
},
|
||||
);
|
||||
|
||||
controller.add(mockSignal);
|
||||
|
||||
await errSub.cancel();
|
||||
expect(errList, isEmpty);
|
||||
}, overrides: <Type, Generator>{
|
||||
Signals: () => Signals(),
|
||||
});
|
||||
|
||||
testUsingContext('non-removed signal handler still runs', () async {
|
||||
testWithoutContext('non-removed signal handler still runs', () async {
|
||||
final Completer<void> completer = Completer<void>();
|
||||
signals.addHandler(signalUnderTest, (ProcessSignal s) {
|
||||
expect(s, signalUnderTest);
|
||||
completer.complete();
|
||||
});
|
||||
|
||||
final Object token = signals.addHandler(signalUnderTest, (ProcessSignal s) {
|
||||
fail('Signal handler should have been removed.');
|
||||
});
|
||||
final Object token = signals.addHandler(
|
||||
signalUnderTest,
|
||||
(ProcessSignal s) {
|
||||
fail('Signal handler should have been removed.');
|
||||
},
|
||||
);
|
||||
await signals.removeHandler(signalUnderTest, token);
|
||||
|
||||
final List<Object> errList = <Object>[];
|
||||
final StreamSubscription<Object> errSub = signals.errors.listen((Object err) {
|
||||
errList.add(err);
|
||||
});
|
||||
final StreamSubscription<Object> errSub = signals.errors.listen(
|
||||
(Object err) {
|
||||
errList.add(err);
|
||||
},
|
||||
);
|
||||
|
||||
controller.add(mockSignal);
|
||||
await completer.future;
|
||||
await errSub.cancel();
|
||||
expect(errList, isEmpty);
|
||||
}, overrides: <Type, Generator>{
|
||||
Signals: () => Signals(),
|
||||
});
|
||||
|
||||
testUsingContext('only handlers for the correct signal run', () async {
|
||||
testWithoutContext('only handlers for the correct signal run', () async {
|
||||
final MockIoProcessSignal mockSignal2 = MockIoProcessSignal();
|
||||
final StreamController<io.ProcessSignal> controller2 = StreamController<io.ProcessSignal>();
|
||||
final ProcessSignal otherSignal = ProcessSignal(mockSignal2);
|
||||
@ -144,19 +147,22 @@ void main() {
|
||||
});
|
||||
|
||||
final List<Object> errList = <Object>[];
|
||||
final StreamSubscription<Object> errSub = signals.errors.listen((Object err) {
|
||||
errList.add(err);
|
||||
});
|
||||
final StreamSubscription<Object> errSub = signals.errors.listen(
|
||||
(Object err) {
|
||||
errList.add(err);
|
||||
},
|
||||
);
|
||||
|
||||
controller.add(mockSignal);
|
||||
await completer.future;
|
||||
await errSub.cancel();
|
||||
expect(errList, isEmpty);
|
||||
}, overrides: <Type, Generator>{
|
||||
Signals: () => Signals(),
|
||||
});
|
||||
|
||||
testUsingContext('all handlers for exiting signals are run before exit', () async {
|
||||
testWithoutContext('all handlers for exiting signals are run before exit', () async {
|
||||
final Signals signals = Signals.test(
|
||||
exitSignals: <ProcessSignal>[signalUnderTest],
|
||||
);
|
||||
final Completer<void> completer = Completer<void>();
|
||||
bool first = false;
|
||||
bool second = false;
|
||||
@ -186,8 +192,6 @@ void main() {
|
||||
|
||||
controller.add(mockSignal);
|
||||
await completer.future;
|
||||
}, overrides: <Type, Generator>{
|
||||
Signals: () => Signals(exitSignals: <ProcessSignal>[signalUnderTest]),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -53,6 +53,7 @@ void main() {
|
||||
|
||||
setUp(() {
|
||||
mockPlatform = MockPlatform();
|
||||
when(mockPlatform.isWindows).thenReturn(false);
|
||||
mockProcessManager = MockProcessManager();
|
||||
flutterPlatform = TestFlutterPlatform();
|
||||
});
|
||||
|
@ -134,6 +134,27 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
test('no unauthorized imports of package:file/local.dart', () {
|
||||
final List<String> whitelistedPath = <String>[
|
||||
globals.fs.path.join(flutterTools, 'lib', 'src', 'base', 'file_system.dart'),
|
||||
];
|
||||
for (final String dirName in <String>['lib', 'bin', 'test']) {
|
||||
final Iterable<File> files = globals.fs.directory(globals.fs.path.join(flutterTools, dirName))
|
||||
.listSync(recursive: true)
|
||||
.where(_isDartFile)
|
||||
.where((FileSystemEntity entity) => !whitelistedPath.contains(entity.path))
|
||||
.map(_asFile);
|
||||
for (final File file in files) {
|
||||
for (final String line in file.readAsLinesSync()) {
|
||||
if (line.startsWith(RegExp(r'import.*package:file/local.dart'))) {
|
||||
final String relativePath = globals.fs.path.relative(file.path, from:flutterTools);
|
||||
fail("$relativePath imports 'package:file/local.dart'; use 'lib/src/base/file_system.dart' instead");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('no unauthorized imports of dart:convert', () {
|
||||
final List<String> whitelistedPaths = <String>[
|
||||
globals.fs.path.join(flutterTools, 'lib', 'src', 'convert.dart'),
|
||||
|
@ -52,7 +52,7 @@ void main() {
|
||||
stdio: null,
|
||||
),
|
||||
);
|
||||
fileSystem = const LocalFileSystemBlockingSetCurrentDirectory();
|
||||
fileSystem = LocalFileSystemBlockingSetCurrentDirectory();
|
||||
processManager = const LocalProcessManager();
|
||||
parser = PlistParser(
|
||||
fileSystem: fileSystem,
|
||||
|
@ -450,7 +450,7 @@ class FakeSignals implements Signals {
|
||||
FakeSignals({
|
||||
this.subForSigTerm,
|
||||
List<ProcessSignal> exitSignals,
|
||||
}) : delegate = Signals(exitSignals: exitSignals);
|
||||
}) : delegate = Signals.test(exitSignals: exitSignals);
|
||||
|
||||
final ProcessSignal subForSigTerm;
|
||||
final Signals delegate;
|
||||
|
@ -14,7 +14,7 @@ import '../src/common.dart';
|
||||
|
||||
const String _kInitialVersion = 'v1.9.1';
|
||||
const String _kBranch = 'dev';
|
||||
const FileSystem fileSystem = LocalFileSystem();
|
||||
final FileSystem fileSystem = LocalFileSystem.instance;
|
||||
const ProcessManager processManager = LocalProcessManager();
|
||||
final Stdio stdio = Stdio();
|
||||
final ProcessUtils processUtils = ProcessUtils(processManager: processManager, logger: StdoutLogger(
|
||||
|
@ -21,7 +21,7 @@ void main() {
|
||||
FlutterRunTestDriver flutter;
|
||||
VmService vmService;
|
||||
|
||||
setUpAll(() async {
|
||||
setUp(() async {
|
||||
tempDir = createResolvedTempDirectorySync('vmservice_integration_test.');
|
||||
|
||||
final BasicProject _project = BasicProject();
|
||||
@ -33,7 +33,7 @@ void main() {
|
||||
vmService = await vmServiceConnectUri('ws://localhost:$port/ws');
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
tearDown(() async {
|
||||
await flutter?.stop();
|
||||
tryToDelete(tempDir);
|
||||
});
|
||||
|
@ -20,7 +20,7 @@ import 'package:meta/meta.dart';
|
||||
import 'package:test_api/test_api.dart' as test_package show TypeMatcher, test; // ignore: deprecated_member_use
|
||||
import 'package:test_api/test_api.dart' hide TypeMatcher, isInstanceOf; // ignore: deprecated_member_use
|
||||
// ignore: deprecated_member_use
|
||||
export 'package:test_core/test_core.dart' hide TypeMatcher, isInstanceOf; // Defines a 'package:test' shim.
|
||||
export 'package:test_core/test_core.dart' hide TypeMatcher, isInstanceOf, test; // Defines a 'package:test' shim.
|
||||
|
||||
/// A matcher that compares the type of the actual value to the type argument T.
|
||||
// TODO(ianh): Remove this once https://github.com/dart-lang/matcher/issues/98 is fixed
|
||||
@ -31,7 +31,9 @@ void tryToDelete(Directory directory) {
|
||||
// on Windows it's common for deletions to fail due to
|
||||
// bogus (we think) "access denied" errors.
|
||||
try {
|
||||
directory.deleteSync(recursive: true);
|
||||
if (directory.existsSync()) {
|
||||
directory.deleteSync(recursive: true);
|
||||
}
|
||||
} on FileSystemException catch (error) {
|
||||
print('Failed to delete ${directory.path}: $error');
|
||||
}
|
||||
@ -156,6 +158,35 @@ Matcher containsIgnoringWhitespace(String toSearch) {
|
||||
);
|
||||
}
|
||||
|
||||
/// The tool overrides `test` to ensure that files created under the
|
||||
/// system temporary directory are deleted after each test by calling
|
||||
/// `LocalFileSystem.dispose()`.
|
||||
@isTest
|
||||
void test(String description, FutureOr<void> body(), {
|
||||
String testOn,
|
||||
Timeout timeout,
|
||||
dynamic skip,
|
||||
List<String> tags,
|
||||
Map<String, dynamic> onPlatform,
|
||||
int retry,
|
||||
}) {
|
||||
test_package.test(
|
||||
description,
|
||||
() async {
|
||||
addTearDown(() async {
|
||||
await LocalFileSystem.dispose();
|
||||
});
|
||||
return body();
|
||||
},
|
||||
timeout: timeout,
|
||||
skip: skip,
|
||||
tags: tags,
|
||||
onPlatform: onPlatform,
|
||||
retry: retry,
|
||||
testOn: testOn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Executes a test body in zone that does not allow context-based injection.
|
||||
///
|
||||
/// For classes which have been refactored to excluded context-based injection
|
||||
@ -168,12 +199,12 @@ Matcher containsIgnoringWhitespace(String toSearch) {
|
||||
void testWithoutContext(String description, FutureOr<void> body(), {
|
||||
String testOn,
|
||||
Timeout timeout,
|
||||
bool skip,
|
||||
dynamic skip,
|
||||
List<String> tags,
|
||||
Map<String, dynamic> onPlatform,
|
||||
int retry,
|
||||
}) {
|
||||
return test_package.test(
|
||||
return test(
|
||||
description, () async {
|
||||
return runZoned(body, zoneValues: <Object, Object>{
|
||||
contextKey: const NoContext(),
|
||||
|
@ -126,7 +126,7 @@ void testUsingContext(
|
||||
SimControl: () => MockSimControl(),
|
||||
Usage: () => FakeUsage(),
|
||||
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(),
|
||||
FileSystem: () => const LocalFileSystemBlockingSetCurrentDirectory(),
|
||||
FileSystem: () => LocalFileSystemBlockingSetCurrentDirectory(),
|
||||
TimeoutConfiguration: () => const TimeoutConfiguration(),
|
||||
PlistParser: () => FakePlistParser(),
|
||||
Signals: () => FakeSignals(),
|
||||
@ -443,7 +443,9 @@ class FakePlistParser implements PlistParser {
|
||||
}
|
||||
|
||||
class LocalFileSystemBlockingSetCurrentDirectory extends LocalFileSystem {
|
||||
const LocalFileSystemBlockingSetCurrentDirectory();
|
||||
LocalFileSystemBlockingSetCurrentDirectory() : super.test(
|
||||
signals: LocalSignals.instance,
|
||||
);
|
||||
|
||||
@override
|
||||
set currentDirectory(dynamic value) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user