blob: 4c22985a0a5c3a0a948d5c4831942cc95f0235cb [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 'package:collection/collection.dart' show SetEquality;
import 'package:test/test.dart';
import 'package:fidl/fidl.dart';
import 'package:fidl_fuchsia_modular/fidl_async.dart' as modular;
import 'package:fidl_fuchsia_modular_session/fidl_async.dart';
import 'package:fidl_fuchsia_modular_testing/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_modular_test/test.dart';
import 'package:fuchsia_scenic/views.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';
final _addModCommand = modular.AddMod(
modName: ['flutter_screencap_test_app.cmx'],
modNameTransitional: 'root',
intent: modular.Intent(action: 'action', handler: _testAppUrl),
surfaceRelation: modular.SurfaceRelation());
final _ermineConfig = BasemgrConfig(
sessionShellMap: [
SessionShellMapEntry(
name: 'Ermine',
config: SessionShellConfig(
appConfig: AppConfig(
url: 'fuchsia-pkg://fuchsia.com/ermine#meta/ermine.cmx',
),
),
),
],
useSessionShellForStoryShellFactory: true,
);
// 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};
ViewToken _createPresentationViewToken() {
final tokenPair = ViewTokenPair();
final presenter = PresenterProxy();
try {
StartupContext.fromStartupInfo().incoming.connectToService(presenter);
presenter.presentView(tokenPair.viewHolderToken, null);
return tokenPair.viewToken;
} 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 incoming = Incoming();
final launchInfo = LaunchInfo(
url: _testAppUrl, directoryRequest: incoming.request().passChannel());
final launcher = LauncherProxy();
context.incoming.connectToService(launcher);
await launcher.createComponent(launchInfo, controllerRequest);
launcher.ctrl.close();
final viewProvider = ViewProviderProxy();
try {
incoming.connectToService(viewProvider);
await viewProvider.createView(
_createPresentationViewToken().value, null, null);
} finally {
viewProvider.ctrl.close();
unawaited(incoming.close());
}
}
Future<void> _launchModUnderTest(TestHarnessProxy testHarness) async {
// get the puppetMaster service from the encapsulated test env
final puppetMaster = modular.PuppetMasterProxy();
await testHarness.connectToModularService(
ModularService.withPuppetMaster(puppetMaster.ctrl.request()));
// use puppetMaster to start a fake story an launch the mod under test
final storyPuppetMaster = modular.StoryPuppetMasterProxy();
await puppetMaster.controlStory(
'flutter_screencap_test', storyPuppetMaster.ctrl.request());
await storyPuppetMaster
.enqueue([modular.StoryCommand.withAddMod(_addModCommand)]);
await storyPuppetMaster.execute();
}
// 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 in test shells should have expected top two colors',
() async {
final testHarness = await launchTestHarness();
try {
await testHarness.run(TestHarnessSpec(
envServices:
EnvironmentServicesSpec(serviceDir: Channel.fromFile('/svc'))));
await _launchModUnderTest(testHarness);
await _expectTopColors(scenic, _expectedTopTwoColors);
} finally {
testHarness.ctrl.close();
}
});
test('flutter screencap as mod in Ermine should have expected top two colors',
() async {
final testHarness = await launchTestHarness();
try {
await testHarness.run(TestHarnessSpec(
basemgrConfig: _ermineConfig,
envServices:
EnvironmentServicesSpec(serviceDir: Channel.fromFile('/svc'))));
await _launchModUnderTest(testHarness);
await _expectTopColors(scenic, _expectedTopTwoColors);
} finally {
testHarness.ctrl.close();
}
});
}