| // 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); |
| } |
| } |
| } |