Add child_build_id property

This will be used when re-running tests to skip the sub-build and
go straight to testing.

See design doc:
https://docs.google.com/document/d/1Jv6rHFwvXdafzpQik-a9fiv3G6YGL9ata_FT8RezOXM/edit#heading=h.ypsbql4vhbwa

Bug: 39904
Change-Id: I86432ba5f9a405d92ee71cf8dd8c600b50213e8f
diff --git a/recipes/fuchsia/fuchsia.expected/successful_build_and_test_not_in_shards.json b/recipes/fuchsia/fuchsia.expected/child_build_provided__test_not_in_shards.json
similarity index 84%
rename from recipes/fuchsia/fuchsia.expected/successful_build_and_test_not_in_shards.json
rename to recipes/fuchsia/fuchsia.expected/child_build_provided__test_not_in_shards.json
index 14111b7..6b2c41b 100644
--- a/recipes/fuchsia/fuchsia.expected/successful_build_and_test_not_in_shards.json
+++ b/recipes/fuchsia/fuchsia.expected/child_build_provided__test_not_in_shards.json
@@ -146,6 +146,7 @@
     "cmd": [],
     "name": "build",
     "~followup_annotations": [
+      "@@@STEP_TEXT@Reusing child build instead of triggering@@@",
       "@@@STEP_LINK@builder-subbuild@https://ci.chromium.org/b/8945511751514863184@@@",
       "@@@SET_BUILD_PROPERTY@integration-revision-count@1@@@"
     ]
@@ -158,106 +159,10 @@
       "cr-buildbucket.appspot.com"
     ],
     "infra_step": true,
