blob: 0dc3aaf3a5b86293b66ee1900d27528a296f85e2 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. 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' show SplayTreeMap;
import 'dart:io';
import 'package:collection/collection.dart' show SetEquality;
import 'package:test/test.dart';
import 'package:fidl/fidl.dart';
import 'package:fidl_fuchsia_io/fidl_async.dart';
import 'package:fidl_fuchsia_sys/fidl_async.dart';
import 'package:fidl_fuchsia_ui_app/fidl_async.dart';
import 'package:fidl_fuchsia_ui_policy/fidl_async.dart';
import 'package:fidl_fuchsia_ui_scenic/fidl_async.dart';
import 'package:fidl_fuchsia_ui_views/fidl_async.dart';
import 'package:fuchsia_services/services.dart';
import 'package:pedantic/pedantic.dart';
import 'package:zircon/zircon.dart';
const _testAppUrl =
'fuchsia-pkg://fuchsia.com/flutter_screencap_test_app#meta/flutter_screencap_test_app.cmx';
const _basemgrUrl = 'fuchsia-pkg://fuchsia.com/basemgr#meta/basemgr.cmx';
const _ermineUrl = 'fuchsia-pkg://fuchsia.com/ermine#meta/ermine.cmx';
// Use a custom timeout rather than the test framework's timeout so that we can
// output a sensible failure message.
final Duration _timeout = Duration(seconds: 15);
const int _blankColor = 0x00000000;
// See lib/main.dart.
final Set<int> _expectedTopTwoColors = {0xFF4dac26, 0xFFd01c8b};
EventPair _createPresentationViewToken() {
final viewTokens = EventPairPair();
assert(viewTokens.status == ZX.OK);
final presenter = PresenterProxy();
try {
StartupContext.fromStartupInfo().incoming.connectToService(presenter);
presenter.presentView(ViewHolderToken(value: viewTokens.second), null);
return viewTokens.first;
} finally {
presenter.ctrl.close();
}
}
Future<bool> _screenshotUntil(ScenicProxy scenic,
bool Function(Scenic$TakeScreenshot$Response) condition) async {
final Stopwatch stopwatch = Stopwatch()..start();
while (stopwatch.elapsed < _timeout) {
if (condition(await scenic.takeScreenshot().timeout(_timeout))) {
return true;
}
}
return false;
}
int _argbFromBgra(List<int> bgra) {
return bgra[0] | bgra[1] << 8 | bgra[2] << 16 | bgra[3] << 24;
}
// Produces a map of 32-bit ARGB colors to pixel counts.
Map<int, int> _computeHistogram(ScreenshotData screenshot) {
final bytes = screenshot.data.vmo.map();
final Map<int, int> histogram = {};
for (int i = 0; i < screenshot.data.size; i += 4) {
// Convert from BGRA to ARGB for consistency with Flutter.
final color = _argbFromBgra(bytes.sublist(i, i + 4));
histogram[color] = (histogram[color] ?? 0) + 1;
}
return histogram;
}
// Produces a sorted map of pixel counts to 32-bit ARGB colors.
SplayTreeMap<int, Set<int>> _invertHistogram(Map<int, int> histogram) {
final sortedHistogram =
SplayTreeMap<int, Set<int>>((count1, count2) => count2.compareTo(count1));
for (final entry in histogram.entries) {
sortedHistogram.putIfAbsent(entry.value, () => {}).add(entry.key);
}
return sortedHistogram;
}
// Displays the test app using root presenter. This should be called from
// within a try/finally or similar construct that closes the component
// controller.
Future<void> _startAppAsRootView(
InterfaceRequest<ComponentController> controllerRequest) async {
final context = StartupContext.fromStartupInfo();
final directory = DirectoryProxy();
final launchInfo = LaunchInfo(
url: _testAppUrl,
directoryRequest: directory.ctrl.request().passChannel());
await context.launcher.createComponent(launchInfo, controllerRequest);
final viewProvider = ViewProviderProxy();
final incoming = Incoming(directory);
try {
incoming.connectToService(viewProvider);
await viewProvider.createView(_createPresentationViewToken(), null, null);
} finally {
viewProvider.ctrl.close();
unawaited(incoming.close());
}
}
// Starts basemgr with dev shells. This should be called from within a
// try/finally or similar construct that closes the component controller.
Future<void> _startDevBasemgr(
InterfaceRequest<ComponentController> controllerRequest) async {
final context = StartupContext.fromStartupInfo();
final launchInfo = LaunchInfo(url: _basemgrUrl, arguments: [
'--base_shell=fuchsia-pkg://fuchsia.com/dev_base_shell#meta/dev_base_shell.cmx',
'--session_shell=fuchsia-pkg://fuchsia.com/dev_session_shell#meta/dev_session_shell.cmx',
'--session_shell_args=--root_module=$_testAppUrl',
'--story_shell=fuchsia-pkg://fuchsia.com/dev_story_shell#meta/dev_story_shell.cmx',
'--test',
'--enable_presenter',
'--run_base_shell_with_test_runner=false'
]);
await context.launcher.createComponent(launchInfo, controllerRequest);
}
// Starts the basemgr configured to launch the Ermine session shell. This
// should be called from within a try/finally or similar construct that closes
// the component controller.
Future<void> _startErmine(
InterfaceRequest<ComponentController> controllerRequest) async {
final context = StartupContext.fromStartupInfo();
final launchInfo =
LaunchInfo(url: _basemgrUrl, arguments: ['--session_shell=$_ermineUrl']);
await context.launcher.createComponent(launchInfo, controllerRequest);
}
// Blank can manifest as invalid screenshots or blackness.
Future<bool> _waitForBlank(ScenicProxy scenic) {
return _screenshotUntil(scenic, (response) {
if (!response.success) {
return true;
} else {
final histogram = _computeHistogram(response.imgData);
return histogram.isEmpty ||
histogram.length == 1 && histogram.keys.single == _blankColor;
}
});
}
// Verifies that the top colors displayed on the screen are the [expected] set
// of 32-bit ARGB colors.
Future<void> _expectTopColors(ScenicProxy scenic, Set<int> expected) async {
final Set<int> topColors = {};
await _screenshotUntil(scenic, (response) {
if (!response.success) {
return false;
}
topColors.clear();
// reduce while; takeWhile is lazy but reduce doesn't short circuit.
_invertHistogram(_computeHistogram(response.imgData))
.values
.takeWhile(
(colors) => (topColors..addAll(colors)).length < expected.length)
.length; // Evaluate length to force the lazy evaluation.
return SetEquality().equals(topColors, expected);
});
expect(topColors, expected);
}
void main() {
final scenic = ScenicProxy();
setUpAll(
() => StartupContext.fromStartupInfo().incoming.connectToService(scenic));
tearDownAll(scenic.ctrl.close);
setUp(() => _waitForBlank(scenic));
// This test uses root presenter to display the flutter screencap test app.
test('flutter screencap as root view should have expected top two colors',
() async {
final controller = ComponentControllerProxy();
try {
await _startAppAsRootView(controller.ctrl.request());
await _expectTopColors(scenic, _expectedTopTwoColors);
} finally {
controller.ctrl.close();
}
});
test(
'flutter screencap as root mod in dev shells should have expected top two colors',
() async {
final controller = ComponentControllerProxy();
try {
await _startDevBasemgr(controller.ctrl.request());
await _expectTopColors(scenic, _expectedTopTwoColors);
} finally {
controller.ctrl.close();
}
});
// This test starts Ermine session shell and uses sessionctl to add the
// flutter screencap test app.
test('flutter screencap as mod in Ermine should have expected top two colors',
() async {
final controller = ComponentControllerProxy();
try {
await _startErmine(controller.ctrl.request());
// sessionctl uses the basemgr debug service exposed on the /hub.
await controller.onDirectoryReady.first;
final ProcessResult result =
await Process.run('/bin/sessionctl', ['add_mod', _testAppUrl]);
print(result.stdout);
expect(result.exitCode, 0, reason: result.stderr);
await _expectTopColors(scenic, _expectedTopTwoColors);
} finally {
controller.ctrl.close();
}
});
}