Watch Bazel generated files for changes
This introduces a way to see what paths have been searched for by the `BazelWorkspace` and adds a polling-based watcher to detect when files generated by Bazel appear (or have changed). This allows us to re-analyze things automatically instead of, e.g., restarting the server. Change-Id: I60eae29b0e4fcc3a91d8d2275c6898e45548ea03 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/168649 Commit-Queue: Michal Terepeta <michalt@google.com> Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
This commit is contained in:
committed by
commit-bot@chromium.org
parent
bf26eb3c94
commit
97093b2594
@@ -31,6 +31,7 @@ import 'package:analyzer/src/source/path_filter.dart';
|
||||
import 'package:analyzer/src/task/options.dart';
|
||||
import 'package:analyzer/src/util/glob.dart';
|
||||
import 'package:analyzer/src/util/yaml.dart';
|
||||
import 'package:analyzer/src/workspace/bazel.dart';
|
||||
import 'package:analyzer_plugin/protocol/protocol_common.dart' as protocol;
|
||||
import 'package:analyzer_plugin/utilities/analyzer_converter.dart';
|
||||
import 'package:path/path.dart' as pathos;
|
||||
@@ -393,6 +394,11 @@ class ContextManagerImpl implements ContextManager {
|
||||
final Map<Folder, StreamSubscription<WatchEvent>> changeSubscriptions =
|
||||
<Folder, StreamSubscription<WatchEvent>>{};
|
||||
|
||||
/// For each root directory stores subscriptions and watchers that we
|
||||
/// established to detect changes to Bazel generated files.
|
||||
final Map<Folder, _BazelWorkspaceSubscription> bazelSubscriptions =
|
||||
<Folder, _BazelWorkspaceSubscription>{};
|
||||
|
||||
ContextManagerImpl(
|
||||
this.resourceProvider,
|
||||
this.sdkManager,
|
||||
@@ -1016,6 +1022,7 @@ class ContextManagerImpl implements ContextManager {
|
||||
contextRoot.optionsFilePath = optionsFile.path;
|
||||
}
|
||||
info.analysisDriver = callbacks.addAnalysisDriver(folder, contextRoot);
|
||||
_watchBazelFilesIfNeeded(folder, info.analysisDriver);
|
||||
if (optionsFile != null) {
|
||||
_analyzeAnalysisOptionsFile(info.analysisDriver, optionsFile.path);
|
||||
}
|
||||
@@ -1116,6 +1123,7 @@ class ContextManagerImpl implements ContextManager {
|
||||
/// Clean up and destroy the context associated with the given folder.
|
||||
void _destroyContext(ContextInfo info) {
|
||||
changeSubscriptions.remove(info.folder)?.cancel();
|
||||
bazelSubscriptions.remove(info.folder)?.cancel();
|
||||
callbacks.removeContext(info.folder, _computeFlushedFiles(info));
|
||||
var wasRemoved = info.parent.children.remove(info);
|
||||
assert(wasRemoved);
|
||||
@@ -1220,6 +1228,47 @@ class ContextManagerImpl implements ContextManager {
|
||||
return rootInfo;
|
||||
}
|
||||
|
||||
/// Establishes watch(es) for the Bazel generated files provided in
|
||||
/// [notification].
|
||||
///
|
||||
/// Whenever the files change, we trigger re-analysis. This allows us to react
|
||||
/// to creation/modification of files that were generated by Bazel.
|
||||
void _handleBazelFileNotification(
|
||||
Folder folder, BazelFileNotification notification) {
|
||||
var fileSubscriptions = bazelSubscriptions[folder].fileSubscriptions;
|
||||
if (fileSubscriptions.containsKey(notification.requested)) {
|
||||
// We have already established a Watcher for this particular path.
|
||||
return;
|
||||
}
|
||||
var watcher = notification.watcher(
|
||||
pollingDelayShort: Duration(seconds: 10),
|
||||
pollingDelayLong: Duration(seconds: 30));
|
||||
var subscription = watcher.events.listen(_handleBazelWatchEvent);
|
||||
fileSubscriptions[notification.requested] =
|
||||
_BazelFilesSubscription(watcher, subscription);
|
||||
watcher.start();
|
||||
}
|
||||
|
||||
/// Notifies the drivers that a generated Bazel file has changed.
|
||||
void _handleBazelWatchEvent(WatchEvent event) {
|
||||
if (event.type == ChangeType.ADD) {
|
||||
for (var driver in driverMap.values) {
|
||||
driver.addFile(event.path);
|
||||
// Since the file has been created after we've searched for it, the
|
||||
// URI resolution is likely wrong, so we need to reset it.
|
||||
driver.resetUriResolution();
|
||||
}
|
||||
} else if (event.type == ChangeType.MODIFY) {
|
||||
for (var driver in driverMap.values) {
|
||||
driver.changeFile(event.path);
|
||||
}
|
||||
} else if (event.type == ChangeType.REMOVE) {
|
||||
for (var driver in driverMap.values) {
|
||||
driver.removeFile(event.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handleWatchEvent(WatchEvent event) {
|
||||
callbacks.broadcastWatchEvent(event);
|
||||
_handleWatchEventImpl(event);
|
||||
@@ -1504,6 +1553,22 @@ class ContextManagerImpl implements ContextManager {
|
||||
driver.configure(sourceFactory: sourceFactory);
|
||||
}
|
||||
|
||||
/// Listens to files generated by Bazel that were found or searched for.
|
||||
///
|
||||
/// This is handled specially because the files are outside the package
|
||||
/// folder, but we still want to watch for changes to them.
|
||||
///
|
||||
/// Does nothing if the [driver] is not in a Bazel workspace.
|
||||
void _watchBazelFilesIfNeeded(Folder folder, AnalysisDriver analysisDriver) {
|
||||
var workspace = analysisDriver.analysisContext.workspace;
|
||||
if (workspace is BazelWorkspace &&
|
||||
!bazelSubscriptions.containsKey(folder)) {
|
||||
var subscription = workspace.bazelCandidateFiles.listen(
|
||||
(notification) => _handleBazelFileNotification(folder, notification));
|
||||
bazelSubscriptions[folder] = _BazelWorkspaceSubscription(subscription);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create and return a source representing the given [file] within the given
|
||||
/// [driver].
|
||||
static Source createSourceInContext(AnalysisDriver driver, File file) {
|
||||
@@ -1622,3 +1687,36 @@ class PackagesFileDisposition extends FolderDisposition {
|
||||
return _embedderLocator;
|
||||
}
|
||||
}
|
||||
|
||||
/// A watcher with subscription used to detect changes to some file.
|
||||
class _BazelFilesSubscription {
|
||||
final BazelFileWatcher watcher;
|
||||
final StreamSubscription<WatchEvent> subscription;
|
||||
|
||||
_BazelFilesSubscription(this.watcher, this.subscription);
|
||||
|
||||
void cancel() {
|
||||
subscription.cancel();
|
||||
watcher.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/// A subscription to notifications from a Bazel workspace.
|
||||
class _BazelWorkspaceSubscription {
|
||||
final StreamSubscription<BazelFileNotification> workspaceSubscription;
|
||||
|
||||
/// For each absolute path that we searched for, provides the subscriptions
|
||||
/// that we established to watch for changes.
|
||||
///
|
||||
/// Note that the absolute path used when searching for a file is not
|
||||
/// necessarily the actual path of the file (see [BazelWorkspace.findFile] for
|
||||
/// details on how the files are searched).
|
||||
final fileSubscriptions = <String, _BazelFilesSubscription>{};
|
||||
|
||||
_BazelWorkspaceSubscription(this.workspaceSubscription);
|
||||
|
||||
void cancel() {
|
||||
workspaceSubscription.cancel();
|
||||
fileSubscriptions.values.forEach((sub) => sub.cancel());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:analysis_server/protocol/protocol.dart';
|
||||
import 'package:analysis_server/protocol/protocol_constants.dart';
|
||||
import 'package:analysis_server/protocol/protocol_generated.dart';
|
||||
import 'package:analyzer_plugin/protocol/protocol_common.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:test_reflective_loader/test_reflective_loader.dart';
|
||||
|
||||
import '../analysis_abstract.dart';
|
||||
|
||||
void main() {
|
||||
defineReflectiveSuite(() {
|
||||
defineReflectiveTests(BazelChangesTest);
|
||||
});
|
||||
}
|
||||
|
||||
@reflectiveTest
|
||||
class BazelChangesTest extends AbstractAnalysisTest {
|
||||
Map<String, List<AnalysisError>> filesErrors = {};
|
||||
Completer<void> processedNotification;
|
||||
|
||||
@override
|
||||
void processNotification(Notification notification) {
|
||||
if (notification.event == ANALYSIS_NOTIFICATION_ERRORS) {
|
||||
var decoded = AnalysisErrorsParams.fromNotification(notification);
|
||||
filesErrors[decoded.file] = decoded.errors;
|
||||
processedNotification?.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void setUp() {
|
||||
super.setUp();
|
||||
|
||||
projectPath = convertPath('/workspaceRoot/third_party/dart/project');
|
||||
testFile =
|
||||
convertPath('/workspaceRoot/third_party/dart/project/lib/test.dart');
|
||||
newFile('/workspaceRoot/WORKSPACE');
|
||||
newFolder('/workspaceRoot/bazel-lib/project');
|
||||
newFolder('/workspaceRoot/bazel-genfiles/project');
|
||||
}
|
||||
|
||||
@override
|
||||
void tearDown() {
|
||||
// Make sure to destroy all the contexts and cancel all subscriptions to
|
||||
// file watchers.
|
||||
server.contextManager.setRoots([], []);
|
||||
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
Future<void> test_findingFileInGenfiles() async {
|
||||
processedNotification = Completer();
|
||||
|
||||
newFile(testFile, content: r'''
|
||||
import 'generated.dart';
|
||||
void main() { fun(); }
|
||||
''');
|
||||
createProject();
|
||||
|
||||
// We should have some errors since the `generated.dart` is not there yet.
|
||||
await processedNotification.future;
|
||||
expect(filesErrors[testFile], isNotEmpty);
|
||||
|
||||
// Clear errors, so that we'll notice new results.
|
||||
filesErrors.clear();
|
||||
processedNotification = Completer();
|
||||
|
||||
// Simulate the creation of a generated file.
|
||||
newFile(
|
||||
'/workspaceRoot/bazel-genfiles/'
|
||||
'third_party/dart/project/lib/generated.dart',
|
||||
content: 'fun() {}');
|
||||
|
||||
// No errors.
|
||||
await processedNotification.future;
|
||||
expect(filesErrors[testFile], isEmpty);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import 'package:test_reflective_loader/test_reflective_loader.dart';
|
||||
|
||||
import 'bazel_changes_test.dart' as bazel_changes;
|
||||
import 'get_errors_test.dart' as get_errors;
|
||||
import 'get_hover_test.dart' as get_hover;
|
||||
import 'get_navigation_test.dart' as get_navigation;
|
||||
@@ -26,6 +27,7 @@ import 'update_content_test.dart' as update_content;
|
||||
|
||||
void main() {
|
||||
defineReflectiveSuite(() {
|
||||
bazel_changes.main();
|
||||
get_errors.main();
|
||||
get_hover.main();
|
||||
get_navigation.main();
|
||||
|
||||
@@ -114,7 +114,10 @@ class ContextBuilder {
|
||||
if (builderOptions.librarySummaryPaths != null) {
|
||||
summaryData = SummaryDataStore(builderOptions.librarySummaryPaths);
|
||||
}
|
||||
final sf = createSourceFactory(path, summaryData: summaryData);
|
||||
Workspace workspace =
|
||||
ContextBuilder.createWorkspace(resourceProvider, path, this);
|
||||
final sf =
|
||||
createSourceFactoryFromWorkspace(workspace, summaryData: summaryData);
|
||||
|
||||
AnalysisDriver driver = AnalysisDriver(
|
||||
analysisDriverScheduler,
|
||||
@@ -143,6 +146,7 @@ class ContextBuilder {
|
||||
resourceProvider,
|
||||
apiContextRoots.first,
|
||||
driver,
|
||||
workspace: workspace,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -209,6 +213,15 @@ class ContextBuilder {
|
||||
return workspace.createSourceFactory(sdk, summaryData);
|
||||
}
|
||||
|
||||
SourceFactory createSourceFactoryFromWorkspace(Workspace workspace,
|
||||
{SummaryDataStore summaryData}) {
|
||||
DartSdk sdk = findSdk(workspace);
|
||||
if (summaryData != null && sdk is SummaryBasedDartSdk) {
|
||||
summaryData.addBundle(null, sdk.bundle);
|
||||
}
|
||||
return workspace.createSourceFactory(sdk, summaryData);
|
||||
}
|
||||
|
||||
/// Add any [declaredVariables] to the list of declared variables used by the
|
||||
/// given analysis [driver].
|
||||
void declareVariablesInDriver(AnalysisDriver driver) {
|
||||
|
||||
@@ -29,7 +29,9 @@ class DriverBasedAnalysisContext implements AnalysisContext {
|
||||
/// to access the file system and that is based on the given analysis
|
||||
/// [driver].
|
||||
DriverBasedAnalysisContext(
|
||||
this.resourceProvider, this.contextRoot, this.driver) {
|
||||
this.resourceProvider, this.contextRoot, this.driver,
|
||||
{Workspace workspace})
|
||||
: _workspace = workspace {
|
||||
driver.analysisContext = this;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// for details. All rights reserved. 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:collection';
|
||||
import 'dart:core';
|
||||
|
||||
@@ -14,8 +15,43 @@ import 'package:analyzer/src/generated/source_io.dart';
|
||||
import 'package:analyzer/src/summary/package_bundle_reader.dart';
|
||||
import 'package:analyzer/src/util/uri.dart';
|
||||
import 'package:analyzer/src/workspace/workspace.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:pub_semver/pub_semver.dart';
|
||||
import 'package:watcher/watcher.dart';
|
||||
|
||||
/// Notification that we issue in [BazelWorkspace.findFile] when searching for
|
||||
/// generated files.
|
||||
///
|
||||
/// This allows clients to watch for changes to the generated files.
|
||||
class BazelFileNotification {
|
||||
/// Candidate paths that we searched.
|
||||
///
|
||||
/// If it's a singleton, then the file was found/resolved.
|
||||
final List<String> candidates;
|
||||
|
||||
/// Absolute path that we tried searching for.
|
||||
///
|
||||
/// This is not necessarily the path of the actual file that will be used. See
|
||||
/// [BazelWorkspace.findFile] for details.
|
||||
final String requested;
|
||||
|
||||
final ResourceProvider _provider;
|
||||
|
||||
BazelFileNotification(this.requested, this.candidates, this._provider);
|
||||
|
||||
BazelFileWatcher watcher(
|
||||
{@required Duration pollingDelayShort,
|
||||
@required Duration pollingDelayLong,
|
||||
Timer Function(Duration, void Function(Timer)) timerFactory =
|
||||
_defaultTimerFactory}) =>
|
||||
BazelFileWatcher(candidates, _provider, pollingDelayShort,
|
||||
pollingDelayLong, timerFactory);
|
||||
|
||||
static Timer _defaultTimerFactory(
|
||||
Duration duration, void Function(Timer) callback) =>
|
||||
Timer.periodic(duration, callback);
|
||||
}
|
||||
|
||||
/// Instances of the class `BazelFileUriResolver` resolve `file` URI's by first
|
||||
/// resolving file uri's in the expected way, and then by looking in the
|
||||
@@ -41,6 +77,140 @@ class BazelFileUriResolver extends ResourceUriResolver {
|
||||
}
|
||||
}
|
||||
|
||||
/// Watches a list of files that should be generated by Bazel.
|
||||
///
|
||||
/// If we didn't find the file initially, we'll have more that one potential
|
||||
/// path. We'll poll them with a shorter delay waiting for at least one
|
||||
/// of the paths to become valid (we assume that users will generate the rather
|
||||
/// sooner than later since they will likely see errors due to unresolved
|
||||
/// files). Once a file appears, we switch to a different mode, where we only
|
||||
/// watch that particular file and poll it less frequently. If the file is
|
||||
/// deleted we go back to the initial mode of eager polling for all paths.
|
||||
class BazelFileWatcher {
|
||||
/// The paths of files that we watch for.
|
||||
///
|
||||
/// If it's a singleton, then the file was found/resolved by the
|
||||
/// [BazelWorkspace].
|
||||
final List<String> _candidates;
|
||||
|
||||
final _eventsController = StreamController<WatchEvent>.broadcast();
|
||||
|
||||
/// The time of last modification of the file under [_validPath].
|
||||
int _lastModified;
|
||||
|
||||
/// How often do we poll a file that we have found.
|
||||
final Duration _pollingDelayLong;
|
||||
|
||||
/// How often do we poll when none of potential files exist.
|
||||
final Duration _pollingDelayShort;
|
||||
|
||||
final ResourceProvider _provider;
|
||||
|
||||
/// One of the [_candidates] that is valid, i.e., we found a file with that
|
||||
/// path.
|
||||
String _validPath;
|
||||
|
||||
Timer _timer;
|
||||
|
||||
/// Used to contruct a [Timer] for polling.
|
||||
final Timer Function(Duration, void Function(Timer)) _timerFactory;
|
||||
|
||||
BazelFileWatcher(this._candidates, this._provider, this._pollingDelayShort,
|
||||
this._pollingDelayLong, this._timerFactory);
|
||||
|
||||
Stream<WatchEvent> get events => _eventsController.stream;
|
||||
|
||||
/// Starts watching the files.
|
||||
///
|
||||
/// To avoid missing events, the clients should first start listening on
|
||||
/// [events] and then call [start].
|
||||
void start() {
|
||||
assert(_timer == null);
|
||||
var info = _pollAll();
|
||||
if (info != null) {
|
||||
_validPath = info.path;
|
||||
_lastModified = info.modified;
|
||||
_setPollingDelayToLong();
|
||||
} else {
|
||||
_setPollingDelayToShort();
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {
|
||||
_timer.cancel();
|
||||
_eventsController.close();
|
||||
}
|
||||
|
||||
void _poll() {
|
||||
if (_eventsController.isClosed) return;
|
||||
|
||||
int modified;
|
||||
if (_validPath == null) {
|
||||
var info = _pollAll();
|
||||
if (info != null) {
|
||||
_validPath = info.path;
|
||||
modified = info.modified;
|
||||
}
|
||||
} else {
|
||||
modified = _pollOne(_validPath);
|
||||
}
|
||||
|
||||
// If there is no file, then we have nothing to do.
|
||||
if (_validPath == null) return;
|
||||
|
||||
if (modified == null && _lastModified != null) {
|
||||
// The file is no longer there, so let's issue a REMOVE event, unset
|
||||
// `_validPath` and set the timer to poll more frequently.
|
||||
_eventsController.add(WatchEvent(ChangeType.REMOVE, _validPath));
|
||||
_validPath = null;
|
||||
_setPollingDelayToShort();
|
||||
} else if (modified != null && _lastModified == null) {
|
||||
_eventsController.add(WatchEvent(ChangeType.ADD, _validPath));
|
||||
_setPollingDelayToLong();
|
||||
} else if (_lastModified != null && modified != _lastModified) {
|
||||
_eventsController.add(WatchEvent(ChangeType.MODIFY, _validPath));
|
||||
}
|
||||
_lastModified = modified;
|
||||
}
|
||||
|
||||
/// Tries polling all the possible paths.
|
||||
///
|
||||
/// Will set [_validPath] and return its modified time if a file is found.
|
||||
/// Returns [null] if nothing is found.
|
||||
FileInfo _pollAll() {
|
||||
assert(_validPath == null);
|
||||
for (var path in _candidates) {
|
||||
var modified = _pollOne(path);
|
||||
if (modified != null) {
|
||||
return FileInfo(path, modified);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns the modified time of the path or `null` if the file does not
|
||||
/// exist.
|
||||
int _pollOne(String path) {
|
||||
try {
|
||||
var file = _provider.getFile(path);
|
||||
return file.modificationStamp;
|
||||
} on FileSystemException catch (_) {
|
||||
// File doesn't exist, so return null.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void _setPollingDelayToLong() {
|
||||
_timer?.cancel();
|
||||
_timer = _timerFactory(_pollingDelayLong, (_) => _poll());
|
||||
}
|
||||
|
||||
void _setPollingDelayToShort() {
|
||||
_timer?.cancel();
|
||||
_timer = _timerFactory(_pollingDelayShort, (_) => _poll());
|
||||
}
|
||||
}
|
||||
|
||||
/// The [UriResolver] that can resolve `package` URIs in [BazelWorkspace].
|
||||
class BazelPackageUriResolver extends UriResolver {
|
||||
final BazelWorkspace _workspace;
|
||||
@@ -189,9 +359,17 @@ class BazelWorkspace extends Workspace
|
||||
/// The absolute path to the `bazel-genfiles` folder.
|
||||
final String genfiles;
|
||||
|
||||
final _bazelCandidateFiles =
|
||||
StreamController<BazelFileNotification>.broadcast();
|
||||
|
||||
BazelWorkspace._(
|
||||
this.provider, this.root, this.readonly, this.binPaths, this.genfiles);
|
||||
|
||||
/// Stream of files that we tried to find along with their potential or actual
|
||||
/// paths.
|
||||
Stream<BazelFileNotification> get bazelCandidateFiles =>
|
||||
_bazelCandidateFiles.stream;
|
||||
|
||||
@override
|
||||
bool get isBazel => true;
|
||||
|
||||
@@ -224,17 +402,16 @@ class BazelWorkspace extends Workspace
|
||||
if (relative == '.') {
|
||||
return null;
|
||||
}
|
||||
// genfiles
|
||||
if (genfiles != null) {
|
||||
File file = provider.getFile(context.join(genfiles, relative));
|
||||
if (file.exists) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
// bin
|
||||
for (String bin in binPaths) {
|
||||
File file = provider.getFile(context.join(bin, relative));
|
||||
// First check genfiles and bin directories
|
||||
var generatedCandidates = <String>[
|
||||
if (genfiles != null) genfiles,
|
||||
...?binPaths
|
||||
].map((prefix) => context.join(prefix, relative));
|
||||
for (var path in generatedCandidates) {
|
||||
File file = provider.getFile(path);
|
||||
if (file.exists) {
|
||||
_bazelCandidateFiles.add(BazelFileNotification(
|
||||
relative, generatedCandidates.toList(), provider));
|
||||
return file;
|
||||
}
|
||||
}
|
||||
@@ -250,6 +427,10 @@ class BazelWorkspace extends Workspace
|
||||
return file;
|
||||
}
|
||||
}
|
||||
// If we couldn't find the file, assume that it has not yet been
|
||||
// generated, so send an event with all the paths that we tried.
|
||||
_bazelCandidateFiles.add(BazelFileNotification(
|
||||
relative, generatedCandidates.toList(), provider));
|
||||
// Not generated, return the default one.
|
||||
return writableFile;
|
||||
} catch (_) {
|
||||
@@ -613,3 +794,9 @@ class BazelWorkspacePackage extends WorkspacePackage {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FileInfo {
|
||||
String path;
|
||||
int modified;
|
||||
FileInfo(this.path, this.modified);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ dependencies:
|
||||
dev_dependencies:
|
||||
analyzer_utilities:
|
||||
path: ../analyzer_utilities
|
||||
async: ^2.0.0
|
||||
linter: any
|
||||
matcher: ^0.12.3
|
||||
pedantic: ^1.9.0
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:analyzer/src/generated/source.dart';
|
||||
import 'package:analyzer/src/summary/package_bundle_reader.dart';
|
||||
import 'package:analyzer/src/test_utilities/resource_provider_mixin.dart';
|
||||
import 'package:analyzer/src/workspace/bazel.dart';
|
||||
import 'package:async/async.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:test_reflective_loader/test_reflective_loader.dart';
|
||||
import 'package:watcher/watcher.dart';
|
||||
|
||||
import '../../generated/test_support.dart';
|
||||
|
||||
@@ -766,6 +770,152 @@ class BazelWorkspacePackageTest with ResourceProviderMixin {
|
||||
class BazelWorkspaceTest with ResourceProviderMixin {
|
||||
BazelWorkspace workspace;
|
||||
|
||||
void test_bazelFileWatcher() async {
|
||||
_addResources([
|
||||
'/workspace/WORKSPACE',
|
||||
]);
|
||||
_MockTimer timer;
|
||||
var timerFactory = (Duration _, void Function(Timer) callback) {
|
||||
timer = _MockTimer(callback);
|
||||
return timer;
|
||||
};
|
||||
var candidates = [
|
||||
convertPath('/workspace/bazel-bin/my/module/test1.dart'),
|
||||
convertPath('/workspace/bazel-genfiles/my/module/test1.dart'),
|
||||
];
|
||||
var watcher = BazelFileWatcher(candidates, resourceProvider, Duration.zero,
|
||||
Duration.zero, timerFactory);
|
||||
var events = StreamQueue(watcher.events);
|
||||
watcher.start();
|
||||
|
||||
// First do some tests with the first candidate path.
|
||||
_addResources([candidates[0]]);
|
||||
timer.triggerCallback();
|
||||
|
||||
var event = await events.next;
|
||||
expect(event.type, ChangeType.ADD);
|
||||
expect(event.path, candidates[0]);
|
||||
|
||||
modifyFile(candidates[0], 'const foo = 42;');
|
||||
timer.triggerCallback();
|
||||
|
||||
event = await events.next;
|
||||
expect(event.type, ChangeType.MODIFY);
|
||||
expect(event.path, candidates[0]);
|
||||
|
||||
_deleteResources([candidates[0]]);
|
||||
timer.triggerCallback();
|
||||
|
||||
event = await events.next;
|
||||
expect(event.type, ChangeType.REMOVE);
|
||||
expect(event.path, candidates[0]);
|
||||
|
||||
// Now check that if we add the *second* candidate, we'll get the
|
||||
// notification for it.
|
||||
_addResources([candidates[1]]);
|
||||
timer.triggerCallback();
|
||||
|
||||
event = await events.next;
|
||||
expect(event.type, ChangeType.ADD);
|
||||
expect(event.path, candidates[1]);
|
||||
|
||||
watcher.stop();
|
||||
expect(await events.rest.isEmpty, true);
|
||||
}
|
||||
|
||||
void test_bazelFileWatcher_existingFile() async {
|
||||
_addResources([
|
||||
'/workspace/WORKSPACE',
|
||||
'/workspace/bazel-bin/my/module/test1.dart',
|
||||
]);
|
||||
BazelWorkspace workspace = BazelWorkspace.find(
|
||||
resourceProvider, convertPath('/workspace/my/module'));
|
||||
_MockTimer timer;
|
||||
var timerFactory = (Duration _, void Function(Timer) callback) {
|
||||
timer = _MockTimer(callback);
|
||||
return timer;
|
||||
};
|
||||
var watcherCompleter = Completer<BazelFileWatcher>();
|
||||
workspace.bazelCandidateFiles.listen((notification) =>
|
||||
watcherCompleter.complete(notification.watcher(
|
||||
pollingDelayLong: Duration.zero,
|
||||
pollingDelayShort: Duration.zero,
|
||||
timerFactory: timerFactory)));
|
||||
|
||||
var file1 =
|
||||
workspace.findFile(convertPath('/workspace/my/module/test1.dart'));
|
||||
expect(file1.exists, true);
|
||||
var watcher = await watcherCompleter.future;
|
||||
var events = StreamQueue(watcher.events);
|
||||
watcher.start();
|
||||
|
||||
// Make sure that triggering the callback, will not generate extra events.
|
||||
timer.triggerCallback();
|
||||
|
||||
var convertedPath =
|
||||
convertPath('/workspace/bazel-bin/my/module/test1.dart');
|
||||
|
||||
// Change the file -- we should get a single MODIFY event and not an ADD
|
||||
// event, since the file already existed.
|
||||
modifyFile(convertedPath, 'const foo = 42;');
|
||||
timer.triggerCallback();
|
||||
var event = await events.next;
|
||||
|
||||
expect(event.type, ChangeType.MODIFY);
|
||||
expect(event.path, convertedPath);
|
||||
|
||||
// But if we delete the file and then re-create it, we should get an ADD
|
||||
// event (after the REMOVE one).
|
||||
deleteFile(convertedPath);
|
||||
timer.triggerCallback();
|
||||
event = await events.next;
|
||||
|
||||
expect(event.type, ChangeType.REMOVE);
|
||||
expect(event.path, convertedPath);
|
||||
|
||||
newFile(convertedPath);
|
||||
timer.triggerCallback();
|
||||
event = await events.next;
|
||||
|
||||
expect(event.type, ChangeType.ADD);
|
||||
expect(event.path, convertedPath);
|
||||
|
||||
watcher.stop();
|
||||
expect(await events.rest.isEmpty, true);
|
||||
}
|
||||
|
||||
void test_bazelNotifications() async {
|
||||
_addResources([
|
||||
'/workspace/WORKSPACE',
|
||||
'/workspace/bazel-bin/my/module/test1.dart',
|
||||
]);
|
||||
BazelWorkspace workspace = BazelWorkspace.find(
|
||||
resourceProvider, convertPath('/workspace/my/module'));
|
||||
var notifications = StreamQueue(workspace.bazelCandidateFiles);
|
||||
|
||||
var file1 =
|
||||
workspace.findFile(convertPath('/workspace/my/module/test1.dart'));
|
||||
expect(file1.exists, true);
|
||||
var notification = await notifications.next;
|
||||
expect(notification.requested, convertPath('my/module/test1.dart'));
|
||||
expect(
|
||||
notification.candidates,
|
||||
containsAll(
|
||||
[convertPath('/workspace/bazel-bin/my/module/test1.dart')]));
|
||||
|
||||
var file2 =
|
||||
workspace.findFile(convertPath('/workspace/my/module/test2.dart'));
|
||||
expect(file2.exists, false);
|
||||
notification = await notifications.next;
|
||||
expect(notification.requested, convertPath('my/module/test2.dart'));
|
||||
expect(
|
||||
notification.candidates,
|
||||
containsAll([
|
||||
convertPath('/workspace/bazel-bin/my/module/test2.dart'),
|
||||
convertPath('/workspace/bazel-genfiles/my/module/test2.dart'),
|
||||
]));
|
||||
}
|
||||
|
||||
void test_find_fail_notAbsolute() {
|
||||
expect(
|
||||
() =>
|
||||
@@ -1015,6 +1165,17 @@ class BazelWorkspaceTest with ResourceProviderMixin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new files and directories from [paths].
|
||||
void _deleteResources(List<String> paths) {
|
||||
for (String path in paths) {
|
||||
if (path.endsWith('/')) {
|
||||
deleteFolder(path.substring(0, path.length - 1));
|
||||
} else {
|
||||
deleteFile(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Expect that [BazelWorkspace.findFile], given [path], returns [equals].
|
||||
void _expectFindFile(String path, {@required String equals}) =>
|
||||
expect(workspace.findFile(convertPath(path)).path, convertPath(equals));
|
||||
@@ -1031,3 +1192,20 @@ class _MockSource implements Source {
|
||||
throw StateError('Unexpected invocation of ${invocation.memberName}');
|
||||
}
|
||||
}
|
||||
|
||||
class _MockTimer implements Timer {
|
||||
final void Function(Timer) callback;
|
||||
|
||||
@override
|
||||
bool isActive = true;
|
||||
|
||||
_MockTimer(this.callback);
|
||||
|
||||
@override
|
||||
int get tick => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
void cancel() => isActive = false;
|
||||
|
||||
void triggerCallback() => callback(this);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user