-    "name": "build.schedule",
-    "stdin": "{\"requests\": [{\"scheduleBuild\": {\"builder\": {\"bucket\": \"ci\", \"builder\": \"builder-subbuild\", \"project\": \"fuchsia\"}, \"experimental\": \"NO\", \"fields\": \"builder,createTime,createdBy,critical,endTime,id,input,number,output,startTime,status,updateTime\", \"gitilesCommit\": {\"host\": \"fuchsia.googlesource.com\", \"id\": \"2d72510e447ab60a9728aeea2362d8be2cbd7789\", \"project\": \"fuchsia\", \"ref\": \"refs/heads/master\"}, \"properties\": {\"$recipe_engine/source_manifest\": {\"debug_dir\": null}, \"gcs_bucket\": \"###fuchsia-build###\", \"manifest\": \"manifest/minimal\", \"packages\": [\"//bundles/buildbot:core\"], \"parent_id\": \"8945511751514863184\", \"project\": \"integration\", \"remote\": \"https://fuchsia.googlesource.com/integration\", \"spec_revision\": \"deadbeef\", \"target\": \"x64\"}, \"requestId\": \"8945511751514863184-00000000-0000-0000-0000-000000001337\", \"swarming\": {\"parentRunId\": \"fake-task-id\"}, \"tags\": [{\"key\": \"user_agent\", \"value\": \"recipe\"}]}}]}",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@",
-      "@@@STEP_LOG_LINE@json.output@{@@@",
-      "@@@STEP_LOG_LINE@json.output@  \"responses\": [@@@",
-      "@@@STEP_LOG_LINE@json.output@    {@@@",
-      "@@@STEP_LOG_LINE@json.output@      \"scheduleBuild\": {@@@",
-      "@@@STEP_LOG_LINE@json.output@        \"id\": \"8945511751514863184\"@@@",
-      "@@@STEP_LOG_LINE@json.output@      }@@@",
-      "@@@STEP_LOG_LINE@json.output@    }@@@",
-      "@@@STEP_LOG_LINE@json.output@  ]@@@",
-      "@@@STEP_LOG_LINE@json.output@}@@@",
-      "@@@STEP_LOG_END@json.output@@@",
-      "@@@STEP_LOG_LINE@request@{@@@",
-      "@@@STEP_LOG_LINE@request@  \"requests\": [@@@",
-      "@@@STEP_LOG_LINE@request@    {@@@",
-      "@@@STEP_LOG_LINE@request@      \"scheduleBuild\": {@@@",
-      "@@@STEP_LOG_LINE@request@        \"builder\": {@@@",
-      "@@@STEP_LOG_LINE@request@          \"bucket\": \"ci\", @@@",
-      "@@@STEP_LOG_LINE@request@          \"builder\": \"builder-subbuild\", @@@",
-      "@@@STEP_LOG_LINE@request@          \"project\": \"fuchsia\"@@@",
-      "@@@STEP_LOG_LINE@request@        }, @@@",
-      "@@@STEP_LOG_LINE@request@        \"experimental\": \"NO\", @@@",
-      "@@@STEP_LOG_LINE@request@        \"fields\": \"builder,createTime,createdBy,critical,endTime,id,input,number,output,startTime,status,updateTime\", @@@",
-      "@@@STEP_LOG_LINE@request@        \"gitilesCommit\": {@@@",
-      "@@@STEP_LOG_LINE@request@          \"host\": \"fuchsia.googlesource.com\", @@@",
-      "@@@STEP_LOG_LINE@request@          \"id\": \"2d72510e447ab60a9728aeea2362d8be2cbd7789\", @@@",
-      "@@@STEP_LOG_LINE@request@          \"project\": \"fuchsia\", @@@",
-      "@@@STEP_LOG_LINE@request@          \"ref\": \"refs/heads/master\"@@@",
-      "@@@STEP_LOG_LINE@request@        }, @@@",
-      "@@@STEP_LOG_LINE@request@        \"properties\": {@@@",
-      "@@@STEP_LOG_LINE@request@          \"$recipe_engine/source_manifest\": {@@@",
-      "@@@STEP_LOG_LINE@request@            \"debug_dir\": null@@@",
-      "@@@STEP_LOG_LINE@request@          }, @@@",
-      "@@@STEP_LOG_LINE@request@          \"gcs_bucket\": \"###fuchsia-build###\", @@@",
-      "@@@STEP_LOG_LINE@request@          \"manifest\": \"manifest/minimal\", @@@",
-      "@@@STEP_LOG_LINE@request@          \"packages\": [@@@",
-      "@@@STEP_LOG_LINE@request@            \"//bundles/buildbot:core\"@@@",
-      "@@@STEP_LOG_LINE@request@          ], @@@",
-      "@@@STEP_LOG_LINE@request@          \"parent_id\": \"8945511751514863184\", @@@",
-      "@@@STEP_LOG_LINE@request@          \"project\": \"integration\", @@@",
-      "@@@STEP_LOG_LINE@request@          \"remote\": \"https://fuchsia.googlesource.com/integration\", @@@",
-      "@@@STEP_LOG_LINE@request@          \"spec_revision\": \"deadbeef\", @@@",
-      "@@@STEP_LOG_LINE@request@          \"target\": \"x64\"@@@",
-      "@@@STEP_LOG_LINE@request@        }, @@@",
-      "@@@STEP_LOG_LINE@request@        \"requestId\": \"8945511751514863184-00000000-0000-0000-0000-000000001337\", @@@",
-      "@@@STEP_LOG_LINE@request@        \"swarming\": {@@@",
-      "@@@STEP_LOG_LINE@request@          \"parentRunId\": \"fake-task-id\"@@@",
-      "@@@STEP_LOG_LINE@request@        }, @@@",
-      "@@@STEP_LOG_LINE@request@        \"tags\": [@@@",
-      "@@@STEP_LOG_LINE@request@          {@@@",
-      "@@@STEP_LOG_LINE@request@            \"key\": \"user_agent\", @@@",
-      "@@@STEP_LOG_LINE@request@            \"value\": \"recipe\"@@@",
-      "@@@STEP_LOG_LINE@request@          }@@@",
-      "@@@STEP_LOG_LINE@request@        ]@@@",
-      "@@@STEP_LOG_LINE@request@      }@@@",
-      "@@@STEP_LOG_LINE@request@    }@@@",
-      "@@@STEP_LOG_LINE@request@  ]@@@",
-      "@@@STEP_LOG_LINE@request@}@@@",
-      "@@@STEP_LOG_END@request@@@",
-      "@@@STEP_LINK@8945511751514863184@https://cr-buildbucket.appspot.com/build/8945511751514863184@@@"
-    ]
-  },
-  {
-    "cmd": [],
-    "name": "build.collect",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "bb",
-      "collect",
-      "-host",
-      "cr-buildbucket.appspot.com",
-      "-interval",
-      "60s",
-      "8945511751514863184"
-    ],
-    "infra_step": true,
-    "name": "build.collect.wait",
-    "timeout": 86400,
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "bb",
-      "batch",
-      "-host",
-      "cr-buildbucket.appspot.com"
-    ],
-    "infra_step": true,
-    "name": "build.collect.get",
+    "name": "build.buildbucket.get",
     "stdin": "{\"requests\": [{\"getBuild\": {\"fields\": \"builder,createTime,createdBy,critical,endTime,id,input,number,output,startTime,status,updateTime\", \"id\": \"8945511751514863184\"}}]}",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_NEST_LEVEL@1@@@",
       "@@@STEP_LOG_LINE@json.output@{@@@",
       "@@@STEP_LOG_LINE@json.output@  \"responses\": [@@@",
       "@@@STEP_LOG_LINE@json.output@    {@@@",
