Optofidelity: Adjustments for one-off tests

This CL makes some minor tweaks to get the system up and running
again for one-off tests only.

BUG=None

Change-Id: I2b4a56564ed5d46dcab11b3081b7dda4052917b9
Reviewed-on: https://chromium-review.googlesource.com/336539
Commit-Ready: Dennis Kempin <denniskempin@chromium.org>
Tested-by: Dennis Kempin <denniskempin@chromium.org>
Reviewed-by: Dennis Kempin <denniskempin@chromium.org>
diff --git a/optofidelity/apps/web/scroll.html b/optofidelity/apps/web/scroll.html
index 719340c..63c37ec 100644
--- a/optofidelity/apps/web/scroll.html
+++ b/optofidelity/apps/web/scroll.html
@@ -21,10 +21,13 @@
     }
     body {
       margin: 0;
-      -webkit-user-select: none;
     }
     #container {
       cursor: pointer;
+      -webkit-user-select: none;
+      -moz-user-select: none;
+      -ms-user-select: none;
+      -webkit-tap-highlight-color: rgba(0,0,0,0);
     }
   </style>
   <script>
diff --git a/optofidelity/config/base.xml b/optofidelity/config/base.xml
index 0162690..4701c2a 100644
--- a/optofidelity/config/base.xml
+++ b/optofidelity/config/base.xml
@@ -31,7 +31,7 @@
       <setting namespace="global"  name="airplane_mode_on" value="1" />
       <setting namespace="global"  name="stay_on_while_plugged_in" value="1" />
     </setup>
-    <collector type="systrace" adb="{dut.adb}" sdk-path="env:ANDROID_SDK_ROOT" />
+    <!--collector type="systrace" adb="{dut.adb}" sdk-path="env:ANDROID_SDK_ROOT" /-->
     <dashboard type="spreadsheet"
                sheet-key="1VmhTaavuPt4ZRgwC_ZBL6CddqpdgC9-04pgy28TvO0I"
                oauth-key-file="file:oauth2key.json"
@@ -43,7 +43,7 @@
 
   <!-- Clank -->
   <subject-type type-name="clank_base" data="file:../apps/web"
-                parent-type="adb_base" margin="(30, 15)">
+                parent-type="adb_base" margin="(30, 15)" channel="">
     <navigator type="web_adb" adb="{dut.adb}">
       <activity name="calibration" file="{subject.data}/flash.html" />
       <activity name="scroll" file="{subject.data}/scroll.html" />
@@ -51,27 +51,29 @@
       <activity name="tap" file="{subject.data}/tap.html" />
       <resource file="{subject.data}/jquery.min.js" />
     </navigator>
-    <updater type="chrome" adb="{dut.adb}"/>
+    <collector type="chrome_profile" adb="{dut.adb}"
+               chromium-path="env:CHROMIUM_SRC" browser="{subject.channel}" />
+    <updater type="chrome" adb="{dut.adb}" channel="{subject.channel}" />
     <benchmark name="tap" type="tap" activity="tap" />
     <benchmark name="scroll" type="line_draw" activity="scroll" />
   </subject-type>
 
-  <subject-type type-name="clank_stable" parent-type="clank_base">
+  <subject-type type-name="clank_stable" parent-type="clank_base"
+                channel="stable">
     <navigator type="web_adb"
                component="com.android.chrome/com.google.android.apps.chrome.Main" />
-    <updater type="chrome" channel="stable"/>
   </subject-type>
 
-  <subject-type type-name="clank_beta" parent-type="clank_base">
+  <subject-type type-name="clank_beta" parent-type="clank_base"
+                channel="beta">
     <navigator type="web_adb"
                component="com.chrome.beta/com.google.android.apps.chrome.Main" />
-    <updater type="chrome" channel="beta"/>
   </subject-type>
 
-  <subject-type type-name="clank_dev" parent-type="clank_base">
+  <subject-type type-name="clank_dev" parent-type="clank_base"
+                channel="dev">
     <navigator type="web_adb"
                component="com.chrome.dev/com.google.android.apps.chrome.Main" />
-    <updater type="chrome" channel="dev"/>
   </subject-type>
 
   <!-- Android Native -->
@@ -92,7 +94,7 @@
   </subject-type>
 
   <subject-type type-name="android_native" parent-type="android_native_base"
