[dart] Start additional bisection build for different failure reason

Change-Id: Id042fcafe0222a43caa7195b425b76d8e421711f
Reviewed-on: https://dart-review.googlesource.com/c/recipes/+/158320
Commit-Queue: Karl Klose <karlklose@google.com>
Reviewed-by: Alexander Thomas <athom@google.com>
diff --git a/README.recipes.md b/README.recipes.md
index 1a000bd..6fe7d6e 100644
--- a/README.recipes.md
+++ b/README.recipes.md
@@ -35,7 +35,9 @@
 
 &mdash; **def [is\_bisecting](/recipe_modules/bisect_build/api.py#20)(self):**
 
-&mdash; **def [schedule](/recipe_modules/bisect_build/api.py#23)(self, repo_url, reason, is_experimental=False):**
+&emsp; **@property**<br>&mdash; **def [is\_enabled](/recipe_modules/bisect_build/api.py#23)(self):**
+
+&mdash; **def [schedule](/recipe_modules/bisect_build/api.py#27)(self, repo_url, reason, is_experimental=False):**
 ### *recipe_modules* / [dart](/recipe_modules/dart)
 
 [DEPS](/recipe_modules/dart/__init__.py#5): [build/goma][build/recipe_modules/goma], [bisect\_build](#recipe_modules-bisect_build), [depot\_tools/bot\_update][depot_tools/recipe_modules/bot_update], [depot\_tools/depot\_tools][depot_tools/recipe_modules/depot_tools], [depot\_tools/gclient][depot_tools/recipe_modules/gclient], [depot\_tools/git][depot_tools/recipe_modules/git], [depot\_tools/gsutil][depot_tools/recipe_modules/gsutil], [depot\_tools/tryserver][depot_tools/recipe_modules/tryserver], [recipe\_engine/buildbucket][recipe_engine/recipe_modules/buildbucket], [recipe\_engine/cipd][recipe_engine/recipe_modules/cipd], [recipe\_engine/context][recipe_engine/recipe_modules/context], [recipe\_engine/file][recipe_engine/recipe_modules/file], [recipe\_engine/isolated][recipe_engine/recipe_modules/isolated], [recipe\_engine/json][recipe_engine/recipe_modules/json], [recipe\_engine/path][recipe_engine/recipe_modules/path], [recipe\_engine/platform][recipe_engine/recipe_modules/platform], [recipe\_engine/properties][recipe_engine/recipe_modules/properties], [recipe\_engine/python][recipe_engine/recipe_modules/python], [recipe\_engine/raw\_io][recipe_engine/recipe_modules/raw_io], [recipe\_engine/runtime][recipe_engine/recipe_modules/runtime], [recipe\_engine/service\_account][recipe_engine/recipe_modules/service_account], [recipe\_engine/step][recipe_engine/recipe_modules/step], [recipe\_engine/swarming][recipe_engine/recipe_modules/swarming]
@@ -127,41 +129,39 @@
 &mdash; **def [RunSteps](/recipes/dart/external.py#13)(api):**
 ### *recipes* / [dart/flutter\_engine](/recipes/dart/flutter_engine.py)
 
-[DEPS](/recipes/dart/flutter_engine.py#11): [build/goma][build/recipe_modules/goma], [bisect\_build](#recipe_modules-bisect_build), [dart](#recipe_modules-dart), [depot\_tools/bot\_update][depot_tools/recipe_modules/bot_update], [depot\_tools/depot\_tools][depot_tools/recipe_modules/depot_tools], [depot\_tools/gclient][depot_tools/recipe_modules/gclient], [depot\_tools/gitiles][depot_tools/recipe_modules/gitiles], [recipe\_engine/buildbucket][recipe_engine/recipe_modules/buildbucket], [recipe\_engine/context][recipe_engine/recipe_modules/context], [recipe\_engine/file][recipe_engine/recipe_modules/file], [recipe\_engine/json][recipe_engine/recipe_modules/json], [recipe\_engine/path][recipe_engine/recipe_modules/path], [recipe\_engine/platform][recipe_engine/recipe_modules/platform], [recipe\_engine/properties][recipe_engine/recipe_modules/properties], [recipe\_engine/python][recipe_engine/recipe_modules/python], [recipe\_engine/runtime][recipe_engine/recipe_modules/runtime], [recipe\_engine/step][recipe_engine/recipe_modules/step]
+[DEPS](/recipes/dart/flutter_engine.py#17): [build/goma][build/recipe_modules/goma], [bisect\_build](#recipe_modules-bisect_build), [dart](#recipe_modules-dart), [depot\_tools/bot\_update][depot_tools/recipe_modules/bot_update], [depot\_tools/depot\_tools][depot_tools/recipe_modules/depot_tools], [depot\_tools/gclient][depot_tools/recipe_modules/gclient], [depot\_tools/gitiles][depot_tools/recipe_modules/gitiles], [recipe\_engine/buildbucket][recipe_engine/recipe_modules/buildbucket], [recipe\_engine/context][recipe_engine/recipe_modules/context], [recipe\_engine/file][recipe_engine/recipe_modules/file], [recipe\_engine/json][recipe_engine/recipe_modules/json], [recipe\_engine/path][recipe_engine/recipe_modules/path], [recipe\_engine/platform][recipe_engine/recipe_modules/platform], [recipe\_engine/properties][recipe_engine/recipe_modules/properties], [recipe\_engine/python][recipe_engine/recipe_modules/python], [recipe\_engine/runtime][recipe_engine/recipe_modules/runtime], [recipe\_engine/step][recipe_engine/recipe_modules/step]
 
-&mdash; **def [AnalyzeDartUI](/recipes/dart/flutter_engine.py#73)(api, checkout_dir):**
+&mdash; **def [AnalyzeDartUI](/recipes/dart/flutter_engine.py#75)(api, checkout_dir):**
 
-&mdash; **def [Build](/recipes/dart/flutter_engine.py#56)(api, checkout_dir, config, \*targets):**
+&mdash; **def [Build](/recipes/dart/flutter_engine.py#58)(api, checkout_dir, config, \*targets):**
 
-&mdash; **def [BuildAndTest](/recipes/dart/flutter_engine.py#356)(api, start_dir, checkout_dir, flutter_rev):**
+&mdash; **def [BuildAndTest](/recipes/dart/flutter_engine.py#358)(api, start_dir, checkout_dir, flutter_rev):**
 
-&mdash; **def [BuildLinux](/recipes/dart/flutter_engine.py#106)(api, checkout_dir):**
+&mdash; **def [BuildLinux](/recipes/dart/flutter_engine.py#108)(api, checkout_dir):**
 
-&mdash; **def [BuildLinuxAndroidArm](/recipes/dart/flutter_engine.py#90)(api, checkout_dir):**
+&mdash; **def [BuildLinuxAndroidArm](/recipes/dart/flutter_engine.py#92)(api, checkout_dir):**
 
-&mdash; **def [BuildLinuxAndroidx86](/recipes/dart/flutter_engine.py#83)(api, checkout_dir):**
+&mdash; **def [BuildLinuxAndroidx86](/recipes/dart/flutter_engine.py#85)(api, checkout_dir):**
 
-&mdash; **def [CopyArtifacts](/recipes/dart/flutter_engine.py#182)(api, engine_src, cached_dest, file_paths):**
+&mdash; **def [CopyArtifacts](/recipes/dart/flutter_engine.py#184)(api, engine_src, cached_dest, file_paths):**
 
-&mdash; **def [GetCheckout](/recipes/dart/flutter_engine.py#133)(api):**
+&mdash; **def [GetCheckout](/recipes/dart/flutter_engine.py#135)(api):**
 
-&mdash; **def [KillTasks](/recipes/dart/flutter_engine.py#48)(api, checkout_dir, ok_ret='any'):**
+&mdash; **def [KillTasks](/recipes/dart/flutter_engine.py#50)(api, checkout_dir, ok_ret='any'):**
 
 Kills leftover tasks from previous runs or steps.
 
-&mdash; **def [RunGN](/recipes/dart/flutter_engine.py#65)(api, checkout_dir, \*args):**
+&mdash; **def [RunGN](/recipes/dart/flutter_engine.py#67)(api, checkout_dir, \*args):**
 
-&mdash; **def [RunSteps](/recipes/dart/flutter_engine.py#326)(api):**
+&mdash; **def [RunSteps](/recipes/dart/flutter_engine.py#328)(api):**
 
-&mdash; **def [TestEngine](/recipes/dart/flutter_engine.py#78)(api, checkout_dir):**
+&mdash; **def [TestEngine](/recipes/dart/flutter_engine.py#80)(api, checkout_dir):**
 
-&mdash; **def [TestFlutter](/recipes/dart/flutter_engine.py#270)(api, start_dir, just_built_dart_sdk):**
+&mdash; **def [TestFlutter](/recipes/dart/flutter_engine.py#272)(api, start_dir, just_built_dart_sdk):**
 
-&mdash; **def [TestObservatory](/recipes/dart/flutter_engine.py#122)(api, checkout_dir):**
+&mdash; **def [TestObservatory](/recipes/dart/flutter_engine.py#124)(api, checkout_dir):**
 
-&mdash; **def [UpdateCachedEngineArtifacts](/recipes/dart/flutter_engine.py#195)(api, flutter, engine_src):**
-
-&mdash; **def [bisection\_is\_enabled](/recipes/dart/flutter_engine.py#44)(api):**
+&mdash; **def [UpdateCachedEngineArtifacts](/recipes/dart/flutter_engine.py#197)(api, flutter, engine_src):**
 ### *recipes* / [dart/forward\_branch](/recipes/dart/forward_branch.py)
 
 [DEPS](/recipes/dart/forward_branch.py#8): [dart](#recipe_modules-dart), [depot\_tools/git][depot_tools/recipe_modules/git], [recipe\_engine/buildbucket][recipe_engine/recipe_modules/buildbucket], [recipe\_engine/path][recipe_engine/recipe_modules/path], [recipe\_engine/properties][recipe_engine/recipe_modules/properties], [recipe\_engine/raw\_io][recipe_engine/recipe_modules/raw_io], [recipe\_engine/step][recipe_engine/recipe_modules/step]
@@ -179,13 +179,9 @@
 &mdash; **def [RunSteps](/recipes/dart/linearize.py#24)(api):**
 ### *recipes* / [dart/neo](/recipes/dart/neo.py)
 
-[DEPS](/recipes/dart/neo.py#16): [bisect\_build](#recipe_modules-bisect_build), [dart](#recipe_modules-dart), [depot\_tools/gitiles][depot_tools/recipe_modules/gitiles], [depot\_tools/osx\_sdk][depot_tools/recipe_modules/osx_sdk], [recipe\_engine/buildbucket][recipe_engine/recipe_modules/buildbucket], [recipe\_engine/context][recipe_engine/recipe_modules/context], [recipe\_engine/path][recipe_engine/recipe_modules/path], [recipe\_engine/properties][recipe_engine/recipe_modules/properties], [recipe\_engine/raw\_io][recipe_engine/recipe_modules/raw_io], [recipe\_engine/step][recipe_engine/recipe_modules/step]
+[DEPS](/recipes/dart/neo.py#23): [bisect\_build](#recipe_modules-bisect_build), [dart](#recipe_modules-dart), [depot\_tools/gitiles][depot_tools/recipe_modules/gitiles], [depot\_tools/osx\_sdk][depot_tools/recipe_modules/osx_sdk], [recipe\_engine/buildbucket][recipe_engine/recipe_modules/buildbucket], [recipe\_engine/context][recipe_engine/recipe_modules/context], [recipe\_engine/path][recipe_engine/recipe_modules/path], [recipe\_engine/properties][recipe_engine/recipe_modules/properties], [recipe\_engine/raw\_io][recipe_engine/recipe_modules/raw_io], [recipe\_engine/step][recipe_engine/recipe_modules/step]
 
-&mdash; **def [RunSteps](/recipes/dart/neo.py#84)(api, properties):**
-
-&mdash; **def [bisection\_is\_enabled](/recipes/dart/neo.py#64)(api):**
-
-&mdash; **def [schedule\_bisection](/recipes/dart/neo.py#68)(api, failure=None):**
+&mdash; **def [RunSteps](/recipes/dart/neo.py#94)(api, properties):**
 ### *recipes* / [dart/package\_co19](/recipes/dart/package_co19.py)
 
 [DEPS](/recipes/dart/package_co19.py#8): [depot\_tools/git][depot_tools/recipe_modules/git], [recipe\_engine/buildbucket][recipe_engine/recipe_modules/buildbucket], [recipe\_engine/cipd][recipe_engine/recipe_modules/cipd], [recipe\_engine/path][recipe_engine/recipe_modules/path], [recipe\_engine/properties][recipe_engine/recipe_modules/properties], [recipe\_engine/step][recipe_engine/recipe_modules/step]
diff --git a/recipe_modules/bisect_build/api.py b/recipe_modules/bisect_build/api.py
index 384dce2..f2da98a 100644
--- a/recipe_modules/bisect_build/api.py
+++ b/recipe_modules/bisect_build/api.py
@@ -20,6 +20,10 @@
   def is_bisecting(self):
     return 'bisect_reason' in self.m.properties
 
+  @property
+  def is_enabled(self):  # pragma: no cover
+    return self.m.properties.get('bisection_enabled', False)
+
   def schedule(self, repo_url, reason, is_experimental=False):
     if self.is_bisecting():
       base_build = self.get_base_build()
diff --git a/recipes/dart/flutter_engine.py b/recipes/dart/flutter_engine.py
index 8e237ed..b7426d9 100644
--- a/recipes/dart/flutter_engine.py
+++ b/recipes/dart/flutter_engine.py
@@ -5,8 +5,14 @@
 import json
 
 from recipe_engine import recipe_api
-from recipe_engine.post_process import (DoesNotRunRE, DropExpectation, Filter,
-                                        MustRun, StatusSuccess, StatusException)
+from recipe_engine.post_process import (
+    DoesNotRunRE,
+    DropExpectation,
+    Filter,
+    MustRun,
+    StatusSuccess,
+    StatusException,
+)
 
 DEPS = [
     'bisect_build',
@@ -41,10 +47,6 @@
 LINEARIZED_REPO_URL = DART_GERRIT + LINEARIZED_REPO
 
 
-def bisection_is_enabled(api):
-  return api.properties.get('bisection_enabled', False)
-
-
 def KillTasks(api, checkout_dir, ok_ret='any'):
   """Kills leftover tasks from previous runs or steps."""
   dart_sdk_dir = checkout_dir.join('third_party', 'dart')
@@ -341,7 +343,7 @@
 
     BuildAndTest(api, start_dir, checkout_dir, flutter_rev)
   except recipe_api.StepFailure as failure:
-    if bisection_is_enabled(api) and api.buildbucket.gitiles_commit.id:
+    if api.bisect_build.is_enabled and api.buildbucket.gitiles_commit.id:
       api.bisect_build.schedule(LINEARIZED_REPO_URL, failure.reason)
     raise
   finally:
diff --git a/recipes/dart/neo.expected/failing-build-starts-bisection.json b/recipes/dart/neo.expected/failing-test-step-starts-bisection.json
similarity index 99%
rename from recipes/dart/neo.expected/failing-build-starts-bisection.json
rename to recipes/dart/neo.expected/failing-test-step-starts-bisection.json
index 02d573a..f4d6676 100644
--- a/recipes/dart/neo.expected/failing-build-starts-bisection.json
+++ b/recipes/dart/neo.expected/failing-test-step-starts-bisection.json
@@ -1839,7 +1839,7 @@
     ],
     "infra_step": true,
     "name": "schedule bisect (f4d35da881f8fd329a4d3e01dd78b66a502d5c49)",
-    "stdin": "{\"requests\": [{\"scheduleBuild\": {\"builder\": {\"bucket\": \"ci\", \"builder\": \"dart2js-win-debug-x64-firefox\", \"project\": \"dart\"}, \"experimental\": \"NO\", \"fields\": \"builder,createTime,createdBy,critical,endTime,id,input,number,output,startTime,status,updateTime\", \"gitilesCommit\": {\"host\": \"dart.googlesource.com\", \"id\": \"f4d35da881f8fd329a4d3e01dd78b66a502d5c49\", \"project\": \"sdk\", \"ref\": \"refs/heads/master\"}, \"priority\": 30, \"properties\": {\"bisect_base_build\": 4711, \"bisect_newer\": [\"8331c527346abecfe0ad081df241512bb4b7df50\"], \"bisect_older\": [], \"bisect_reason\": \"FAILURE\", \"bisection_enabled\": true}, \"requestId\": \"8945511751514863184-00000000-0000-0000-0000-000000001337\", \"tags\": [{\"key\": \"user_agent\", \"value\": \"recipe\"}]}}]}",
+    "stdin": "{\"requests\": [{\"scheduleBuild\": {\"builder\": {\"bucket\": \"ci\", \"builder\": \"dart2js-win-debug-x64-firefox\", \"project\": \"dart\"}, \"experimental\": \"NO\", \"fields\": \"builder,createTime,createdBy,critical,endTime,id,input,number,output,startTime,status,updateTime\", \"gitilesCommit\": {\"host\": \"dart.googlesource.com\", \"id\": \"f4d35da881f8fd329a4d3e01dd78b66a502d5c49\", \"project\": \"sdk\", \"ref\": \"refs/heads/master\"}, \"priority\": 30, \"properties\": {\"bisect_base_build\": 4711, \"bisect_newer\": [\"8331c527346abecfe0ad081df241512bb4b7df50\"], \"bisect_older\": [], \"bisect_reason\": \"1 out of 1 aggregated steps failed: Step('test results') (retcode: 1)\", \"bisection_enabled\": true}, \"requestId\": \"8945511751514863184-00000000-0000-0000-0000-000000001337\", \"tags\": [{\"key\": \"user_agent\", \"value\": \"recipe\"}]}}]}",
     "~followup_annotations": [
       "@@@STEP_LOG_LINE@json.output@{@@@",
       "@@@STEP_LOG_LINE@json.output@  \"responses\": [@@@",
@@ -1880,7 +1880,7 @@
       "@@@STEP_LOG_LINE@request@            \"8331c527346abecfe0ad081df241512bb4b7df50\"@@@",
       "@@@STEP_LOG_LINE@request@          ], @@@",
       "@@@STEP_LOG_LINE@request@          \"bisect_older\": [], @@@",
-      "@@@STEP_LOG_LINE@request@          \"bisect_reason\": \"FAILURE\", @@@",
+      "@@@STEP_LOG_LINE@request@          \"bisect_reason\": \"1 out of 1 aggregated steps failed: Step('test results') (retcode: 1)\", @@@",
       "@@@STEP_LOG_LINE@request@          \"bisection_enabled\": true@@@",
       "@@@STEP_LOG_LINE@request@        }, @@@",
       "@@@STEP_LOG_LINE@request@        \"requestId\": \"8945511751514863184-00000000-0000-0000-0000-000000001337\", @@@",
diff --git a/recipes/dart/neo.py b/recipes/dart/neo.py
index 4e0958c..132947e 100644
--- a/recipes/dart/neo.py
+++ b/recipes/dart/neo.py
@@ -5,7 +5,14 @@
 from google.protobuf import struct_pb2, json_format
 from recipe_engine import recipe_api
 
-from recipe_engine.post_process import DoesNotRunRE, MustRun, StatusException, StatusFailure, StatusSuccess
+from recipe_engine.post_process import (
+    DoesNotRunRE,
+    DropExpectation,
+    MustRun,
+    StatusException,
+    StatusFailure,
+    StatusSuccess,
+)
 
 from PB.go.chromium.org.luci.buildbucket.proto import common as common_pb2
 
@@ -61,23 +68,26 @@
 }
 
 
-def bisection_is_enabled(api):
-  return api.properties.get('bisection_enabled', False)
+def _is_infra_failure(api, failure):
+  return (isinstance(failure, api.step.InfraFailure) or
+          api.dart.has_infra_failure(failure))
 
 
-def schedule_bisection(api, failure=None):
-  if not bisection_is_enabled(api):
+def _compute_failure_reason(api, failure):
+  assert not failure or not _is_infra_failure(api, failure)
+  return failure.reason if failure else api.bisect_build.REASON_SUCCESS
+
+
+def _schedule_bisection(api, failure=None):
+  if not api.bisect_build.is_enabled:
     # This builder is not configured to use bisection.
     return
-  if (isinstance(failure, api.step.InfraFailure) or
-      api.dart.has_infra_failure(failure)):
+  if _is_infra_failure(api, failure):
     # TODO(karlklose): fan out on infra failures during bisection and consider
     #                  starting a bisection for some kinds of infra failures.
     return
   elif api.bisect_build.is_bisecting() or failure:
-    # TODO(karlklose): distinguish between test failures and build failures (and
-    #                  start additional bisection runs for different failures).
-    reason = "FAILURE" if failure else api.bisect_build.REASON_SUCCESS
+    reason = _compute_failure_reason(api, failure)
     api.bisect_build.schedule("https://dart.googlesource.com/sdk", reason)
 
 
@@ -89,9 +99,9 @@
     with api.osx_sdk('mac'), api.context(env=env):
       _run_steps_impl(api, properties)
   except api.step.StepFailure as failure:
-    schedule_bisection(api, failure)
+    _schedule_bisection(api, failure)
     raise
-  schedule_bisection(api)
+  _schedule_bisection(api)
 
 
 def _run_steps_impl(api, properties):
@@ -184,7 +194,7 @@
   )
 
   yield api.test(
-      'failing-build-starts-bisection',
+      'failing-test-step-starts-bisection',
       api.buildbucket.ci_build(
           builder='dart2js-win-debug-x64-firefox',
           git_repo='https://dart.googlesource.com/sdk',
@@ -204,10 +214,32 @@
       api.step_data(
           'gitiles log: 2d72510e447ab60a9728aeea2362d8be2cbd7789..%s' %
           TESTED_REVISION, api.gitiles.make_log_test_data('master')),
-      # Make build fail by failing the 'test results' step
+      # Make test step fail by failing the 'test results' step
       api.step_data('test results', retcode=1),
       api.post_process(
           MustRun,
           'schedule bisect (f4d35da881f8fd329a4d3e01dd78b66a502d5c49)'),
       api.post_process(StatusFailure),
   )
+
+  yield api.test(
+      'different-failure-in-bisection-schedules-two-bisection-builds',
+      api.buildbucket.ci_build(
+          builder='dart2js-win-debug-x64-firefox',
+          git_repo='https://dart.googlesource.com/sdk',
+          project='dart',
+          revision=TESTED_REVISION),
+      api.properties(
+          bisection_enabled=True,
+          bisect_newer=['a', 'b', 'c'],
+          bisect_older=['c', 'd', 'e'],
+          bisect_base_build=4711,
+          bisect_reason="Step('Build') (retcode: 1)",
+      ),
+      # Make build step fail by failing the 'test results' step
+      api.step_data('test results', retcode=1),
+      api.post_process(MustRun, 'schedule bisect (b)'),
+      api.post_process(MustRun, 'schedule bisect (d)'),
+      api.post_process(StatusFailure),
+      api.post_process(DropExpectation),
+  )