bisect-kit: Add a reproduced in summary log

TEST log sceanarios:
  1. Reproduced case
  2. Unreproduced failures
  3. Unreproduced regression
  4. Failure before init (e.g. Failed in ./setup_cros_bisect.py)

BUG=b:227289034, b:227289270, b:227289527
TEST=./diagnose_cros_tast.py view
TEST=./diagnose_cros_autotest.py view

Change-Id: I3cb2e53829858966f3418a16a16bf30833f64057
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/bisect-kit/+/3586473
Reviewed-by: Tai-Hsu Lin <sheckylin@chromium.org>
Commit-Queue: Zheng-Jie Chang <zjchang@chromium.org>
Tested-by: Zheng-Jie Chang <zjchang@chromium.org>
diff --git a/bisect_kit/bisector_cli.py b/bisect_kit/bisector_cli.py
index 465bf5e..ffec82d 100644
--- a/bisect_kit/bisector_cli.py
+++ b/bisect_kit/bisector_cli.py
@@ -677,6 +677,9 @@
         'rev_info': [],
     }
 
+    summary.update({
+        'reproduced': self.strategy.check_reproduced(),
+    })
     for info in self.strategy.rev_info:
       info_dict = info.to_dict()
       detail = self.states.details.get(info.rev, {})
diff --git a/bisect_kit/strategy.py b/bisect_kit/strategy.py
index e49da54..fa1de3f 100644
--- a/bisect_kit/strategy.py
+++ b/bisect_kit/strategy.py
@@ -716,9 +716,9 @@
   def get_range(self, confidence=None):
     """Gets narrowed-down bisection range so far.
 
-    Using a heuristic algorithm to find a range (left, right) that,
-    sum(prob[left:right]) > confidence. Some optimizations are applied so the
-    range may not be the narrowest.
+    Using a heuristic algorithm to find a semi-open range (left, right] that,
+    sum(prob[left+1:right+1]) >= confidence. Some optimizations are applied so
+    the range may not be the narrowest.
 
     If `confidence` value is bigger, the returned range is bigger.
 
@@ -816,6 +816,27 @@
             term_old=self.term_map.get('old', 'old'),
             term_new=self.term_map.get('new', 'new'))
 
+  def check_reproduced(self):
+    """Checks if the bisection is reproduced.
+
+    Returns true if:
+      1. Both old and new versions are tested, has 'old' or 'new' results.
+      2. It's not unreproduced, passes check_verification_range().
+
+    Returns:
+      True if the regression is reproduced.
+    """
+    old_count = self.rev_info[self.old_idx]['old']
+    new_count = self.rev_info[self.new_idx]['new']
+    if old_count == 0 or new_count == 0:
+      return False
+    try:
+      self.check_verification_range()
+    except (errors.VerifyOldBehaviorFailed, errors.VerifyNewBehaviorFailed,
+            errors.WrongAssumption):
+      return False
+    return True
+
   def check_proceed_with_skip(self, idx, reason):
     """Checks if we should proceed after skips.