-                margin="(20, 15)">
+                margin="(10, 15)">
     <benchmark name="tap" type="tap" activity="tap" />
     <benchmark name="scroll" type="line_draw" activity="scroll" />
   </subject-type>
@@ -113,7 +115,7 @@
   </subject-type>
 
   <!-- iOS -->
-  <subject-type type-name="ios" parent-type="base" margin="(15, 15)">
+  <subject-type type-name="ios" parent-type="base" margin="(5, 20)">
     <navigator type="robot">
       <activity name="neutral" icon="file:icons/neutral.shm" />
       <activity name="calibration" icon="file:icons/flash.shm" />
diff --git a/optofidelity/config/temp.xml b/optofidelity/config/temp.xml
index 369faa2..adc65b4 100644
--- a/optofidelity/config/temp.xml
+++ b/optofidelity/config/temp.xml
@@ -1,13 +1,13 @@
 <optofidelity>
   <include file="base.xml" />
 
-  <dut name="bullhead" backend-name="temp3" margin="(15, 15)" exposure="2" adb="00656e9429c02d0f">
+  <dut name="shamrock" type="base" backend-name="angler" margin="(15, 15)"
+       exposure="2" adb="bbabca4c" port="13">
     <subject name="native" type="android_native" />
   </dut>
-  <dut name="ryu" backend-name="nexus9" adb="usb:9-1.1.3.3" exposure="2">
+
+  <dut name="seed" type="base" backend-name="bullhead" margin="(20, 20)"
+       exposure="2" adb="552cfc20" port="12">
     <subject name="native" type="android_native" />
   </dut>
-  <dut name="iphone6plus" backend-name="temp2" exposure="2.5">
-    <subject name="native" type="ios" />
-  </dut>
 </optofidelity>
