blob: 626abb360c9445bd7dfdf00f531df3d43b6f4194 [file]
// 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 'dart:convert';
import 'dart:io' as io;
import 'package:dartdev/dartdev.dart';
import 'package:dartdev/src/unified_analytics.dart';
import 'package:file/memory.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
// Needed to reset the global HTTP client after a test.
import 'package:pub/src/http.dart' as pub show withHttpClient;
import 'package:test/test.dart';
import 'package:unified_analytics/unified_analytics.dart';
import 'experiment_util.dart';
import 'utils.dart';
List<Map<String, Object?>> extractAnalytics(io.ProcessResult result) {
return LineSplitter.split(
result.stderr,
).where((line) => line.startsWith('[analytics]: ')).map((line) {
return (json.decode(line.substring('[analytics]: '.length)) as Map)
.cast<String, Object?>();
}).toList();
}
void main() {
final experiments = experimentsWithValidation();
group('VM -> CLI flag smoke test:', () {
late DartdevRunner command;
setUp(() {
command = DartdevRunner([], isAnalyticsTest: true);
});
test('--no-analytics', () async {
final result = await command.runCommand(
command.parse(['--no-analytics']),
);
expect(result, 0);
expect(command.unifiedAnalytics.telemetryEnabled, false);
});
test('--suppress-analytics', () async {
final result = await command.runCommand(
command.parse(['--suppress-analytics']),
);
expect(result, 0);
expect(command.unifiedAnalytics.telemetryEnabled, false);
});
test('--suppress-analytics and --disable-analytics', () async {
final result = await command.runCommand(
command.parse(['--suppress-analytics', '--disable-analytics']),
);
// --suppress-analytics and --disable-analytics can't be provided
// together to ensure analytics state properly sticks.
expect(result, 254);
});
test('--suppress-analytics and --enable-analytics', () async {
final result = await command.runCommand(
command.parse(['--suppress-analytics', '--enable-analytics']),
);
// --suppress-analytics and --enable-analytics can't be provided
// together to ensure analytics state properly sticks.
expect(result, 254);
});
});
group('Sending analytics', () {
test('help', () async {
final p = project();
final analytics = await p.runLocalWithFakeAnalytics(['help']);
expect(analytics.sentEvents, [
Event.dartCliCommandExecuted(name: 'help', enabledExperiments: ''),
]);
});
test('create', () async {
final p = project();
final analytics = await p.runLocalWithFakeAnalytics([
'create',
'--no-pub',
'-tpackage-simple',
path.join(io.Directory.systemTemp.createTempSync().path, 'name'),
]);
expect(analytics.sentEvents, [
Event.dartCliCommandExecuted(name: 'create', enabledExperiments: ''),
]);
});
group('pub', () {
test(
'get',
() async {
final p = project(
pubspecExtras: {
'dependencies': {'lints': '2.0.1'},
},
);
final analytics = await p.runLocalWithFakeAnalytics(['pub', 'get']);
// Pub no longer sends custom analytics,
// so only the command should be sent.
expect(analytics.sentEvents, [
Event.dartCliCommandExecuted(
name: 'pub/get',
enabledExperiments: '',
),
]);
},
// This test does a pub get, so it might run slow. Consider adding
// retries if necessary.
timeout: Timeout.factor(5),
);
});
test('format', () async {
final p = project();
final analytics = await p.runLocalWithFakeAnalytics([
'format',
'-l80',
'.',
]);
expect(analytics.sentEvents, [
Event.dartCliCommandExecuted(name: 'format', enabledExperiments: ''),
]);
});
test('run', () async {
final p = project(mainSrc: 'void main(List<String> args) => print(args)');
await pub.withHttpClient(client: http.Client(), () async {
final analytics = await p.runLocalWithFakeAnalytics([
'run',
'--no-pause-isolates-on-exit',
'--enable-asserts',
'lib/main.dart',
'--argument',
]);
expect(analytics.sentEvents, [
Event.dartCliCommandExecuted(name: 'run', enabledExperiments: ''),
]);
});
});
group('run --enable-experiments', () {
for (final experiment in experiments) {
test(experiment.name, () async {
final p = project(mainSrc: experiment.validation);
{
for (final no in ['', 'no-']) {
await pub.withHttpClient(client: http.Client(), () async {
final analytics = await p.runLocalWithFakeAnalytics([
'run',
'--enable-experiment=$no${experiment.name}',
'lib/main.dart',
]);
expect(analytics.sentEvents, [
Event.dartCliCommandExecuted(
name: 'run',
enabledExperiments: '$no${experiment.name}',
),
]);
});
}
}
});
}
});
test('compile', () async {
final p = project(
mainSrc: 'void main(List<String> args) => print(args);',
);
final analytics = await p.runLocalWithFakeAnalytics([
'compile',
'kernel',
'lib/main.dart',
'-o',
'main.kernel',
]);
expect(analytics.sentEvents, [
Event.dartCliCommandExecuted(
name: 'compile/kernel',
enabledExperiments: '',
),
]);
});
});
group('Analytics hang test', () {
test('close hangs but runner finishes quickly', () async {
final fs = MemoryFileSystem.test(style: FileSystemStyle.posix);
final homeDirectory = fs.directory('/');
final fakeAnalytics = Analytics.fake(
tool: DashTool.dartTool,
homeDirectory: homeDirectory,
dartVersion: 'dartVersion',
fs: fs,
);
final hangingAnalytics = HangingAnalytics(fakeAnalytics);
final runner = DartdevRunner(
['--no-analytics'],
analyticsOverride: hangingAnalytics,
);
final stopwatch = Stopwatch()..start();
final result = await runner.runCommand(runner.parse(['--no-analytics']));
stopwatch.stop();
expect(result, 0);
// The timeout is 250ms, so it should definitely finish in less
// than 1 second.
// If it hung, it would take much longer or never finish.
expect(stopwatch.elapsedMilliseconds, lessThan(1000));
});
});
group('unified analytics helpers:', () {
test('sanitizeStacktrace strips file paths and compresses whitespace', () {
const rawStack =
'#0 main (file:///Users/username/project/bin/main.dart:10:5)\n'
'#1 helper (file:///home/user/app/lib/helper.dart:2:1)';
final sanitized = sanitizeStacktrace(rawStack, shorten: true);
expect(sanitized, isNot(contains('/Users/username/project/bin/')));
expect(sanitized, contains('main.dart'));
expect(sanitized, contains('helper.dart'));
});
test('getDartStorageDirectory resolves user .dart directory', () {
final dir = getDartStorageDirectory();
expect(dir, isNotNull);
// Ensure the directory exists and its basename is '.dart'.
expect(dir!.existsSync(), isTrue);
expect(path.basename(dir.path), '.dart');
});
});
group('isBot environment detection:', () {
test('isBot function executes without error', () {
// isBot should execute synchronously without throwing
expect(() => isBot(), returnsNormally);
});
});
}
class HangingAnalytics implements Analytics {
final FakeAnalytics _delegate;
final Completer<void> _closeCompleter = Completer<void>();
HangingAnalytics(this._delegate);
@override
Future<void> close({int delayDuration = 250}) => _closeCompleter.future;
@override
bool get shouldShowMessage => _delegate.shouldShowMessage;
@override
String get getConsentMessage => _delegate.getConsentMessage;
@override
void clientShowedMessage() => _delegate.clientShowedMessage();
@override
Future<void> setTelemetry(bool value) => _delegate.setTelemetry(value);
@override
bool get telemetryEnabled => _delegate.telemetryEnabled;
@override
Future<http.Response>? send(Event event) {
_delegate.send(event);
return null;
}
@override
dynamic noSuchMethod(Invocation invocation) {
super.noSuchMethod(invocation);
}
}