diff --git a/recipes/fuchsia/fuchsia.py b/recipes/fuchsia/fuchsia.py
index 08877fe..6aa9fc5 100644
--- a/recipes/fuchsia/fuchsia.py
+++ b/recipes/fuchsia/fuchsia.py
@@ -62,6 +62,12 @@
 ]
 
 PROPERTIES = {
+    'child_build_id':
+        Property(
+            kind=long,
+            help=('The buildbucket ID of the child build. If set, '
+                  'will use this build instead of launching a new one.'),
+            default=None),
     'spec_remote':
         Property(
             kind=str,
@@ -70,7 +76,7 @@
 }
 
 
-def RunSteps(api, spec_remote):
+def RunSteps(api, child_build_id, spec_remote):
   # Resolve the build input to always contain a Gitiles commit.
   api.build_input_resolver.resolve(
       default_project_url='https://fuchsia.googlesource.com/fuchsia')
@@ -98,8 +104,8 @@
         'if not running tests, use the fuchsia/build recipe directly')
 
   with api.step.nest('build') as presentation:
-    child_build = run_build_steps(api, presentation, spec_revision,
-                                  orchestrator_id)
+    child_build = run_build_steps(api, presentation, child_build_id,
+                                  spec_revision, orchestrator_id)
     child_props = json_format.MessageToDict(child_build.output.properties)
     orchestration_inputs = collect_test_orchestration_inputs(api, child_props)
     # Copy to our own properties so the results uploader in google3 can find
@@ -115,42 +121,49 @@
   run_test_steps(api, orchestration_inputs, spec)
 
 
-def run_build_steps(api, presentation, spec_revision, orchestrator_id):
-  parent_properties = api.properties.thaw()
-  # These are reserved by kitchen and swarming. See
-  # https://chromium.googlesource.com/infra/infra/+/2c2389a00fcdb93d90a628f941814f2abd34428e/go/src/infra/tools/kitchen/cook.go#266
-  # and https://chromium.googlesource.com/infra/infra/+/7fcd559afa7a866a5ad039019e6ef6a91922e09c/appengine/cr-buildbucket/validation.py#36.
-  # We also should not override the 'recipe' of the child builder.
-  reject_keys = {
-      '$recipe_engine/path', '$recipe_engine/step', 'bot_id', 'path_config',
-      'buildbucket', '$recipe_engine/buildbucket', 'buildername', 'branch',
-      'repository', '$recipe_engine/runtime', 'recipe'
-  }
-  properties = {
-      key: val
-      for key, val in parent_properties.items()
-      if key and key not in reject_keys
-  }
-  properties.update({
-      'spec_revision': spec_revision,
-      'parent_id': orchestrator_id,
-  })
+def run_build_steps(api, presentation, child_build_id, spec_revision,
+                    orchestrator_id):
   builder_name = '{}-subbuild'.format(api.buildbucket.build.builder.builder)