diff --git a/optofidelity/optofidelity/benchmark/benchmark.py b/optofidelity/optofidelity/benchmark/benchmark.py
index 18d3a42..c03a0a6 100644
--- a/optofidelity/optofidelity/benchmark/benchmark.py
+++ b/optofidelity/optofidelity/benchmark/benchmark.py
@@ -63,8 +63,10 @@
     """
     self.results.metadata["subject_name"] = subject.name
     subject.navigator.OpenActivity(self._delegate.activity)
-    self.video = self._delegate.ExecuteOnSubject(subject)
-    subject.navigator.CloseActivity()
+    try:
+      self.video = self._delegate.ExecuteOnSubject(subject)
+    finally:
+      subject.navigator.CloseActivity()
     self.framerate = self.video.framerate
     self.trace = None
     self.results.measurements = None
diff --git a/optofidelity/optofidelity/components.py b/optofidelity/optofidelity/components.py
index 9945db9..c30c4ab 100644
--- a/optofidelity/optofidelity/components.py
+++ b/optofidelity/optofidelity/components.py
@@ -9,7 +9,8 @@
 from .detection.fake import FakeVideoProcessor
 from .orchestrator import Orchestrator
 from .orchestrator.access import Access, CambrionixAccess, FakeAccess
-from .orchestrator.collector import Collector, FakeCollector, SystraceCollector
+from .orchestrator.collector import (ChromeProfileCollector, Collector,
+                                     FakeCollector, SystraceCollector)
 from .orchestrator.dashboard import (ChromePerfDashboard, Dashboard,
                                      FakeDashboard, SpreadsheetDashboard)
 from .orchestrator.subject_setup import (ADBSettingsSetup, FakeSubjectSetup,
@@ -77,6 +78,7 @@
   },
   Collector: {
     "fake": FakeCollector,
-    "systrace": SystraceCollector
+    "systrace": SystraceCollector,
+    "chrome_profile": ChromeProfileCollector
   }
 }
diff --git a/optofidelity/optofidelity/detection/_finger_detector.py b/optofidelity/optofidelity/detection/_finger_detector.py
index 96a5d1f..26a3ac1 100644
--- a/optofidelity/optofidelity/detection/_finger_detector.py
+++ b/optofidelity/optofidelity/detection/_finger_detector.py
@@ -26,7 +26,7 @@
   LOW_PASS_KERNEL_SIZE = 3
   """Kernel size of time-series low pass for smoothing finger location."""
 
-  MIN_FINGER_WEIGHT = 0.3
+  MIN_FINGER_WEIGHT = 0.5
 
   def Preprocess(self, calib_frame, debugger):
     def log(msg, *params):
diff --git a/optofidelity/optofidelity/detection/_state_change_detector.py b/optofidelity/optofidelity/detection/_state_change_detector.py
index 46648e2..3b16ed6 100644
--- a/optofidelity/optofidelity/detection/_state_change_detector.py
+++ b/optofidelity/optofidelity/detection/_state_change_detector.py
@@ -34,7 +34,8 @@
 
   NAME = "state_change"
 
-  SETTLED_THRESHOLDS = dict(max_mean_distance=2.0, window_size=20)
+  SETTLED_THRESHOLDS = dict(max_rel_mean_distance=1.0, max_mean_distance=0.1,
+                            window_size=20, combination="or")
   """Thresholds to identify when a state has settled.
      (see nputil.FindSettlingIndex)"""
 
@@ -238,16 +239,17 @@
   def GenerateEvents(self, preprocessed_data, debug=False):
     """Segments the array of state change levels into discrete state changes."""
     if debug:
-      for data in preprocessed_data:
-        print "%.4f" % data
+      for i, data in enumerate(preprocessed_data):
+        print "%04d: %.4f" % (i, data)
 
     level = np.asarray(preprocessed_data, np.float)
     normalized = nputil.NormalizeStates(level)
+
     filtered = nputil.LowPass(normalized, 5)
 
     if debug:
       pyplot.figure()
-      pyplot.plot(normalized)
+      pyplot.plot(normalized, "-x")
       pyplot.plot(filtered)
 
     for start, end in nputil.FindStateChanges(normalized,
diff --git a/optofidelity/optofidelity/orchestrator/access.py b/optofidelity/optofidelity/orchestrator/access.py
index b25a7d5..a2ed342 100644
--- a/optofidelity/optofidelity/orchestrator/access.py
+++ b/optofidelity/optofidelity/orchestrator/access.py
@@ -37,7 +37,8 @@
 
 
 class CambrionixAccess(Access):
-  ADB_TIMEOUT = 10
+  ADB_TIMEOUT = 2 * 60
+  """Wait for device to connect for 2 minutes."""
 
   def __init__(self, serial_device, port_id, adb_serial):
     super(CambrionixAccess, self).__init__()
diff --git a/optofidelity/optofidelity/orchestrator/collector.py b/optofidelity/optofidelity/orchestrator/collector.py
index ecef4b0..ea58ac5 100644
--- a/optofidelity/optofidelity/orchestrator/collector.py
+++ b/optofidelity/optofidelity/orchestrator/collector.py
@@ -103,3 +103,44 @@
   @property
   def report_links(self):
     return [("Systrace", "systrace.html")]
+
+
+class ChromeProfileCollector(Collector):
+  """Collection of Chrome profile traces."""
+
+  def __init__(self, adb_device, chromium_path, browser):
+    profile_script = os.path.join(chromium_path, "build", "android",
+                                  "adb_profile_chrome")
+    self.base_cmd = [profile_script,
+                     "-d", adb_device,
+                     "--browser", browser,
+                     "--systrace", "gfx,view,input,freq,sched"]
+    self.process = None
+    self.tempfile = NamedTemporaryFile()
+
+  @classmethod
+  def FromConfig(cls, parameters, children):
+    return cls(parameters["adb"], parameters["chromium-path"],
+               parameters["browser"])
+
+  def Start(self, duration):
+    seconds = int(math.ceil(float(duration) / 1000.0))
+    cmd = self.base_cmd + ["-t", str(seconds), "-o", self.tempfile.name]
+    _log.info("Starting: %s", " ".join(cmd))
+    self.process = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE,
+                                    stderr=subprocess.STDOUT)
+
+  def Stop(self):
+    stdout, stderr = self.process.communicate()
+    if self.process.returncode > 0:
+      _log.error(stdout)
+      _log.error("Systrace failed to collect")
+    else:
+      _log.debug(stdout)
+
+  def Save(self, folder):
+    shutil.copy(self.tempfile.name, os.path.join(folder, "chrome_profile.html"))
+
+  @property
+  def report_links(self):
+    return [("ChromeProfile", "chrome_profile.html")]
\ No newline at end of file
diff --git a/optofidelity/optofidelity/system/navigator.py b/optofidelity/optofidelity/system/navigator.py
index ca02be4..fb4c390 100644
--- a/optofidelity/optofidelity/system/navigator.py
+++ b/optofidelity/optofidelity/system/navigator.py
@@ -89,16 +89,16 @@
     return cls()
 
   def Open(self):
-    if self._is_open:
-      raise Exception("Subject already open.")
+    # if self._is_open:
+    #   raise Exception("Subject already open.")
     self._Open()
     self._is_open = True
 
   def Close(self):
-    if not self._is_open:
-      raise Exception("Subject has not been opened.")
-    if self._current_activity:
-      raise Exception("Activity still open.")
+    # if not self._is_open:
+    #   raise Exception("Subject has not been opened.")
+    # if self._current_activity:
+    #   raise Exception("Activity still open.")
     self._Close()
     self._is_open = False
 
@@ -114,10 +114,10 @@
       self.Close()
 
   def OpenActivity(self, activity_name):
-    if not self._is_open:
-      raise Exception("Can't open activity if subject has not been opened.")
-    if self._current_activity:
-      raise Exception("Another activity is already open.")
+    # if not self._is_open:
+    #   raise Exception("Can't open activity if subject has not been opened.")
+    # if self._current_activity:
+    #   raise Exception("Another activity is already open.")
     if not self.HasActivity(activity_name):
       raise ValueError("Unknown activity name '%s'" % activity_name)
 
@@ -126,8 +126,8 @@
 
   def CloseActivity(self):
     """Close previously opened activity."""
-    if not self._current_activity:
-      raise Exception("No activity open.")
+    # if not self._current_activity:
+    #   raise Exception("No activity open.")
     self._CloseActivity()
     self._current_activity = None
 
diff --git a/optofidelity/optofidelity/util/nputil.py b/optofidelity/optofidelity/util/nputil.py
index 03dc8ef..b9a2bb9 100644
--- a/optofidelity/optofidelity/util/nputil.py
+++ b/optofidelity/optofidelity/util/nputil.py
@@ -66,11 +66,24 @@
 def NormalizeStates(array, crossing_threshold=0.5):
   """Normalize array to 0..1"""
   pre_norm = Normalize(array)
-  min = np.mean(pre_norm[pre_norm < crossing_threshold])
-  max = np.mean(pre_norm[pre_norm > crossing_threshold])
+  min = np.median(pre_norm[pre_norm < crossing_threshold])
+  max = np.median(pre_norm[pre_norm > crossing_threshold])
   return (pre_norm - min) / (max - min)
 
 
+def HysteresisThreshold(array, t_low, t_high, initial=False):
+  """Hysteresis thresholding of array."""
+  result = np.empty(array.shape, dtype=np.bool)
+  current = initial
+  for index, value in enumerate(array):
+    if value > t_high:
+      current = True
+    elif value < t_low:
+      current = False
+    result[index] = current
+  return result
+
+
 def LowPass(array, kernel_size=5):
   """Returns low pass filtered array."""
   kernel = np.ones(kernel_size,) / kernel_size
@@ -190,34 +203,58 @@
 def FindSettlingIndex(profile, start, window_size=8,
                       min_value=None, max_value=None, max_mid_range=None,
                       max_slope=None, max_distance=None, max_mean_distance=None,
+                      max_rel_mean_distance=None, combination="and",
                       debug=False):
   """Returns index of first settled value."""
+  smoothed = LowPass(profile, 5)
+  increasing = smoothed[start] < smoothed[start + 1]
+
   for i in range(start, len(profile) - 1):
+    debug_values = []
+    conditions = []
+    def AddCondition(threshold, value, name, condition):
+      if threshold is None:
+        return
+      if debug:
+        debug_values.append(u"%s=%.2f%s" % (name, value, u"\u2713" if condition else ""))
+      conditions.append(condition)
+    def MinCondition(threshold, value, name):
+      AddCondition(threshold, value, name, value > threshold)
+    def MaxCondition(threshold, value, name):
+      AddCondition(threshold, value, name, value < threshold)
+
+    value = profile[i]
     window_end = Truncate(i + window_size, 0, len(profile))
     moving_window = profile[i:(window_end + 1)]
-    value = profile[i]
-    mid_range = np.max(moving_window) - np.min(moving_window)
-    slope = profile[i + 1] - profile[i]
-    distance = (i - start)
     window_mean = np.mean(moving_window[1:])
     window_std = np.std(moving_window[1:])
-    mean_distance = np.abs(value - window_mean) / window_std
+    mean_distance = window_mean - value
+    if not increasing:
+      mean_distance *= -1;
+
+    MinCondition(min_value, value, "v")
+    MaxCondition(max_value, value, "v")
+    MaxCondition(max_slope, np.abs(profile[i + 1] - profile[i]), "s")
+    MaxCondition(max_mid_range, np.max(moving_window) - np.min(moving_window), "mr")
+    MaxCondition(max_mean_distance, mean_distance, "md")
+    MaxCondition(max_rel_mean_distance, mean_distance / window_std, "rmd")
+
     if debug:
-      print "%03d: v=%.2f mr=%.2f s=%.2f d=%d md=%.2f" % (
-          i, value, mid_range, slope, distance, mean_distance)
-    if max_distance is not None and distance > max_distance:
+      separator = u" %s " % combination
+      print "%03d: d=%d" % (i, (i - start)), separator.join(debug_values)
+
+    if max_distance is not None and (i - start) > max_distance:
       return None
-    if min_value is not None and value < min_value:
-      continue
-    if max_value is not None and value > max_value:
-      continue
-    if max_slope is not None and np.abs(slope) > max_slope:
-      continue
-    if max_mid_range is not None and mid_range > max_mid_range:
-      continue
-    if max_mean_distance is not None and mean_distance > max_mean_distance:
-      continue
-    return i
+
+    if combination == "and":
+      if np.all(conditions):
+        return i
+    elif combination == "or":
+      if np.any(conditions):
+        return i
+    else:
+      raise ValueError("Combination has to be one of: (and, or)")
+
   return len(profile) - 1
 
 
@@ -242,7 +279,7 @@
   """
   # Find indices when the array is crossing the threshold, then find the start
   # and end for each crossing.
