Index: test/test-action.js |
=================================================================== |
new file mode 100644 |
--- /dev/null |
+++ b/test/test-action.js |
@@ -0,0 +1,763 @@ |
+ActionTest = AsyncTestCase( "ActionTest" ); |
+ |
+/** |
+ * Test that variables are defined |
+ */ |
+ActionTest.prototype.test__source_is_well_formed = function() |
+{ |
+ assertTrue( Action != null ); |
+ assertTrue( Action.dispatch != null ); |
+}; |
+ |
+//------------------------------------------------------- |
+// Utility |
+//------------------------------------------------------- |
+ |
+//------------------------------------------------------- |
+// Generic tests |
+//------------------------------------------------------- |
+/** |
+ * |
+ * @param {Action.Asynchronous_Action} action |
+ * @param {string} state_name |
+ */ |
+function verify_state( action, state_name ) |
+{ |
+ var expected = Action.State[ state_name ]; |
+ assertEquals( "action state is not '" + state_name + "'.", expected, action.state ); |
+} |
+ |
+/** |
+ * Check that an action executes its body and returns. Generic for simple, non-compound actions. |
+ * |
+ * @param {function} factory |
+ * A factory function that yields an action. |
+ * @param queue |
+ */ |
+function simple_try( factory, queue ) |
+{ |
+ /** |
+ * @type {Action.Asynchronous_Action} |
+ */ |
+ var d = null; |
+ var sequence = 0; |
+ |
+ function trial() |
+ { |
+ verify_state( d, "Running" ); |
+ sequence += 1; |
+ } |
+ |
+ queue.call( "Go phase.", function( callbacks ) |
+ { |
+ var monitored_trial = callbacks.add( trial ); |
+ d = factory( monitored_trial ); |
+ verify_state( d, "Ready" ); |
+ assertEquals( 0, sequence ); |
+ d.go(); |
+ verify_state( d, "Running" ); |
+ assertEquals( 0, sequence ); |
+ } ); |
+ |
+ queue.call( "End phase.", function() |
+ { |
+ verify_state( d, "Done" ); |
+ assertEquals( 1, sequence ); |
+ } ); |
+} |
+ |
+/** |
+ * Check that an action executes its body and the finisher function and returns. Generic for simple, non-compound |
+ * actions. Verifying execution is done with both direct and indirect means. |
+ * |
+ * @param {function} factory |
+ * A factory function that yields an action. |
+ * @param queue |
+ */ |
+function simple_finally( factory, queue ) |
+{ |
+ /** |
+ * @type {Action.Asynchronous_Action} |
+ */ |
+ var d; |
+ var sequence = 0; |
+ |
+ function trial() |
+ { |
+ verify_state( d, "Running" ); |
+ sequence += 1; |
+ } |
+ |
+ function catcher() |
+ { |
+ fail( "Action under test should not throw an exception nor call its catcher." ); |
+ } |
+ |
+ function finisher() |
+ { |
+ verify_state( d, "Done" ); |
+ sequence += 2; |
+ } |
+ |
+ queue.call( "Go phase.", function( callbacks ) |
+ { |
+ var monitored_trial = callbacks.add( trial ); |
+ var monitored_finisher = callbacks.add( finisher ); |
+ d = factory( monitored_trial, monitored_finisher, catcher ); |
+ verify_state( d, "Ready" ); |
+ assertEquals( 0, sequence ); |
+ d.go(); |
+ verify_state( d, "Running" ); |
+ assertEquals( 0, sequence ); |
+ } ); |
+ |
+ queue.call( "End phase.", function() |
+ { |
+ verify_state( d, "Done" ); |
+ assertEquals( 3, sequence ); |
+ } ); |
+} |
+ |
+/** |
+ * Run a simple trial, one that throws an exception, with both finisher and catcher. Indirectly verify that all |
+ * three run, but directly verify only the finally and catch. |
+ * |
+ * Generic for simple non-compound actions. |
+ * |
+ * @param {function} factory |
+ * A factory function that yields an action. |
+ * @param queue |
+ */ |
+function simple_catch( factory, queue ) |
+{ |
+ /** |
+ * @type {Action.Asynchronous_Action} |
+ */ |
+ var d; |
+ var sequence = 0; |
+ |
+ function trial() |
+ { |
+ verify_state( d, "Running" ); |
+ assertEquals( 0, sequence ); |
+ sequence += 1; |
+ throw new Error( "This error is part of the test." ); |
+ } |
+ |
+ function catcher() |
+ { |
+ verify_state( d, "Exception" ); |
+ assertEquals( 1, sequence ); |
+ sequence += 2; |
+ } |
+ |
+ function finisher() |
+ { |
+ verify_state( d, "Exception" ); |
+ assertEquals( 3, sequence ); |
+ sequence += 4; |
+ } |
+ |
+ queue.call( "Go phase.", function( callbacks ) |
+ { |
+ /* If we monitor the trial by adding to the callback list, it will report the exception as an error, which is not |
+ * what we want. We indirectly test that it runs by incrementing the sequence number. |
+ */ |
+ var monitored_catch = callbacks.add( catcher ); |
+ var monitored_finally = callbacks.add( finisher ); |
+ d = factory( trial, monitored_finally, monitored_catch ); |
+ verify_state( d, "Ready" ); |
+ assertEquals( 0, sequence ); |
+ d.go(); |
+ verify_state( d, "Running" ); |
+ assertEquals( 0, sequence ); |
+ } ); |
+ |
+ queue.call( "End phase.", function() |
+ { |
+ verify_state( d, "Exception" ); |
+ assertEquals( 7, sequence ); |
+ } ); |
+} |
+ |
+/** |
+ * Run a simple trial, one that throws an exception, with both finisher and catcher. Indirectly verify that all |
+ * three run, but directly verify only the finally and catch. |
+ * |
+ * Generic for simple non-compound actions. |
+ * |
+ * @param {function} factory |
+ * A factory function that yields an action. |
+ * @param queue |
+ */ |
+function simple_abort( factory, queue ) |
+{ |
+ /** |
+ * @type {Action.Asynchronous_Action} |
+ */ |
+ var d; |
+ var sequence = 0; |
+ |
+ function trial() |
+ { |
+ fail( "Executed trial when aborted." ); |
+ } |
+ |
+ function catcher() |
+ { |
+ verify_state( d, "Exception" ); |
+ assertEquals( 0, sequence ); |
+ sequence += 2; |
+ } |
+ |
+ function finisher() |
+ { |
+ verify_state( d, "Exception" ); |
+ assertEquals( 2, sequence ); |
+ sequence += 4; |
+ } |
+ |
+ queue.call( "Go phase.", function( callbacks ) |
+ { |
+ /* If we monitor the trial by adding to the callback list, it will report the exception as an error, which is not |
+ * what we want. We indirectly test that it runs by incrementing the sequence number. |
+ */ |
+ var monitored_catch = callbacks.add( catcher ); |
+ var monitored_finally = callbacks.add( finisher ); |
+ d = factory( trial, monitored_finally, monitored_catch ); |
+ verify_state( d, "Ready" ); |
+ assertEquals( 0, sequence ); |
+ d.go(); |
+ verify_state( d, "Running" ); |
+ assertEquals( 0, sequence ); |
+ d.abort(); |
+ verify_state( d, "Exception" ); |
+ assertEquals( 6, sequence ); |
+ } ); |
+} |
+ |
+function simple_value( factory, queue ) |
+{ |
+ /** |
+ * @type {Action.Asynchronous_Action} |
+ */ |
+ var d; |
+ |
+ function trial() |
+ { |
+ return [ 1, "two" ]; |
+ } |
+ |
+ function finisher( a, b ) |
+ { |
+ assertEquals( 1, a ); |
+ assertEquals( "two", b ); |
+ } |
+ |
+ queue.call( "Phase[1] Go.", function( callbacks ) |
+ { |
+ /* If we monitor the trial by adding to the callback list, it will report the exception as an error, which is not |
+ * what we want. We indirectly test that it runs by incrementing the sequence number. |
+ */ |
+ var monitored_finally = callbacks.add( finisher ); |
+ d = factory( trial, monitored_finally ); |
+ verify_state( d, "Ready" ); |
+ d.go(); |
+ verify_state( d, "Running" ); |
+ } ); |
+ |
+ queue.call( "Phase[2] Complete.", function() |
+ { |
+ verify_state( d, "Done" ); |
+ } ); |
+} |
+ |
+//------------------------------------------------------- |
+// Defer |
+//------------------------------------------------------- |
+/** |
+ * Factory for Defer objects |
+ * @param trial |
+ * @param finisher |
+ * @param catcher |
+ * @return {Action.Defer} |
+ */ |
+function defer_factory( trial, finisher, catcher ) |
+{ |
+ return new Action.Defer( trial, finisher, catcher ); |
+} |
+ |
+ActionTest.prototype.test_defer_try = function( queue ) |
+{ |
+ simple_try( defer_factory, queue ); |
+}; |
+ |
+ActionTest.prototype.test_defer_finally = function( queue ) |
+{ |
+ simple_finally( defer_factory, queue ); |
+}; |
+ |
+ActionTest.prototype.test_defer_catch = function( queue ) |
+{ |
+ simple_catch( defer_factory, queue ); |
+}; |
+ |
+ActionTest.prototype.test_defer_abort = function( queue ) |
+{ |
+ simple_abort( defer_factory, queue ); |
+}; |
+ |
+ActionTest.prototype.test_simple_value = function( queue ) |
+{ |
+ simple_value( defer_factory, queue ); |
+}; |
+ |
+//------------------------------------------------------- |
+// Delay |
+//------------------------------------------------------- |
+ |
+/** |
+ * Factory for Delay objects |
+ * @param delay |
+ * @param trial |
+ * @param finisher |
+ * @param catcher |
+ * @return {Action.Delay} |
+ */ |
+function delay_factory( delay, trial, finisher, catcher ) |
+{ |
+ return new Action.Delay( trial, delay, finisher, catcher ); |
+} |
+ |
+function simple_delay_factory( trial, finisher, catcher ) |
+{ |
+ return delay_factory( 2, trial, finisher, catcher ); |
+} |
+ |
+ActionTest.prototype.test_delay_tries = function( queue ) |
+{ |
+ simple_try( simple_delay_factory, queue ); |
+}; |
+ |
+ActionTest.prototype.test_delay_finally = function( queue ) |
+{ |
+ simple_finally( simple_delay_factory, queue ); |
+}; |
+ |
+ActionTest.prototype.test_delay_catch = function( queue ) |
+{ |
+ simple_catch( simple_delay_factory, queue ); |
+}; |
+ |
+//------------------------------------------------------- |
+// JM_Reporting implementation in Asynchronous_Action |
+//------------------------------------------------------- |
+var Asynchronous_Action__Test = AsyncTestCase( "Asynchronous_Action__Test" ); |
+ |
+function assert_reporting_is_empty( action ) |
+{ |
+ if ( "_end_watchers" in action ) |
+ { |
+ // If the array exists, everything in it must be null. |
+ var i; |
+ for ( i = 0 ; i < action._end_watchers.length ; ++i ) |
+ { |
+ if ( action._end_watchers[ i ] ) |
+ fail( "_end_watchers is still present and not empty." ); |
+ } |
+ } |
+ // If the _end_watchers array is absent, then the outbound reporting links are absent and the test passes. |
+} |
+ |
+ |
+/* |
+ * Test plan: Make a simple action and join to it twice. Ensure that all mutual references are null everything |
+ * completes. |
+ */ |
+Asynchronous_Action__Test.prototype.test_reporting__refencerences_are_absent_upon_completion = function( queue ) |
+{ |
+ var defer, join1, join2; |
+ |
+ function null_function() |
+ { |
+ } |
+ |
+ function reporting_has_watchers( action, n ) |
+ { |
+ assertTrue( "_end_watchers" in action ); |
+ assertEquals( n, action._end_watchers.length ); |
+ } |
+ |
+ function attentive_is_empty( action ) |
+ { |
+ assertNull( "joined_action should be null.", action.joined_action ); |
+ } |
+ |
+ queue.call( "Phase[1]=Go.", function( callbacks ) |
+ { |
+ // argument[2] is the number of times to expect this function |
+ var monitored_trial = callbacks.add( null_function, 1, 5000, "defer trial function" ); |
+ var monitored_finisher = callbacks.add( null_function, 2, 5000, "join finisher function" ); |
+ defer = new Action.Defer( monitored_trial ); |
+ reporting_has_watchers( defer, 0 ); |
+ join1 = new Action.Join( defer, monitored_finisher ); |
+ join2 = new Action.Join( defer, monitored_finisher ); |
+ reporting_has_watchers( defer, 0 ); |
+ join1.go(); |
+ reporting_has_watchers( defer, 1 ); |
+ join2.go(); |
+ reporting_has_watchers( defer, 2 ); |
+ defer.go(); |
+ verify_state( defer, "Running" ); |
+ verify_state( join1, "Running" ); |
+ } ); |
+ |
+ queue.call( "Phase[2]=Complete.", function() |
+ { |
+ verify_state( defer, "Done" ); |
+ verify_state( join1, "Done" ); |
+ verify_state( join2, "Done" ); |
+ assert_reporting_is_empty( defer ); |
+ assert_reporting_is_empty( join1 ); |
+ assert_reporting_is_empty( join2 ); |
+ attentive_is_empty( join1 ); |
+ attentive_is_empty( join2 ); |
+ } ); |
+}; |
+ |
+Asynchronous_Action__Test.prototype.test_reporting__refencerences_are_absent_after_cancel = function( queue ) |
+{ |
+ var defer, join; |
+ |
+ function null_function() |
+ { |
+ } |
+ |
+ function reporting_has_watchers( action, n ) |
+ { |
+ assertTrue( "_end_watchers" in action ); |
+ assertEquals( n, action._end_watchers.length ); |
+ } |
+ |
+ function attentive_is_empty( action ) |
+ { |
+ assertNull( "joined_action should be null.", action.joined_action ); |
+ } |
+ |
+ queue.call( "Phase[1]=Go.", function( callbacks ) |
+ { |
+ // argument[2] is the number of times to expect this function |
+ var monitored_finisher = callbacks.add( null_function, 1, 5000, "join finisher function" ); |
+ defer = new Action.Defer( null_function ); |
+ reporting_has_watchers( defer, 0 ); |
+ join = new Action.Join( defer, monitored_finisher ); |
+ reporting_has_watchers( defer, 0 ); |
+ join.go(); |
+ reporting_has_watchers( defer, 1 ); |
+ verify_state( defer, "Ready" ); |
+ verify_state( join, "Running" ); |
+ join.cancel(); |
+ verify_state( defer, "Ready" ); |
+ verify_state( join, "Done" ); |
+ assert_reporting_is_empty( defer ); |
+ assert_reporting_is_empty( join ); |
+ attentive_is_empty( join ); |
+ } ); |
+}; |
+ |
+//------------------------------------------------------- |
+// Join |
+//------------------------------------------------------- |
+ActionTest.prototype.test_join__throw_on_null_constructor_argument = function() |
+{ |
+ try |
+ { |
+ new Action.Join( null ); |
+ fail( "Join must throw an exception when passed a null constructor argument." ); |
+ } |
+ catch ( e ) |
+ { |
+ // Exception is what's supposed to happen. |
+ } |
+}; |
+ |
+function join_test( variation, factory, queue ) |
+/** |
+ * Combined variations on a number of simple join tests. There's an ordinary action and a join that it depends upon. |
+ * Both are invoked. The ordinary action executes first and then the join does. |
+ * |
+ * There are four operations {construct, go} x {ordinary, join} and some ordering dependencies. The construction of the |
+ * join must come after that of the ordinary action. Each invocation must come after construction. Given these |
+ * constraints, there are three possible orders. |
+ * |
+ * @param variation |
+ * @param factory |
+ * @param queue |
+ */ |
+{ |
+ var sequence = 0; |
+ var defer = null, join = null; |
+ |
+ function deferred_trial() |
+ { |
+ // The Defer instance should run first |
+ verify_state( defer, "Running" ); |
+ assertEquals( "deferred trial. sequence", 0, sequence ); |
+ sequence += 1; |
+ } |
+ |
+ function join_catcher() |
+ { |
+ fail( "Joined catcher should not be called." ); |
+ } |
+ |
+ function join_finisher( x ) |
+ { |
+ assertEquals( "joined finisher. number of arguments", 1, arguments.length ); |
+ assertEquals( "joined finisher. first argument", "ABC", x ); |
+ // The Join instance should run second. |
+ verify_state( defer, "Done" ); |
+ verify_state( join, "Done" ); |
+ assertEquals( "joined finisher. sequence", 1, sequence ); |
+ sequence += 2; |
+ } |
+ |
+ /* |
+ * The split finisher supports the case where we need to construct a join in one phase but launch it in another. |
+ * Because of a monitored function must complete in the same phase in which it set to be monitored, we need a static |
+ * function that we can use to initialize and a variable we can initialize in a later phase. |
+ */ |
+ var split_finisher_f; |
+ |
+ function split_finisher() |
+ { |
+ if ( !split_finisher_f ) |
+ throw new Error( "split_finisher_f is not initialized" ); |
+ split_finisher_f.apply( this, arguments ); |
+ } |
+ |
+ function monitored_join_finisher( callbacks ) |
+ { |
+ return callbacks.add( join_finisher, null, 2000, "join finisher" ); |
+ } |
+ |
+ function make_join( finisher ) |
+ { |
+ join = factory( defer, finisher, join_catcher ); |
+ verify_state( join, "Ready" ); |
+ } |
+ |
+ function join_go() |
+ { |
+ join.go( "ABC" ); |
+ } |
+ |
+ queue.call( "Phase[1]=Go.", function( callbacks ) |
+ { |
+ /* |
+ * Construction of the Defer instance always has to come first. |
+ */ |
+ defer = new Action.Defer( callbacks.add( deferred_trial, null, 2000, "defer_trial" ) ); |
+ verify_state( defer, "Ready" ); |
+ switch ( variation ) |
+ { |
+ case "existing ready": |
+ /* |
+ * Invoke the join before the defer has been invoked. This tests joining to a ready action. |
+ */ |
+ make_join( monitored_join_finisher( callbacks ) ); |
+ join_go(); |
+ defer.go(); |
+ verify_state( defer, "Running" ); |
+ verify_state( join, "Running" ); |
+ break; |
+ case "existing running": |
+ /* |
+ * Invoke the join after the defer has been invoked. This tests joining to a running action. |
+ */ |
+ make_join( monitored_join_finisher( callbacks ) ); |
+ defer.go(); |
+ join_go(); |
+ /* |
+ * The defer is running, but it hasn't completed yet, so the join hasn't completed yet. Contrast this with |
+ * the split version, where the defer action has already completed when we invoke the join. |
+ */ |
+ verify_state( defer, "Running" ); |
+ verify_state( join, "Running" ); |
+ break; |
+ case "existing running split": |
+ /* |
+ * Invoke the defer after the join is invoked, but invoke the join later. This test ensures that the join |
+ * does not complete prematurely. |
+ */ |
+ make_join( split_finisher ); |
+ defer.go(); |
+ verify_state( defer, "Running" ); |
+ verify_state( join, "Ready" ); |
+ break; |
+ case "new running": |
+ /* |
+ * Construct the join after defer has already been invoked. |
+ */ |
+ defer.go(); |
+ make_join( monitored_join_finisher( callbacks ) ); |
+ join_go(); |
+ verify_state( defer, "Running" ); |
+ verify_state( join, "Running" ); |
+ break; |
+ default: |
+ throw new Error( "unknown variation" ); |
+ } |
+ } ); |
+ |
+ queue.call( "Phase[2]=Intermediate.", function( callbacks ) |
+ { |
+ switch ( variation ) |
+ { |
+ case "existing running split": |
+ split_finisher_f = monitored_join_finisher( callbacks ); |
+ /* |
+ * The join should not yet have run at this point. |
+ */ |
+ verify_state( defer, "Done" ); |
+ verify_state( join, "Ready" ); |
+ /* |
+ * We invoke the join on a completed action. As a result, the join will complete immediately. |
+ */ |
+ join_go(); |
+ verify_state( defer, "Done" ); |
+ verify_state( join, "Done" ); |
+ break; |
+ default: |
+ /* |
+ * We're already verified the variation in the first phase. Some variations have no intermediate phase. |
+ */ |
+ break; |
+ } |
+ } ); |
+ |
+ queue.call( "Phase[3]=End.", function() |
+ { |
+ verify_state( defer, "Done" ); |
+ verify_state( join, "Done" ); |
+ assertEquals( 3, sequence ); |
+ } ); |
+} |
+ |
+//------------------------------------------------------- |
+// Join |
+//------------------------------------------------------- |
+/** |
+ * @param action |
+ * @param [finisher] |
+ * @param [catcher] |
+ * @return {Action.Join} |
+ */ |
+function join_factory( action, finisher, catcher ) |
+{ |
+ return new Action.Join( action, finisher, catcher ); |
+} |
+ |
+ActionTest__Join = AsyncTestCase( "Join" ); |
+ |
+ActionTest__Join.prototype.test_join__existing_join_to_new_defer_instance = function( queue ) |
+{ |
+ join_test( "existing ready", join_factory, queue ); |
+}; |
+ |
+ActionTest__Join.prototype.test_join__existing_join_to_running_defer_instance = function( queue ) |
+{ |
+ join_test( "existing running", join_factory, queue ); |
+}; |
+ |
+ActionTest__Join.prototype.test_join__existing_join_to_running_defer_instance__split = function( queue ) |
+{ |
+ join_test( "existing running split", join_factory, queue ); |
+}; |
+ |
+ActionTest__Join.prototype.test_join__new_join_to_running_defer_instance = function( queue ) |
+{ |
+ join_test( "new running", join_factory, queue ) |
+}; |
+ |
+//------------------------------------------------------- |
+// ActionTest__Join_Timeout |
+//------------------------------------------------------- |
+/** |
+ * Join_Timeout factory set at 15 seconds, which should be enough longer than callback limit, set to 2 seconds, to |
+ * avoid false negatives. |
+ * |
+ * @param action |
+ * @param [finisher] |
+ * @param [catcher] |
+ * @return {Action.Join_Timeout} |
+ */ |
+function join_timeout_factory( action, finisher, catcher ) |
+{ |
+ return new Action.Join_Timeout( action, 15000, finisher, catcher ); |
+} |
+ |
+ActionTest__Join_Timeout = AsyncTestCase( "Join_Timeout" ); |
+ |
+ActionTest__Join_Timeout.prototype.test_join_timeout__existing_join_to_new_defer_instance = function( queue ) |
+{ |
+ join_test( "existing ready", join_timeout_factory, queue ); |
+}; |
+ |
+ActionTest__Join_Timeout.prototype.test_join_timeout__existing_join_to_running_defer_instance = function( queue ) |
+{ |
+ join_test( "existing running", join_timeout_factory, queue ); |
+}; |
+ |
+ActionTest__Join_Timeout.prototype.test_join_timeout__existing_join_to_running_defer_instance__split = function( queue ) |
+{ |
+ join_test( "existing running split", join_timeout_factory, queue ); |
+}; |
+ |
+ActionTest__Join_Timeout.prototype.test_join_timeout__new_join_to_running_defer_instance = function( queue ) |
+{ |
+ join_test( "new running", join_timeout_factory, queue ) |
+}; |
+ |
+ActionTest__Join_Timeout.prototype.test_join_timeout__simple_timeout = function( queue ) |
+{ |
+ var sequence = 0; |
+ |
+ function defer_trial() |
+ { |
+ fail( "The trial on the defer object should not be called." ); |
+ } |
+ |
+ function join_catch() |
+ { |
+ assertEquals( 0, sequence ); |
+ sequence += 1; |
+ verify_state( defer, "Ready" ); |
+ verify_state( join, "Exception" ); |
+ assertTrue( "Join should be exceptional.", !join.completed_well ); |
+ } |
+ |
+ function join_finally() |
+ { |
+ } |
+ |
+ /* |
+ * Construct the defer object outside the test queue because it does not generate a callback in this case. |
+ */ |
+ var defer = new Action.Defer( defer_trial ); |
+ verify_state( defer, "Ready" ); |
+ var join; |
+ queue.call( "Phase[1]=Go.", function( callbacks ) |
+ { |
+ /* |
+ * Timeout is set to a very short time. |
+ */ |
+ var monitored_join_catch = callbacks.add( join_catch, null, 5000, "join catch" ); |
+ join = new Action.Join_Timeout( defer, 1, join_finally, monitored_join_catch ); |
+ join.go(); |
+ } ); |
+ /* |
+ * No need to launch the Defer action. If the timeout doesn't trigger the catcher, the test fails. |
+ */ |
+}; |
+ |