blob: ff625180c1d8de186d6cc0ed16f423b8bbae75a9 [file] [log] [blame]
// Copyright 2018 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:math' show Random;
import 'package:test/test.dart';
import 'checker.dart';
import 'computational_graph.dart';
import 'evaluation_order.dart';
import 'fleet_state.dart';
import 'node.dart';
import 'single_order_test_failure.dart';
// Sledge is supposed to work with multiple connections, and order of operations
// and synchronization is unpredictable. It’s hard to cover all cases by tests
// manually.
//
// This framework should allow to write a generic tests. So that developer may
// write one test description and framework would evaluate different scenarios
// of execution.
//
// Framework should allow to write tests on different layers of Sledge:
// - layer of data types
// - layer of documents
// - layer of Sledge
//
// To run different scenarios, we build a computational DAG (directed acyclic
// graph). And each topological sort of that graph is a correct evaluation order.
//
// All operations that are related to a single node are ordered. Relation
// between operations on different nodes based on synchronization operations.
//
typedef CheckerGenerator<T> = Checker<T> Function();
/// Fleet of instances.
class Fleet<T extends dynamic> {
int _fleetSize;
List<Node> _lastModifications;
final Node _initialModification = new Node('init');
final ComputationalGraph graph = new ComputationalGraph();
final T Function(int) _instanceGenerator;
final List<CheckerGenerator<T>> _checkerGenerators = <CheckerGenerator<T>>[];
double _expectedSyncsPerAction = 0.0;
final Random random = new Random(1);
Fleet(this._fleetSize, this._instanceGenerator) {
_lastModifications =
new List<Node>.filled(_fleetSize, _initialModification);
graph.addNode(_initialModification);
}
void _addNode(Node node, int id) {
graph
..addNode(node)
..addRelation(_lastModifications[id], node);
_lastModifications[id] = node;
}
// Perform synchronization of [group] of instances, specified by ids.
// It builds a chain of pairwise synchronization:
// (1, 2) (2, 3) ... (k-1, k) (k, k-2) (k-2, k-3) ... (3, 2) (2, 1)
// And adds them into the computational graph. Each two consecutive
// synchronizations share a node. So their order is fixed in the graph.
// (k, k-2) (k-2, k-3) ... (3, 2) (2, 1) are necessary to back propagate the
// changes made in `k`.
void synchronize(List<int> group) {
if (group.length <= 1) {
return;
}
final list = <int>[]..addAll(group)..addAll(group.reversed.skip(2));
for (int i = 0; i < list.length - 1; i++) {
Node node = new SynchronizationNode(
'${list[i]}_${list[i + 1]}-n${graph.nodes.length}',
list[i],
list[i + 1]);
_addNode(node, list[i]);
_addNode(node, list[i + 1]);
}
}
void runInTransaction(int id, Future Function(T) modification) {
final node =
new ModificationNode<T>('$id-n${graph.nodes.length}', id, modification);
_addNode(node, id);
}
/// Adds checker that would be called after execution of each node, including
/// randomly generated synchronization nodes.
void addChecker(CheckerGenerator<T> checkerGenerator) {
_checkerGenerators.add(checkerGenerator);
}
/// Sets the expected amount of random synchronizations after execution of
/// each node, excluding randomly generated synchronization nodes, to
/// [expectedSyncsPerAction].
void setRandomSynchronizationsRate(double expectedSyncsPerAction) {
if (expectedSyncsPerAction < 0) {
throw new ArgumentError(
'Number of synchronizations must be greater or equal to 0. Got $expectedSyncsPerAction.');
}
_expectedSyncsPerAction = expectedSyncsPerAction;
}
/// Executes the operations in all nodes in a given [order]. If [order] is not
/// specified, execute all nodes in some fixed order, that would be same over
/// all calls.
Future testSingleOrder(
{EvaluationOrder order, bool enableRandomSyncronization = true}) async {
order ??= graph.orders.first;
final fleetState = new FleetState<T>(_fleetSize, _instanceGenerator);
for (final newChecker in _checkerGenerators) {
fleetState.addChecker(newChecker());
}
// [completedOrder] contains all nodes from [order] in the same order, and
// additional randomly generated synchronization nodes.
EvaluationOrder completedOrder = order;
if (enableRandomSyncronization &&
_fleetSize > 1 &&
_expectedSyncsPerAction > 0) {
completedOrder = new EvaluationOrder([]);
for (final node in order.nodes) {
completedOrder.nodes.add(node);
// Geometric distribution (the probability distribution of the number Y
// of failures before the first success) is used to generate a number of
// synchronization nodes:
// E(Y) = (1 - p) / p
// So for fixed E(Y):
// p = 1 / (1 + E(Y))
while (random.nextDouble() >= 1.0 / (1.0 + _expectedSyncsPerAction)) {
// To generate equiprobably a pair of different ids from
// range [0, _fleetSize):
// - generate equiprobably id1 from [0, _fleetSize)
// - generate equiprobably id2 from [0, _fleetSize)\{id1}
int instanceId1 = random.nextInt(_fleetSize);
int instanceId2 = random.nextInt(_fleetSize - 1);
if (instanceId2 >= instanceId1) {
instanceId2++;
}
completedOrder.nodes.add(new SynchronizationNode.generated(
'${completedOrder.nodes.length}', instanceId1, instanceId2));
}
}
}
for (int i = 0; i < completedOrder.nodes.length; i++) {
try {
await fleetState.applyNode(completedOrder.nodes[i], i);
} on TestFailure catch (failure) {
// ignore: only_throw_errors
throw new SingleOrderTestFailure(
failure, completedOrder, completedOrder.nodes[i]);
}
}
}
/// Executes the operations in all nodes in an order specified by [nodeIds].
/// If [allowPartial] is false, checks that all [graph] nodes are present in
/// [nodeIds].
/// If [allowGenerated] is false, throws an Error when [nodeIds] contains id
/// of randomly generated synchronization node.
///
/// It can be used to reproduce previous execution order with an information
/// from TestFailure message.
Future testFixedOrder(Iterable<String> nodeIds,
{bool allowPartial = false, bool allowGenerated = true}) async {
await testSingleOrder(
order: new EvaluationOrder.fromIds(nodeIds, graph.nodes,
allowPartial: allowPartial, allowGenerated: allowGenerated),
enableRandomSyncronization: false);
}
/// Executes the operations in all nodes in [count] random orders.
///
/// On each step a random node is chosen to be executed equiprobably from
/// nodes available to execution. Note that this algorithm does not provide
/// equiprobable distribution over all correct execution orders.
Future testRandomOrders(int count) async {
for (int i = 0; i < count; i++) {
await testSingleOrder(order: graph.getRandomOrder());
}
}
Future testAllOrders() async {
for (final order in graph.orders) {
await testSingleOrder(order: order);
}
}
}