-  crossings = FindZeroCrossings(LowPass(array, 3) - crossing_threshold)
+  crossings = FindZeroCrossings(LowPass(array, 5) - crossing_threshold)
 
   for crossing in crossings:
     prev = array[max(crossing - 5, 0)]
diff --git a/optofidelity/regression_tests.py b/optofidelity/regression_tests.py
index 8b60523..5bcd373 100644
--- a/optofidelity/regression_tests.py
+++ b/optofidelity/regression_tests.py
@@ -58,6 +58,8 @@
       Test("20150928_2234_000", {}),
   "keyboard.mask_issues.s6edge":
       Test("20151002_1417_000", {}),
+  "keyboard.segmentation_error.s6edge":
+      Test("20151008_0437_000", {}),
 
   "baseline.tap.nexus4":
       Test("20150825_1908_000", {"DownLatency": 98, "UpLatency": 81}),
diff --git a/optofidelity/requirements.txt b/optofidelity/requirements.txt
index 5441ff1..51ee984 100644
--- a/optofidelity/requirements.txt
+++ b/optofidelity/requirements.txt
@@ -1,26 +1,48 @@
-alembic==0.7.5.post2
+alembic==0.8.5
 Bottleneck==1.0.0
-dataset==0.5.6
-decorator==3.4.2
-Jinja2==2.7.3
-Mako==1.0.1
+cffi==1.5.2
+cryptography==1.3.1
+cycler==0.10.0
+dask==0.8.1
+dataset==0.6.2
+decorator==4.0.9
+enum34==1.1.2
+funcsigs==0.4
+gspread==0.3.0
+httplib2==0.9.2
+idna==2.1
+ipaddress==1.0.16
+Jinja2==2.8
+Mako==1.0.4
 MarkupSafe==0.23