-  # If this task was launched by led, we launch the child with led as well.
-  # This lets us ensure that the parent and child use the same version of
-  # the recipes code. That is a requirement for testing this recipe, as well as
-  # for avoiding the need to do soft transitions when updating the interface
-  # between the parent and child recipes.
-  if api.led.launched_by_led:
-    output_build, build_url = build_with_led(api, builder_name, properties,
-                                             presentation)
+  if child_build_id:
+    # Text is meant to avoid confusion.
+    presentation.step_text = 'Reusing child build instead of triggering'
+    output_build = api.buildbucket.get(child_build_id)
+    build_url = 'https://ci.chromium.org/b/%d' % child_build_id
   else:
-    output_build, build_url = build_with_buildbucket(api, builder_name,
-                                                     properties)
+    parent_properties = api.properties.thaw()
+    # These are reserved by kitchen and swarming. See
+    # https://chromium.googlesource.com/infra/infra/+/2c2389a00fcdb93d90a628f941814f2abd34428e/go/src/infra/tools/kitchen/cook.go#266
+    # and https://chromium.googlesource.com/infra/infra/+/7fcd559afa7a866a5ad039019e6ef6a91922e09c/appengine/cr-buildbucket/validation.py#36.
+    # We also should not override the 'recipe' of the child builder.
+    reject_keys = {
+        '$recipe_engine/path', '$recipe_engine/step', 'bot_id', 'path_config',
+        'buildbucket', '$recipe_engine/buildbucket', 'buildername', 'branch',
+        'repository', '$recipe_engine/runtime', 'recipe'
+    }
+    properties = {
+        key: val
+        for key, val in parent_properties.items()
+        if key and key not in reject_keys
+    }
+    properties.update({
+        'spec_revision': spec_revision,
+        'parent_id': orchestrator_id,
+    })
+    # If this task was launched by led, we launch the child with led as well.
+    # This lets us ensure that the parent and child use the same version of
+    # the recipes code. That is a requirement for testing this recipe, as well as
+    # for avoiding the need to do soft transitions when updating the interface
+    # between the parent and child recipes.
+    if api.led.launched_by_led:
+      output_build, build_url = build_with_led(api, builder_name, properties,
+                                               presentation)
+    else:
+      output_build, build_url = build_with_buildbucket(api, builder_name,
+                                                       properties)
   presentation.links[builder_name] = build_url
   if output_build.status == common_pb2.INFRA_FAILURE:
     raise api.step.InfraFailure('build raised infra failure')
-  if output_build.status != common_pb2.SUCCESS:
+  elif output_build.status != common_pb2.SUCCESS:
     raise api.step.StepFailure('build failed')
   return output_build
 
@@ -478,21 +491,20 @@
       ]) + spec_data(
           gcs_bucket='gcs-bucket', variants=('profile',)) + test_step_data()
 
+  child_build = ci_build_message(
+      api=api,
+      output_props={
+          'integration-revision-count': 1,
+          'test_orchestration_inputs_hash': 'abc',
+      },
+      status='SUCCESS',
+  )
   yield api.fuchsia.test(
-      'successful_build_and_test_not_in_shards',
+      'child_build_provided__test_not_in_shards',
       clear_default_steps=True,
       steps=[
-          child_build_steps(
-              api=api,
-              build=ci_build_message(
-                  api=api,
-                  output_props={
-                      'integration-revision-count': 1,
-                      'test_orchestration_inputs_hash': 'abc',
-                  },
-                  status='SUCCESS',
-              ),
-          ),
+          api.buildbucket.simulated_get(
+              child_build, step_name='build.buildbucket.get'),
           api.override_step_data(
               'run tests.attempt 0.collect',
               api.swarming.collect([
@@ -503,8 +515,9 @@
                   ),
               ])),
       ]) + spec_data(
-          gcs_bucket='gcs-bucket',
-          test_in_shards=False) + test_step_data(test_in_shards=False)
+          gcs_bucket='gcs-bucket', test_in_shards=False) + test_step_data(
+              test_in_shards=False) + api.properties(
+                  child_build_id=child_build.id)
 
   yield api.fuchsia.test(
       'build_only_failed',