blob: a5afb152eb20110199b9b91e1b8a3e7bbe9c7674 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore_for_file: implementation_imports
// ignore_for_file: invalid_use_of_visible_for_testing_member
// TODO(#1987): rewrite these tests for the flutter app.
/*
@TestOn('vm')
import 'dart:async';
import 'dart:ui';
import 'package:devtools_app/src/inspector/flutter_widget.dart';
import 'package:devtools_app/src/inspector/inspector_service.dart';
import 'package:devtools_app/src/inspector/inspector_tree.dart';
import 'package:devtools_app/src/logging/logging_controller.dart';
import 'package:devtools_app/src/table_data.dart';
import 'package:test/test.dart';
import 'support/fake_inspector_tree.dart';
import 'support/file_utils.dart';
import 'support/flutter_test_environment.dart';
/// If this test starts timing out it probably means the number of log messages
/// received has changed. Enable this flag to see what log messages are being
/// sent to debug the issue.
bool debugLogMessages = false;
/// Broadcast stream of log status messages received.
StreamController<String> logStatusController;
/// Broadcast stream of log data with gc events filtered out to keep tests predictable.
StreamController<LogData> logDataController;
// Broadcast stream with an event each time the details selection changes.
StreamController<void> detailsSelectionController;
List<LogData> unhandledLogData = [];
Stream<String> get logStatusStream => logStatusController.stream;
Stream<LogData> get logDataStream => logDataController.stream;
Stream<void> get detailsSelectionStream => detailsSelectionController.stream;
/// Current inspector tree displayed in the details view if any
FakeInspectorTree currentDetailsTree;
/// Current details text shown in the details view if any.
String currentDetailsText;
/// Returns a future after length new log data entries that are not GC events
/// have been added to the controller.
Future<List<LogData>> waitForNextLogData(int length) async {
if (unhandledLogData.length < length) {
await for (LogData value in logDataStream) {
assert(value == unhandledLogData.last);
if (unhandledLogData.length >= length) break;
}
}
final ret = unhandledLogData;
unhandledLogData = [];
return ret;
}
Future<void> runLoggingControllerTests(FlutterTestEnvironment env) async {
// Required as the logging view depends on the inspector which needs a version
// of the widget catalog.
Catalog.setCatalog(Catalog.decode(await widgetsJson()));
final detailsValuesSet = <LogData>[];
LoggingController loggingController;
// Whether the log view is currently visible.
bool visible;
InspectorService inspectorService;
env.afterNewSetup = () async {
await ensureInspectorServiceDependencies();
};
env.afterEverySetup = () async {
inspectorService = await InspectorService.create(env.service);
visible = false;
detailsValuesSet.clear();
logStatusController = StreamController.broadcast();
logDataController = StreamController.broadcast();
detailsSelectionController = StreamController.broadcast();
{
final logDataItemsSeen = <LogData>{};
loggingController = LoggingController(
onLogCountStatusChanged: (logStatus) {
logStatusController.add(logStatus);
// Brute force approach to find all new log data entries.
for (var row in loggingController.data) {
if (logDataItemsSeen.add(row) && row.kind != 'gc') {
if (debugLogMessages) {
print(
'Received LogMessage(kind: ${row.kind}, summary: ${row.summary})');
}
logDataController.add(row);
unhandledLogData.add(row);
}
}
},
isVisible: () => visible,
);
loggingController.detailsController = LoggingDetailsController(
onShowInspector: () {},
onShowDetails: ({String text, InspectorTreeController tree}) {
currentDetailsTree = tree;
currentDetailsText = text;
detailsSelectionController.add(null);
},
createLoggingTree: ({VoidCallback onSelectionChange}) {
return FakeInspectorTree()
..config = InspectorTreeConfig(
summaryTree: false,
treeType: FlutterTreeType.widget,
onNodeAdded: (_, __) {},
onSelectionChange: onSelectionChange,
onExpand: (_) {},
onHover: (_) {},
);
},
);
}
loggingController.loggingTableModel = TableData();
if (env.reuseTestEnvironment) {
// TODO(jacobr): should we reset the service extension settings?
}
visible = true;
};
env.beforeEveryTearDown = () async {
loggingController?.dispose();
loggingController = null;
inspectorService?.dispose();
inspectorService = null;
};
group('logging controller tests', () {
test('structured errors', () async {
// Enable structured errors.
await serviceManager.serviceExtensionManager.setServiceExtensionState(
structuredErrors.extension,
true,
true,
);
// Clear the logging messages to avoid unpredictability about what point
// the debugging tool was attached.
loggingController.clear();
unhandledLogData.clear();
final evalOnDartLibrary = EvalOnDartLibrary(
['package:flutter_error_app/main.dart'],
env.service,
);
await evalOnDartLibrary.eval(
'print("Example message A',
isAlive: null,
);
await evalOnDartLibrary.eval(
'print("Example message B',
isAlive: null,
);
var logEntries = await waitForNextLogData(2);
expect(logEntries[0].kind, equals('stdout'));
expect(logEntries[0].summary, equals('Example message A\n'));
expect(logEntries[1].kind, equals('stdout'));
expect(logEntries[1].summary, equals('Example message B\n'));
await evalOnDartLibrary.eval(
'navigateToScreen("Missing Material Example")',
isAlive: null,
);
logEntries = await waitForNextLogData(3);
expect(logEntries[0].kind, 'flutter.navigation');
expect(logEntries[0].summary, equals('MaterialPageRoute<dynamic>(null)'));
expect(logEntries[0].node, isNull);
{
final row = logEntries[1];
expect(row.kind, 'flutter.error');
// This error message will change when we apply further cleanup to the error messages.
expect(row.summary, 'No Material widget found.');
expect(row.node, isNotNull);
final index = loggingController.loggingTableModel.data.indexOf(row);
// The logging table may contain
// other rows related to GC that we aren't interested in.
expect(index, greaterThanOrEqualTo(2));
expect(currentDetailsTree, isNull);
final selectionChanged = detailsSelectionStream.first;
// Trigger the details selection change.
loggingController.loggingTableModel.setSelection(row, index);
await selectionChanged;
expect(currentDetailsText, isNull);
expect(currentDetailsTree, isNotNull);
expect(
normalizeErrorText(currentDetailsTree.toStringDeep()),
equalsGoldenIgnoringHashCodes(
'logging_controller_material_error.txt',
),
);
}
{
final row = logEntries[2];
expect(row.kind, 'flutter.error');
expect(
row.summary,
matches(RegExp(
r'A RenderFlex overflowed by \d+ pixels on the bottom.')));
expect(row.node, isNotNull);
final index = loggingController.loggingTableModel.data.indexOf(row);
// The logging table may contain
// other rows related to GC that we aren't interested in.
expect(index, greaterThanOrEqualTo(4));
final selectionChanged = detailsSelectionStream.first;
// Trigger the details selection change.
loggingController.loggingTableModel.setSelection(row, index);
await selectionChanged;
expect(currentDetailsText, isNull);
expect(currentDetailsTree, isNotNull);
expect(
normalizeErrorText(currentDetailsTree.toStringDeep()),
equalsGoldenIgnoringHashCodes(
'logging_controller_overflow_error.txt',
),
);
}
{
final row = logEntries[0];
expect(row.kind, 'flutter.navigation');
final index = loggingController.loggingTableModel.data.indexOf(row);
// The logging table may contain
// other rows related to GC that we aren't interested in.
expect(index, greaterThanOrEqualTo(0));
final selectionChanged = detailsSelectionStream.first;
// Trigger the details selection change.
loggingController.loggingTableModel.setSelection(row, index);
await selectionChanged;
expect(
normalizeErrorText(currentDetailsText),
equalsGoldenIgnoringHashCodes(
'logging_controller_navigation.txt',
),
);
expect(currentDetailsTree, isNull);
}
await env.tearDownEnvironment();
});
}, timeout: const Timeout.factor(8));
}
/// Normalize text in error messages that is likely unstable.
String normalizeErrorText(String message) {
final lines = message.split('\n');
return lines.map((line) {
if (line.contains('file:///') || line.contains('package:')) {
return '<STACK_TRACE_LINE>';
}
line = line.replaceAll(RegExp(r'#[0-9a-f]{5}'), '#00000');
return line.replaceAll(RegExp(r'\d+(\.\d+)?'), '<NUMBER>');
}).join('\n');
}
*/