-matplotlib==1.4.3
-mock==1.0.1
-networkx==1.9.1
-nose==1.3.6
-nose2==0.5.0
-numpy==1.9.2
-Pillow==2.8.1
-pyparsing==2.0.3
-python-dateutil==2.4.2
-python-slugify==1.0.1
-pytz==2015.2
+matplotlib==1.5.1
+mock==1.3.0
+networkx==1.11
+normality==0.2.4
+nose==1.3.7
+nose2==0.6.4
+numpy==1.11.0
+oauth2client==1.5.2
+pbr==1.8.1
+Pillow==3.1.1
+pyasn1==0.1.9
+pyasn1-modules==0.0.8
+pycparser==2.14
+pycurl==7.43.0
+pyOpenSSL==16.0.0
+pyparsing==2.1.1
+pyserial==3.0.1
+python-dateutil==2.5.2
+python-editor==0.5
+python-slugify==1.2.0
+pytz==2016.3
 PyYAML==3.11
-requests==2.6.0
+requests==2.9.1
+rsa==3.4.2
 safetynet==0.2.2
 scikit-image==0.11.3
-scipy==0.15.1
-six==1.9.0
-SQLAlchemy==0.9.9
-Unidecode==0.4.17
+scipy==0.17.0
+six==1.10.0
+SQLAlchemy==1.0.12
+toolz==0.7.4
+Unidecode==0.4.19
diff --git a/optofidelity/tests/config.py b/optofidelity/tests/config.py
index 67c3f6a..624bed4 100644
--- a/optofidelity/tests/config.py
+++ b/optofidelity/tests/config.py
@@ -41,7 +41,7 @@
         raise AssertionError("User did not accept test result")
 
 CONFIG = TestConfig(
-  adb_device_id = None,
+  adb_device_id = "NP5A340401",
   robot_ip = None,
   phantom_ip = None,
   dut_name = None,
@@ -50,6 +50,7 @@
   chromeperf_endpoint = None,
   gs_folder_url = None,
   android_sdk_path = None,
+  chromium_path = "/usr/local/google/home/denniskempin/workspace/chromium/src",
 
   cambrionix_serial_device = None,
   cambrionix_dut_port_id = None,
diff --git a/optofidelity/tests/detection/test_state_change_detector.py b/optofidelity/tests/detection/test_state_change_detector.py
index 9d720fa..5c0f75f 100644
--- a/optofidelity/tests/detection/test_state_change_detector.py
+++ b/optofidelity/tests/detection/test_state_change_detector.py
@@ -102,6 +102,20 @@
   0.0109, 0.0149, 0.0137, 0.0097, 0.0099, 0.0112, 0.0144, 0.0122, 0.0087,
 ]
 
