blob: da935aff7f76273f3e000d039e2b9d8bf4253814 [file] [log] [blame]
// Copyright (c) 2021, 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 'dart:io';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../support/integration_tests.dart';
void main() {
// Skip on Windows.
if (Platform.isWindows) {
return;
}
defineReflectiveSuite(() {
defineReflectiveTests(BazelChangesTest);
});
}
@reflectiveTest
class BazelChangesTest extends AbstractAnalysisServerIntegrationTest {
var processedNotification = Completer<void>();
/// Path to the `command.log` file.
///
/// Writing to it should trigger our change detection to run.
late String commandLogPath;
late String bazelRoot;
late String tmpPath;
late String workspacePath;
late String bazelOrBlazeOutPath;
late String bazelOrBlazeBinPath;
late String bazelOrBlazeGenfilesPath;
late Directory oldSourceDirectory;
String inTmpDir(String relative) =>
path.join(tmpPath, relative.replaceAll('/', path.separator));
String inWorkspace(String relative) =>
path.join(workspacePath, relative.replaceAll('/', path.separator));
@override
Future setUp() async {
await super.setUp();
oldSourceDirectory = sourceDirectory;
tmpPath = Directory(Directory.systemTemp
.createTempSync('analysisServer')
.resolveSymbolicLinksSync())
.path;
workspacePath = inTmpDir('workspace_root');
writeFile(inWorkspace('WORKSPACE'), '');
sourceDirectory = Directory(inWorkspace('third_party/dart/project'));
sourceDirectory.createSync(recursive: true);
bazelRoot = inTmpDir('bazel_or_blaze_root');
Directory(bazelRoot).createSync(recursive: true);
bazelOrBlazeOutPath =
'$bazelRoot/execroot/bazel_or_blaze_workspace/bazel_or_blaze-out';
bazelOrBlazeBinPath =
'$bazelRoot/execroot/bazel_or_blaze_workspace/bazel_or_blaze-out/bin';
bazelOrBlazeGenfilesPath =
'$bazelRoot/execroot/bazel_or_blaze_workspace/bazel_or_blaze-out/genfiles';
Directory(inTmpDir(bazelOrBlazeOutPath)).createSync(recursive: true);
Directory(inTmpDir(bazelOrBlazeBinPath)).createSync(recursive: true);
Directory(inTmpDir(bazelOrBlazeGenfilesPath)).createSync(recursive: true);
commandLogPath = inTmpDir('$bazelRoot/command.log');
}
@override
Future tearDown() async {
Directory(tmpPath).deleteSync(recursive: true);
sourceDirectory = oldSourceDirectory;
await super.tearDown();
}
// Add a bit more time -- the isolate take a while to start when the test is
// not run from a snapshot.
@TestTimeout(Timeout.factor(2))
Future<void> test_bazelChanges() async {
await testChangesImpl('bazel');
}
// Add a bit more time -- the isolate take a while to start when the test is
// not run from a snapshot.
@TestTimeout(Timeout.factor(2))
Future<void> test_blazeChanges() async {
await testChangesImpl('blaze');
}
Future<void> testChangesImpl(String prefix) async {
var testFile = inWorkspace('${sourceDirectory.path}/lib/test.dart');
var errors = <AnalysisError>[];
onAnalysisErrors.listen((event) {
if (event.file == testFile) {
errors.addAll(event.errors);
processedNotification.complete();
}
});
var resetCompleterAndErrors = () async {
// This is necessary because our polling uses modification timestamps
// whose resolution seems to be too small for a test like this (i.e., we
// write to the `command.log` file, but if the modification timestamp
// doesn't change, we won't detect the change).
await Future.delayed(Duration(seconds: 1));
errors.clear();
processedNotification = Completer();
};
writeFile(testFile, r'''
import 'generated.dart';
void main() { my_fun(); }
''');
standardAnalysisSetup();
await processedNotification.future;
expect(errors, isNotEmpty);
expect(errors[0].message, contains('generated.dart'));
// This seems to be necessary (at least when running the test from source),
// because it takes a while for the watcher isolate to start.
await Future.delayed(Duration(seconds: 10));
await resetCompleterAndErrors();
var generatedFilePath = inWorkspace(
'$bazelOrBlazeGenfilesPath/third_party/dart/project/lib/generated.dart');
writeFile(generatedFilePath, 'my_fun() {}');
_createSymlinks(prefix);
writeFile(commandLogPath, 'Build completed successfully');
await processedNotification.future;
expect(errors, isEmpty);
// Now let's write a file that does not define `my_fun` -- we should get an
// error again.
await resetCompleterAndErrors();
writeFile(generatedFilePath, 'different_fun() {}');
writeFile(commandLogPath, 'Build completed');
await processedNotification.future;
expect(errors, isNotEmpty);
// Now delete the file completely.
await resetCompleterAndErrors();
File(generatedFilePath).deleteSync();
_deleteSymlinks(prefix);
writeFile(commandLogPath, 'Build did NOT complete successfully');
await processedNotification.future;
expect(errors, isNotEmpty);
// And finally re-add the correct file -- errors should go away once again.
await resetCompleterAndErrors();
writeFile(generatedFilePath, 'my_fun() {}');
_createSymlinks(prefix);
writeFile(commandLogPath, 'Build completed successfully');
await processedNotification.future;
expect(errors, isEmpty);
}
void _createSymlinks(String prefix) {
Link(inWorkspace('$prefix-out')).createSync(bazelOrBlazeOutPath);
Link(inWorkspace('$prefix-bin')).createSync(bazelOrBlazeBinPath);
Link(inWorkspace('$prefix-genfiles')).createSync(bazelOrBlazeGenfilesPath);
}
void _deleteSymlinks(String prefix) {
Link(inWorkspace('$prefix-out')).deleteSync();
Link(inWorkspace('$prefix-bin')).deleteSync();
Link(inWorkspace('$prefix-genfiles')).deleteSync();
}
}