From d51f08c136bdb0a1058ffeffc6d6d992b767070d Mon Sep 17 00:00:00 2001 From: Dillon Nys Date: Fri, 29 Sep 2023 12:14:59 -0700 Subject: [PATCH] test(core): Clean up and expand state machine tests --- .../lib/src/state_machine/state_machine.dart | 3 +- .../test/state_machine/my_state_machine.dart | 27 +- .../state_machine/state_machine_test.dart | 289 +++++++++++++----- 3 files changed, 246 insertions(+), 73 deletions(-) diff --git a/packages/amplify_core/lib/src/state_machine/state_machine.dart b/packages/amplify_core/lib/src/state_machine/state_machine.dart index 1c5d7e415f..c98002ea5b 100644 --- a/packages/amplify_core/lib/src/state_machine/state_machine.dart +++ b/packages/amplify_core/lib/src/state_machine/state_machine.dart @@ -104,7 +104,8 @@ abstract class StateMachineManager< await for (final completer in _eventController.stream) { try { await dispatch(completer.event, completer).completed; - } on Object { + } on Object catch (e) { + logger.verbose('Failed hard on event: ${completer.event}', e); continue; } } diff --git a/packages/amplify_core/test/state_machine/my_state_machine.dart b/packages/amplify_core/test/state_machine/my_state_machine.dart index 8bfc13ccf1..d213560fd0 100644 --- a/packages/amplify_core/test/state_machine/my_state_machine.dart +++ b/packages/amplify_core/test/state_machine/my_state_machine.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'package:amplify_core/amplify_core.dart'; +import 'package:test/test.dart'; final _builders = true; @override - bool get shouldEmit => false; + final bool shouldEmit; } final class MyEvent extends StateMachineEvent { @@ -38,6 +51,12 @@ final class MyEvent extends StateMachineEvent { @override MyPreconditionException? checkPrecondition(MyState currentState) { + if (type == MyType.failPrecondition) { + return const MyPreconditionException( + 'Failed precondition', + shouldEmit: true, + ); + } if (currentState.type == type) { return const MyPreconditionException('Cannot process event of same type'); } @@ -98,6 +117,8 @@ class MyStateMachine extends StateMachine(), manager); + expect(manager.get(), manager); + expect(manager.get(), manager); + + final stateMachine = manager.getOrCreate(MyStateMachine.type); + expect(stateMachine.get(), manager); + expect(stateMachine.get(), manager); + expect(stateMachine.get(), manager); }); test('checkPrecondition blocks abberant events', () async { final currentState = - stateMachine.getOrCreate(MyStateMachine.type).currentState; + manager.getOrCreate(MyStateMachine.type).currentState; expect(currentState.type, equals(MyType.initial)); - stateMachine.accept(const MyEvent(MyType.initial)).ignore(); expect( - stateMachine.stream, + manager.stream, neverEmits(anything), ); - - await stateMachine.close(); + await manager.accept(const MyEvent(MyType.initial)).completed; + await manager.close(); }); test('dispatches correctly', () { - stateMachine.accept(const MyEvent(MyType.doWork)).ignore(); + manager.accept(const MyEvent(MyType.doWork)).ignore(); expect( - stateMachine.stream, + manager.stream, emitsThrough(const MyState(MyType.success)), ); }); @@ -50,18 +61,21 @@ void main() { // isn't much use trying to pin down specifics here. if (zIsWeb) return isA(); return isA().having( - (chain) => chain.traces.map((trace) => trace.toString()), - 'traces', + (chain) => chain.traces + .expand((trace) => trace.frames) + .map((frame) => frame.toString()), + 'frames', containsAllInOrder([ contains('MyStateMachine.doWork'), contains('StateMachineManager.$method'), + contains('state_machine_test.dart'), ]), ); } test('emitted from stream', () { expect( - stateMachine.stream, + manager.stream, emitsThrough( isA().having( (s) => s.stackTrace, @@ -70,12 +84,12 @@ void main() { ), ), ); - stateMachine.accept(const MyEvent(MyType.tryWork)).ignore(); + manager.accept(const MyEvent(MyType.tryWork)).ignore(); }); test('accept', () async { final completion = - await stateMachine.accept(const MyEvent(MyType.tryWork)).completed; + await manager.accept(const MyEvent(MyType.tryWork)).completed; expect( completion, isA().having( @@ -88,7 +102,7 @@ void main() { test('acceptAndComplete', () async { try { - await stateMachine.acceptAndComplete(const MyEvent(MyType.tryWork)); + await manager.acceptAndComplete(const MyEvent(MyType.tryWork)); fail( 'acceptAndComplete should rethrow the exception from the ' 'state machine', @@ -99,9 +113,8 @@ void main() { }); test('dispatch', () async { - final completion = await stateMachine - .dispatch(const MyEvent(MyType.tryWork)) - .completed; + final completion = + await manager.dispatch(const MyEvent(MyType.tryWork)).completed; expect( completion, isA().having( @@ -114,7 +127,7 @@ void main() { test('dispatchAndComplete', () async { try { - await stateMachine.dispatchAndComplete(const MyEvent(MyType.tryWork)); + await manager.dispatchAndComplete(const MyEvent(MyType.tryWork)); fail( 'dispatchAndComplete should rethrow the exception from the ' 'state machine', @@ -126,107 +139,234 @@ void main() { }); group('cross-zone', () { - test('success', () { - final completer = stateMachine.accept(const MyEvent(MyType.doWork)); + test('success', () async { + final completer = manager.accept(const MyEvent(MyType.doWork)); + final succeeds = completion( + isA().having((s) => s.type, 'type', MyType.success), + ); + final expectations = >[]; runZoned(() { - expect( - completer.completed, + final accepted = expectLater( + completer.accepted, completes, reason: 'Should complete in forked zone', ); + final completed = expectLater( + completer.completed, + succeeds, + reason: 'Should complete in forked zone', + ); + expectations.addAll([accepted, completed]); }); runZonedGuarded( () { - expect( - completer.completed, + final accepted = expectLater( + completer.accepted, completes, reason: 'Should complete with different error zone', ); + final completed = expectLater( + completer.completed, + succeeds, + reason: 'Should complete with different error zone', + ); + expectations.addAll([accepted, completed]); }, - (e, st) {}, + (e, st) => fail(e.toString()), + ); + final accepted = expectLater( + completer.accepted, + completes, + reason: 'Should complete in root zone', ); - expect(completer.completed, completes); + final completed = expectLater( + completer.completed, + succeeds, + reason: 'Should complete in root zone', + ); + expectations.addAll([accepted, completed]); + await Future.wait(expectations); }); - test('errors', () { - final completer = stateMachine.accept(const MyEvent(MyType.failHard)); + test('errors', () async { + final completer = manager.accept(const MyEvent(MyType.failHard)); + final fails = throwsA(isA()); + final expectations = >[]; runZoned(() { - expect( + final accepted = expectLater( + completer.accepted, + completes, + reason: 'Should complete in forked zone', + ); + final completed = expectLater( completer.completed, - throwsA(isA()), + fails, reason: 'Should complete in forked zone', ); + expectations.addAll([accepted, completed]); }); runZonedGuarded( () { - expect( + final accepted = expectLater( + completer.accepted, + completes, + reason: 'Should complete with different error zone', + ); + final completed = expectLater( completer.completed, - throwsA(isA()), + fails, reason: 'Should complete with different error zone', ); + expectations.addAll([accepted, completed]); }, - (e, st) {}, + (e, st) => fail(e.toString()), + ); + final accepted = expectLater( + completer.accepted, + completes, + reason: 'Should complete in root zone', ); - expect(completer.completed, throwsA(isA())); + final completed = expectLater( + completer.completed, + fails, + reason: 'Should complete in root zone', + ); + expectations.addAll([accepted, completed]); + await Future.wait(expectations); }); }); - group('subscribeTo', () { - test('can listen to other machines', () { - stateMachine.accept(const MyEvent(MyType.delegateWork)).ignore(); + group('failures', () { + test('failed preconditions are recoverable', () async { expect( - stateMachine.stream, + manager.stream, emitsInOrder([ - const MyState(MyType.delegateWork), - const WorkerState(WorkType.doWork), - const WorkerState(WorkType.success), + const MyState(MyType.doWork), + const MyState(MyType.success), + const MyState(MyType.error), + const MyState(MyType.doWork), const MyState(MyType.success), ]), ); + await expectLater( + ( + manager.accept(const MyEvent(MyType.doWork)).completed, + manager.accept(const MyEvent(MyType.success)).completed, + manager.accept(const MyEvent(MyType.failPrecondition)).completed, + manager.accept(const MyEvent(MyType.doWork)).completed, + ).wait, + completes, + ); }); - test('can listen multiple times to other machines', () async { - stateMachine.accept(const MyEvent(MyType.delegateWork)).ignore(); - await expectLater( - stateMachine.stream, + test('hard failures are recoverable', () async { + expect( + manager.stream, emitsInOrder([ - const MyState(MyType.delegateWork), - const WorkerState(WorkType.doWork), - const WorkerState(WorkType.success), + const MyState(MyType.doWork), + const MyState(MyType.success), + const MyState(MyType.failHard), + emitsError(isA()), + const MyState(MyType.doWork), const MyState(MyType.success), ]), ); - - stateMachine.accept(const MyEvent(MyType.delegateWork)).ignore(); - await expectLater( - stateMachine.stream, + expect( + manager.getOrCreate(MyStateMachine.type).stream, emitsInOrder([ - const MyState(MyType.delegateWork), - const WorkerState(WorkType.doWork), - const WorkerState(WorkType.success), + const MyState(MyType.doWork), const MyState(MyType.success), + const MyState(MyType.failHard), + emitsError(isA()), + const MyState(MyType.doWork), + const MyState(MyType.success), + ]), + ); + await expectLater( + ( + manager.accept(const MyEvent(MyType.doWork)).completed, + manager.accept(const MyEvent(MyType.failHard)).completed, + manager.accept(const MyEvent(MyType.doWork)).completed, + ).wait, + throwsA( + isA>().having( + (e) { + final (v1, v2, v3) = e.values; + final (e1, e2, e3) = e.errors; + return [v1, v2, v3, e1, e2, e3]; + }, + 'result', + containsAllInOrder([ + isA(), + null, + isA(), + null, + isA() + .having((e) => e.error, 'error', isA()), + null, + ]), + ), + ), + ); + }); + }); + + group('subscribeTo', () { + test('can listen to other machines', () { + manager.accept(const MyEvent(MyType.delegateWork)).ignore(); + expect( + manager.stream, + emitsInOrder(const [ + MyState(MyType.delegateWork), + WorkerState(WorkType.doWork), + WorkerState(WorkType.success), + MyState(MyType.success), + ]), + ); + }); + + test('can listen multiple times to other machines', () async { + manager.accept(const MyEvent(MyType.delegateWork)).ignore(); + await expectLater( + manager.stream, + emitsInOrder(const [ + MyState(MyType.delegateWork), + WorkerState(WorkType.doWork), + WorkerState(WorkType.success), + MyState(MyType.success), + ]), + ); + + manager.accept(const MyEvent(MyType.delegateWork)).ignore(); + await expectLater( + manager.stream, + emitsInOrder(const [ + MyState(MyType.delegateWork), + WorkerState(WorkType.doWork), + WorkerState(WorkType.success), + MyState(MyType.success), ]), ); }); }); test('queues calls to accept appropriately', () async { - final tryWork1 = stateMachine.accept(const MyEvent(MyType.tryWork)); - final delegate = stateMachine.accept( + final tryWork1 = manager.accept(const MyEvent(MyType.tryWork)); + final delegate = manager.accept( const MyEvent(MyType.delegateWork), ); - final tryWork2 = stateMachine.accept(const MyEvent(MyType.tryWork)); + final tryWork2 = manager.accept(const MyEvent(MyType.tryWork)); await expectLater( - stateMachine.stream, - emitsInOrder([ - const MyState(MyType.tryWork), - const MyState(MyType.error), - const MyState(MyType.delegateWork), - const WorkerState(WorkType.doWork), - const WorkerState(WorkType.success), - const MyState(MyType.success), - const MyState(MyType.tryWork), - const MyState(MyType.error), + manager.stream, + emitsInOrder(const [ + MyState(MyType.tryWork), + MyState(MyType.error), + MyState(MyType.delegateWork), + WorkerState(WorkType.doWork), + WorkerState(WorkType.success), + MyState(MyType.success), + MyState(MyType.tryWork), + MyState(MyType.error), ]), ); expect(await tryWork1.completed, const MyState(MyType.error)); @@ -235,3 +375,14 @@ void main() { }); }); } + +typedef _Future3Value = ( + StateMachineState?, + StateMachineState?, + StateMachineState?, +); +typedef _Future3Error = ( + AsyncError?, + AsyncError?, + AsyncError?, +);