+change_sequence_noisy2 = [
+  0.0688, 0.0682, 0.0693, 0.0479, 0.0645, 0.0631, 0.0608, 0.0628, 0.0486,
+  0.0648, 0.0589, 0.0667, 0.0621, 0.0528, 0.0607, 0.0598, 0.0700, 0.0601,
+  0.0611, 0.0655, 0.0652, 0.0700, 0.0559, 0.0609, 0.0660, 0.0667, 0.0719,
+  0.0511, 0.0671, 0.0644, 0.0680, 0.0717, 0.0490, 0.0635, 0.0680, 0.0638,
+  0.0703, 0.0469, 0.0644, 0.0644, 0.0821, 0.0795, 0.0949, 0.1106, 0.1189,
+  0.1014, 0.0852, 0.1063, 0.0946, 0.1146, 0.0967, 0.0859, 0.1100, 0.1015,
+  0.1096, 0.0909, 0.0856, 0.1122, 0.0986, 0.1055, 0.0895, 0.0869, 0.1121,
+  0.0997, 0.1053, 0.0865, 0.0896, 0.1171, 0.1019, 0.1032, 0.0838, 0.0921,
+  0.1175, 0.1134, 0.0978, 0.0807, 0.0973, 0.1193, 0.1171, 0.0970, 0.0808,
+  0.1035, 0.1216, 0.1177, 0.0966, 0.0800, 0.1112, 0.1028, 0.1120, 0.0915,
+  0.0805, 0.1153, 0.1011, 0.1096, 0.0896, 0.0822, 0.1207, 0.1001, 0.1064,
+]
+
 change_sequence_multilevel = [
   0.0106, 0.0119, 0.0149, 0.0138, 0.0132, 0.0115, 0.0141, 0.0152, 0.0136,
   0.0129, 0.0116, 0.0142, 0.0152, 0.0136, 0.0121, 0.0113, 0.0134, 0.0132,
@@ -158,11 +172,12 @@
   0.0119, 0.0141, 0.0129, 0.0133, 0.0142, 0.0122, 0.0140, 0.0136, 0.0124,
 ]
 
