Merge "Defer exceptions in calculating node_data()"
diff --git a/api/gabbits/stacks.yaml b/api/gabbits/stacks.yaml
index b37eabd..cb67e71 100644
--- a/api/gabbits/stacks.yaml
+++ b/api/gabbits/stacks.yaml
@@ -106,6 +106,8 @@
           type: OS::Heat::TestResource
           properties:
             value: {get_param: test_val}
+            action_wait_secs:
+              update: 1
       outputs:
         output_value:
           value: {get_attr: [test, output]}
@@ -138,6 +140,7 @@
     delay: 1.0
   response_json_paths:
     $.stack.stack_status: UPDATE_COMPLETE
+    $.stack.updated_time: /^(?!$HISTORY['poll for stack UPDATE_COMPLETE'].$RESPONSE['$.stack.updated_time'])/
 
 - name: list stack outputs
   GET: $LAST_URL/outputs
diff --git a/common/config.py b/common/config.py
index d7126c4..eddac01 100644
--- a/common/config.py
+++ b/common/config.py
@@ -140,6 +140,10 @@
     cfg.ListOpt('skip_test_stack_action_list',
                 help="List of stack actions in tests to skip "
                      "ex. ABANDON, ADOPT, SUSPEND, RESUME"),
+    cfg.BoolOpt('convergence_engine_enabled',
+                default=True,
+                help="Test features that are only present for stacks with "
+                     "convergence enabled."),
     cfg.IntOpt('volume_size',
                default=1,
                help='Default size in GB for volumes created by volumes tests'),
diff --git a/common/test.py b/common/test.py
index d43dece..a6aab7e 100644
--- a/common/test.py
+++ b/common/test.py
@@ -68,6 +68,17 @@
         return randbits
 
 
+def requires_convergence(test_method):
+    '''Decorator for convergence-only tests.
+
+    The decorated test will be skipped when convergence is disabled.
+    '''
+    convergence_enabled = config.CONF.heat_plugin.convergence_engine_enabled
+    skipper = testtools.skipUnless(convergence_enabled,
+                                   "Convergence-only tests are disabled")
+    return skipper(test_method)
+
+
 class HeatIntegrationTest(testscenarios.WithScenarios,
                           testtools.TestCase):
 
diff --git a/functional/test_simultaneous_update.py b/functional/test_simultaneous_update.py
new file mode 100644
index 0000000..0c562c0
--- /dev/null
+++ b/functional/test_simultaneous_update.py
@@ -0,0 +1,93 @@
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+
+import copy
+import time
+
+from heat_integrationtests.common import test
+from heat_integrationtests.functional import functional_base
+
+_test_template = {
+    'heat_template_version': 'pike',
+    'description': 'Test template to create two resources.',
+    'resources': {
+        'test1': {
+            'type': 'OS::Heat::TestResource',
+            'properties': {
+                'value': 'Test1',
+                'fail': False,
+                'update_replace': False,
+                'wait_secs': 0,
+            }
+        },
+        'test2': {
+            'type': 'OS::Heat::TestResource',
+            'properties': {
+                'value': 'Test1',
+                'fail': False,
+                'update_replace': False,
+                'wait_secs': 0,
+                'action_wait_secs': {
+                    'create': 30,
+                }
+            },
+            'depends_on': ['test1']
+        }
+    }
+}
+
+
+def get_templates(fail=False, delay_s=None):
+    before = copy.deepcopy(_test_template)
+
+    after = copy.deepcopy(before)
+    for r in after['resources'].values():
+        r['properties']['value'] = 'Test2'
+
+    before_props = before['resources']['test2']['properties']
+    before_props['fail'] = fail
+    if delay_s is not None:
+        before_props['action_wait_secs']['create'] = delay_s
+
+    return before, after
+
+
+class SimultaneousUpdateStackTest(functional_base.FunctionalTestsBase):
+
+    @test.requires_convergence
+    def test_retrigger_success(self):
+        before, after = get_templates()
+        stack_id = self.stack_create(template=before,
+                                     expected_status='CREATE_IN_PROGRESS')
+        time.sleep(10)
+
+        self.update_stack(stack_id, after)
+
+    @test.requires_convergence
+    def test_retrigger_failure(self):
+        before, after = get_templates(fail=True)
+        stack_id = self.stack_create(template=before,
+                                     expected_status='CREATE_IN_PROGRESS')
+        time.sleep(10)
+
+        self.update_stack(stack_id, after)
+
+    @test.requires_convergence
+    def test_retrigger_timeout(self):
+        before, after = get_templates(delay_s=70)
+        stack_id = self.stack_create(template=before,
+                                     expected_status='CREATE_IN_PROGRESS',
+                                     timeout=1)
+        time.sleep(50)
+
+        self.update_stack(stack_id, after)