Add functional test coverage for PATCH updates

Adds coverage for patch updates which didn't land previously with the
code.

Change-Id: I66b721d1af787e9d64b8b942818a6c8b4412fa7b
Related-Bug: #1224828
diff --git a/common/test.py b/common/test.py
index 46911f0..21c5ba1 100644
--- a/common/test.py
+++ b/common/test.py
@@ -329,10 +329,11 @@
             stack_identifier, 'DELETE_COMPLETE',
             success_on_not_found=True)
 
-    def update_stack(self, stack_identifier, template, environment=None,
+    def update_stack(self, stack_identifier, template=None, environment=None,
                      files=None, parameters=None, tags=None,
                      expected_status='UPDATE_COMPLETE',
-                     disable_rollback=True):
+                     disable_rollback=True,
+                     existing=False):
         env = environment or {}
         env_files = files or {}
         parameters = parameters or {}
@@ -354,7 +355,8 @@
                     disable_rollback=disable_rollback,
                     parameters=parameters,
                     environment=env,
-                    tags=tags
+                    tags=tags,
+                    existing=existing
                 )
             except heat_exceptions.HTTPConflict as ex:
                 # FIXME(sirushtim): Wait a little for the stack lock to be
diff --git a/functional/test_create_update.py b/functional/test_create_update.py
index 1b273dd..4597d4c 100644
--- a/functional/test_create_update.py
+++ b/functional/test_create_update.py
@@ -102,11 +102,15 @@
 
     provider_group_template = '''
 heat_template_version: 2013-05-23
+parameters:
+  count:
+    type: number
+    default: 2
 resources:
   test_group:
     type: OS::Heat::ResourceGroup
     properties:
-      count: 2
+      count: {get_param: count}
       resource_def:
         type: My::TestResource
 '''
@@ -134,6 +138,20 @@
       user_data: {get_param: user_data}
 '''
 
+    fail_param_template = '''
+heat_template_version: 2014-10-16
+parameters:
+  do_fail:
+    type: boolean
+    default: False
+resources:
+  aresource:
+    type: OS::Heat::TestResource
+    properties:
+      value: Test
+      fail: {get_param: do_fail}
+'''
+
     def setUp(self):
         super(UpdateStackTest, self).setUp()
 
@@ -317,7 +335,7 @@
         '''Test two-level nested update.'''
         # Create a ResourceGroup (which creates a nested stack),
         # containing provider resources (which create a nested
-        # stack), thus excercising an update which traverses
+        # stack), thus exercising an update which traverses
         # two levels of nesting.
         template = _change_rsrc_properties(
             test_template_one_resource, ['test1'],
@@ -414,3 +432,69 @@
             stack_identifier,
             template=self.update_userdata_template,
             parameters=parms_updated)
+
+    def test_stack_update_provider_group_patch(self):
+        '''Test two-level nested update with PATCH'''
+        template = _change_rsrc_properties(
+            test_template_one_resource, ['test1'],
+            {'value': 'test_provider_group_template'})
+        files = {'provider.template': json.dumps(template)}
+        env = {'resource_registry':
+               {'My::TestResource': 'provider.template'}}
+
+        stack_identifier = self.stack_create(
+            template=self.provider_group_template,
+            files=files,
+            environment=env
+        )
+
+        initial_resources = {'test_group': 'OS::Heat::ResourceGroup'}
+        self.assertEqual(initial_resources,
+                         self.list_resources(stack_identifier))
+
+        # Prove the resource is backed by a nested stack, save the ID
+        nested_identifier = self.assert_resource_is_a_stack(stack_identifier,
+                                                            'test_group')
+
+        # Then check the expected resources are in the nested stack
+        nested_resources = {'0': 'My::TestResource',
+                            '1': 'My::TestResource'}
+        self.assertEqual(nested_resources,
+                         self.list_resources(nested_identifier))
+
+        # increase the count, pass only the paramter, no env or template
+        params = {'count': 3}
+        self.update_stack(stack_identifier, parameters=params, existing=True)
+
+        # Parent resources should be unchanged and the nested stack
+        # should have been updated in-place without replacement
+        self.assertEqual(initial_resources,
+                         self.list_resources(stack_identifier))
+
+        # Resource group stack should also be unchanged (but updated)
+        nested_stack = self.client.stacks.get(nested_identifier)
+        self.assertEqual('UPDATE_COMPLETE', nested_stack.stack_status)
+        # Add a resource, as we should have added one
+        nested_resources['2'] = 'My::TestResource'
+        self.assertEqual(nested_resources,
+                         self.list_resources(nested_identifier))
+
+    def test_stack_update_from_failed_patch(self):
+        '''Test PATCH update from a failed state.'''
+
+        # Start with empty template
+        stack_identifier = self.stack_create(
+            template='heat_template_version: 2014-10-16')
+
+        # Update with a good template, but bad parameter
+        self.update_stack(stack_identifier,
+                          template=self.fail_param_template,
+                          parameters={'do_fail': True},
+                          expected_status='UPDATE_FAILED')
+
+        # PATCH update, only providing the parameter
+        self.update_stack(stack_identifier,
+                          parameters={'do_fail': False},
+                          existing=True)
+        self.assertEqual({u'aresource': u'OS::Heat::TestResource'},
+                         self.list_resources(stack_identifier))