+
 class StateChangeDetectorTest(DetectorTest):
   def testEventGeneration(self):
     expected_events = [
       StateChangeEvent(95, 90, StateChangeEvent.STATE_OPEN),
-      StateChangeEvent(156, 150, StateChangeEvent.STATE_CLOSED),
+      StateChangeEvent(153, 150, StateChangeEvent.STATE_CLOSED),
     ]
     self.assertExpectedEventsGenerated(StateChangeDetector(None, None),
                                        change_sequence_1, expected_events,
@@ -170,7 +185,7 @@
 
   def testPWMProofEvents(self):
     expected_events = [
-      StateChangeEvent(140, 138, StateChangeEvent.STATE_OPEN),
+      StateChangeEvent(140, 139, StateChangeEvent.STATE_OPEN),
       StateChangeEvent(216, 215, StateChangeEvent.STATE_CLOSED),
     ]
     self.assertExpectedEventsGenerated(StateChangeDetector(None, None),
@@ -180,16 +195,24 @@
   def testNoisyEventGeneration(self):
     expected_events = [
       StateChangeEvent(115, 115, StateChangeEvent.STATE_OPEN),
-      StateChangeEvent(195, 193, StateChangeEvent.STATE_CLOSED),
+      StateChangeEvent(195, 190, StateChangeEvent.STATE_CLOSED),
     ]
     self.assertExpectedEventsGenerated(StateChangeDetector(None, None),
                                        change_sequence_noisy, expected_events,
                                        ignore=AnalogStateEvent)
 
+  def testNoisyEventGeneration2(self):
+    expected_events = [
+      StateChangeEvent(42, 40, StateChangeEvent.STATE_OPEN),
+    ]
+    self.assertExpectedEventsGenerated(StateChangeDetector(None, None),
+                                       change_sequence_noisy2, expected_events,
+                                       ignore=AnalogStateEvent)
+
   def testMultilevelEventGeneration(self):
     expected_events = [
       StateChangeEvent(193, 192, StateChangeEvent.STATE_OPEN),
-      StateChangeEvent(268, 268, StateChangeEvent.STATE_CLOSED),
+      StateChangeEvent(269, 268, StateChangeEvent.STATE_CLOSED),
       StateChangeEvent(346, 344, StateChangeEvent.STATE_OPEN),
       StateChangeEvent(420, 420, StateChangeEvent.STATE_CLOSED),
     ]
diff --git a/optofidelity/tests/orchestrator/test_collectors.py b/optofidelity/tests/orchestrator/test_collectors.py
index 92bce72..32f9233 100644
--- a/optofidelity/tests/orchestrator/test_collectors.py
+++ b/optofidelity/tests/orchestrator/test_collectors.py
@@ -3,16 +3,17 @@
 # found in the LICENSE file.
 """Tests for the Orchestrator class."""
 from unittest import TestCase
+import os
 import shutil
 import tempfile
-import os
 
-from optofidelity.orchestrator.collector import SystraceCollector
+from optofidelity.orchestrator.collector import (ChromeProfileCollector,
+                                                 SystraceCollector)
 from optofidelity.util import CreateComponentFromXML
 from tests.config import CONFIG
 
 
-class TestSystraceCollection(TestCase):
+class TestSystraceCollector(TestCase):
   def setUp(self):
     self.tempdir = tempfile.mkdtemp()
 
@@ -34,3 +35,28 @@
     self.assertTrue(os.path.exists(filename))
 
     CONFIG.AskUserAccept("Verify file://" + filename)
+
+
+class TestChromeProfileCollector(TestCase):
+  def setUp(self):
+    self.tempdir = tempfile.mkdtemp()
+
+  def tearDown(self):
+    shutil.rmtree(self.tempdir)
+
+  def testCollection(self):
+    adb_device = CONFIG["adb_device_id"]
+    chromium_path = CONFIG["chromium_path"]
+    config_str = ("<collector adb='{adb}' chromium-path='{chromium_path}' " +
+                  "browser='stable' />")
+    config = config_str.format(adb=adb_device, chromium_path=chromium_path)
+
+    collector = CreateComponentFromXML(ChromeProfileCollector, config)
+    collector.Start(1000)
+    collector.Stop()
+    collector.Save(self.tempdir)
+
+    filename = os.path.join(self.tempdir, "chrome_profile.html")
+    self.assertTrue(os.path.exists(filename))
+
+    CONFIG.AskUserAccept("Verify file://" + filename)