def main(argv): parser = _stbt.core.argparser() parser.prog = 'stbt run' parser.description = 'Run an stb-tester test script' parser.add_argument( '--cache', default=imgproc_cache.default_filename, help="Path for image-processing cache (default: %(default)s") parser.add_argument( '--save-screenshot', default='on-failure', choices=['always', 'on-failure', 'never'], help="Save a screenshot at the end of the test to screenshot.png") parser.add_argument( '--save-thumbnail', default='never', choices=['always', 'on-failure', 'never'], help="Save a thumbnail at the end of the test to thumbnail.jpg") parser.add_argument( 'script', metavar='FILE[::TESTCASE]', help=( "The python test script to run. Optionally specify a python " "function name to run that function; otherwise only the script's " "top-level will be executed.")) parser.add_argument( 'args', nargs=argparse.REMAINDER, metavar='ARG', help='Additional arguments passed on to the test script (in sys.argv)') args = parser.parse_args(argv[1:]) debug("Arguments:\n" + "\n".join([ "%s: %s" % (k, v) for k, v in args.__dict__.items()])) dut = _stbt.core.new_device_under_test_from_config(args) with sane_unicode_and_exception_handling(args.script), \ video(args, dut), \ imgproc_cache.setup_cache(filename=args.cache): test_function = load_test_function(args.script, args.args) test_function.call()
def swipe(self, start_position, end_position): """Swipe from one point to another point. :param start_position: A `stbt.Region` or (x, y) tuple of coordinates at which to start. :param end_position: A `stbt.Region` or (x, y) tuple of coordinates at which to stop. Example:: d.swipe((100, 100), (100, 400)) """ x1, y1 = _centre_point(start_position) x2, y2 = _centre_point(end_position) debug("AdbDevice.swipe((%d,%d), (%d,%d))" % (x1, y1, x2, y2)) x1, y1 = self._to_native_coordinates(x1, y1) x2, y2 = self._to_native_coordinates(x2, y2) command = [ "shell", "input", "swipe", str(x1), str(y1), str(x2), str(y2) ] self.adb(command, timeout_secs=10)
def frames(self, timeout_secs=None): if timeout_secs is not None: end_time = self._time.time() + timeout_secs timestamp = None first = True while True: if self._use_old_threading_behaviour: timestamp = self._last_grabbed_frame_time ddebug("user thread: Getting sample at %s" % self._time.time()) frame = self._display.get_frame(max(10, timeout_secs), since=timestamp) ddebug("user thread: Got sample at %s" % self._time.time()) timestamp = frame.time if self._use_old_threading_behaviour: self._last_grabbed_frame_time = timestamp if not first and timeout_secs is not None and timestamp > end_time: debug("timed out: %.3f > %.3f" % (timestamp, end_time)) return yield frame first = False
def __exit__(self, _1, _2, _3): # Drain the frame queue while self._frames: self._push_sample(self._frames.pop()) if self._sample_count > 0: state = self.sink_pipeline.get_state(0) if (state[0] != Gst.StateChangeReturn.SUCCESS or state[1] != Gst.State.PLAYING): debug("teardown: Sink pipeline not in state PLAYING: %r" % (state, )) debug("teardown: Sending eos on sink pipeline") if self.appsrc.emit("end-of-stream") == Gst.FlowReturn.OK: self.sink_pipeline.send_event(Gst.Event.new_eos()) if not self.received_eos.wait(10): debug("Timeout waiting for sink EOS") else: debug("Sending EOS to sink pipeline failed") else: debug("SinkPipeline teardown: Not sending EOS, no samples sent") self.sink_pipeline.set_state(Gst.State.NULL) # Don't want to cause the Display object to hang around on our account, # we won't be raising any errors from now on anyway: self._raise_in_user_thread = None
def __exit__(self, _1, _2, _3): # Drain the frame queue while self._frames: self._push_sample(self._frames.pop()) if self._sample_count > 0: state = self.sink_pipeline.get_state(0) if (state[0] != Gst.StateChangeReturn.SUCCESS or state[1] != Gst.State.PLAYING): debug("teardown: Sink pipeline not in state PLAYING: %r" % (state,)) debug("teardown: Sending eos on sink pipeline") if self.appsrc.emit("end-of-stream") == Gst.FlowReturn.OK: self.sink_pipeline.send_event(Gst.Event.new_eos()) if not self.received_eos.wait(10): debug("Timeout waiting for sink EOS") else: debug("Sending EOS to sink pipeline failed") else: debug("SinkPipeline teardown: Not sending EOS, no samples sent") self.sink_pipeline.set_state(Gst.State.NULL) # Don't want to cause the Display object to hang around on our account, # we won't be raising any errors from now on anyway: self._raise_in_user_thread = None
def __init__(self, user_source_pipeline, sink_pipeline): import time self._condition = threading.Condition() # Protects last_frame self.last_frame = None self.last_used_frame = None self.source_pipeline = None self.init_time = time.time() self.tearing_down = False appsink = ("appsink name=appsink max-buffers=1 drop=false sync=true " "emit-signals=true " "caps=video/x-raw,format=BGR") # Notes on the source pipeline: # * _stbt_raw_frames_queue is kept small to reduce the amount of slack # (and thus the latency) of the pipeline. # * _stbt_user_data_queue before the decodebin is large. We don't want # to drop encoded packets as this will cause significant image # artifacts in the decoded buffers. We make the assumption that we # have enough horse-power to decode the incoming stream and any delays # will be transient otherwise it could start filling up causing # increased latency. self.source_pipeline_description = " ! ".join([ user_source_pipeline, 'queue name=_stbt_user_data_queue max-size-buffers=0 ' ' max-size-bytes=0 max-size-time=10000000000', "decodebin", 'queue name=_stbt_raw_frames_queue max-size-buffers=2', 'videoconvert', 'video/x-raw,format=BGR', appsink ]) self.create_source_pipeline() self._sink_pipeline = sink_pipeline debug("source pipeline: %s" % self.source_pipeline_description)
def draw_text(text, duration_secs=3): """Write the specified text to the output video. :param str text: The text to write. :param duration_secs: The number of seconds to display the text. :type duration_secs: int or float """ debug(text) return _dut.draw_text(text, duration_secs)
def __init__(self, user_sink_pipeline, raise_in_user_thread, save_video=""): import time as _time self.annotations_lock = threading.Lock() self.text_annotations = [] self.annotations = [] self._raise_in_user_thread = raise_in_user_thread self.received_eos = threading.Event() self._frames = deque(maxlen=35) self._time = _time self._sample_count = 0 # The test script can draw on the video, but this happens in a different # thread. We don't know when they're finished drawing so we just give # them 0.5s instead. self._sink_latency_secs = 0.5 sink_pipeline_description = ( "appsrc name=appsrc format=time is-live=true " "caps=video/x-raw,format=(string)BGR ") if save_video and user_sink_pipeline: sink_pipeline_description += "! tee name=t " src = "t. ! queue leaky=downstream" else: src = "appsrc." if save_video: if not save_video.endswith(".webm"): save_video += ".webm" debug("Saving video to '%s'" % save_video) sink_pipeline_description += ( "{src} ! videoconvert ! " "vp8enc cpu-used=6 min_quantizer=32 max_quantizer=32 ! " "webmmux ! filesink location={save_video} ").format( src=src, save_video=save_video) if user_sink_pipeline: sink_pipeline_description += ( "{src} ! videoconvert ! {user_sink_pipeline}").format( src=src, user_sink_pipeline=user_sink_pipeline) self.sink_pipeline = Gst.parse_launch(sink_pipeline_description) sink_bus = self.sink_pipeline.get_bus() sink_bus.connect("message::error", self._on_error) sink_bus.connect("message::warning", self._on_warning) sink_bus.connect("message::eos", self._on_eos_from_sink_pipeline) sink_bus.add_signal_watch() self.appsrc = self.sink_pipeline.get_by_name("appsrc") debug("sink pipeline: %s" % sink_pipeline_description)
def as_precondition(message): """Context manager that replaces test failures with test errors. Stb-tester's reports show test failures (that is, `UITestFailure` or `AssertionError` exceptions) as red results, and test errors (that is, unhandled exceptions of any other type) as yellow results. Note that `wait_for_match`, `wait_for_motion`, and similar functions raise a `UITestFailure` when they detect a failure. By running such functions inside an `as_precondition` context, any `UITestFailure` or `AssertionError` exceptions they raise will be caught, and a `PreconditionError` will be raised instead. When running a single testcase hundreds or thousands of times to reproduce an intermittent defect, it is helpful to mark unrelated failures as test errors (yellow) rather than test failures (red), so that you can focus on diagnosing the failures that are most likely to be the particular defect you are looking for. For more details see `Test failures vs. errors <http://stb-tester.com/preconditions>`__. :param str message: A description of the precondition. Word this positively: "Channels tuned", not "Failed to tune channels". :raises: `PreconditionError` if the wrapped code block raises a `UITestFailure` or `AssertionError`. Example:: def test_that_the_on_screen_id_is_shown_after_booting(): channel = 100 with stbt.as_precondition("Tuned to channel %s" % channel): mainmenu.close_any_open_menu() channels.goto_channel(channel) power.cold_reboot() assert channels.is_on_channel(channel) stbt.wait_for_match("on-screen-id.png") """ try: yield except (UITestFailure, AssertionError) as e: debug( "stbt.as_precondition caught a %s exception and will " "re-raise it as PreconditionError.\nOriginal exception was:\n%s" % (type(e).__name__, traceback.format_exc(e))) exc = PreconditionError(message, e) if hasattr(e, 'screenshot'): exc.screenshot = e.screenshot # pylint: disable=attribute-defined-outside-init,no-member raise exc
def __exit__(self, _1, _2, _3): self.tearing_down = True self.source_pipeline, source = None, self.source_pipeline if source: if self.source_teardown_eos: debug("teardown: Sending eos on source pipeline") for elem in gst_iterate(source.iterate_sources()): elem.send_event(Gst.Event.new_eos()) if not self.appsink_await_eos(source.get_by_name('appsink'), timeout=10): debug("Source pipeline did not teardown gracefully") source.set_state(Gst.State.NULL) source = None
def __exit__(self, _1, _2, _3): self.tearing_down = True self.source_pipeline, source = None, self.source_pipeline if source: if self.source_teardown_eos: debug("teardown: Sending eos on source pipeline") for elem in gst_iterate(source.iterate_sources()): elem.send_event(Gst.Event.new_eos()) if not self.appsink_await_eos( source.get_by_name('appsink'), timeout=10): debug("Source pipeline did not teardown gracefully") source.set_state(Gst.State.NULL) source = None
def as_precondition(message): """Context manager that replaces test failures with test errors. Stb-tester's reports show test failures (that is, `UITestFailure` or `AssertionError` exceptions) as red results, and test errors (that is, unhandled exceptions of any other type) as yellow results. Note that `wait_for_match`, `wait_for_motion`, and similar functions raise a `UITestFailure` when they detect a failure. By running such functions inside an `as_precondition` context, any `UITestFailure` or `AssertionError` exceptions they raise will be caught, and a `PreconditionError` will be raised instead. When running a single testcase hundreds or thousands of times to reproduce an intermittent defect, it is helpful to mark unrelated failures as test errors (yellow) rather than test failures (red), so that you can focus on diagnosing the failures that are most likely to be the particular defect you are looking for. For more details see `Test failures vs. errors <http://stb-tester.com/preconditions>`__. :param str message: A description of the precondition. Word this positively: "Channels tuned", not "Failed to tune channels". :raises: `PreconditionError` if the wrapped code block raises a `UITestFailure` or `AssertionError`. Example:: def test_that_the_on_screen_id_is_shown_after_booting(): channel = 100 with stbt.as_precondition("Tuned to channel %s" % channel): mainmenu.close_any_open_menu() channels.goto_channel(channel) power.cold_reboot() assert channels.is_on_channel(channel) stbt.wait_for_match("on-screen-id.png") """ try: yield except (UITestFailure, AssertionError) as e: debug("stbt.as_precondition caught a %s exception and will " "re-raise it as PreconditionError.\nOriginal exception was:\n%s" % (type(e).__name__, traceback.format_exc(e))) exc = PreconditionError(message, e) if hasattr(e, 'screenshot'): exc.screenshot = e.screenshot # pylint: disable=attribute-defined-outside-init,no-member raise exc
def _adb(self, command, timeout_secs=None, **kwargs): _command = [] if timeout_secs is not None: _command += ["timeout", "%fs" % timeout_secs] _command += [self.adb_binary] if self.adb_server: _command += ["-H", self.adb_server] if self.adb_device: _command += ["-s", self.adb_device] _command += command debug("AdbDevice.adb: About to run command: %r\n" % _command) output = subprocess.check_output( _command, stderr=subprocess.STDOUT, **kwargs) return output
def _mainloop(): mainloop = GLib.MainLoop.new(context=None, is_running=False) thread = threading.Thread(target=mainloop.run) thread.daemon = True thread.start() try: yield finally: mainloop.quit() thread.join(10) debug("teardown: Exiting (GLib mainloop %s)" % ("is still alive!" if thread.isAlive() else "ok"))
def _adb(self, command, timeout_secs=None, **kwargs): _command = [] if timeout_secs is not None: _command += ["timeout", "%fs" % timeout_secs] _command += [self.adb_binary] if self.adb_server: _command += ["-H", self.adb_server] if self.adb_device: _command += ["-s", self.adb_device] _command += command debug("AdbDevice.adb: About to run command: %r\n" % _command) output = subprocess.check_output( _command, stderr=subprocess.STDOUT, **kwargs).decode("utf-8") return output
def _mainloop(): mainloop = GLib.MainLoop.new(context=None, is_running=False) thread = threading.Thread(target=mainloop.run) thread.daemon = True thread.start() try: yield finally: mainloop.quit() thread.join(10) debug("teardown: Exiting (GLib mainloop %s)" % ( "is still alive!" if thread.isAlive() else "ok"))
def _push_sample(self, sample): # Calculate whether we need to draw any annotations on the output video. now = sample.time annotations = [] with self.annotations_lock: # Remove expired annotations self.text_annotations = [x for x in self.text_annotations if now < x.end_time] current_texts = [x for x in self.text_annotations if x.time <= now] for annotation in list(self.annotations): if annotation.time == now: annotations.append(annotation) if now >= annotation.time: self.annotations.remove(annotation) sample = gst_sample_make_writable(sample) img = array_from_sample(sample, readwrite=True) # Text: _draw_text( img, datetime.datetime.fromtimestamp(now).strftime("%H:%M:%S.%f")[:-4], (10, 30), (255, 255, 255)) for i, x in enumerate(reversed(current_texts)): origin = (10, (i + 2) * 30) age = float(now - x.time) / 3 color = (native(int(255 * max([1 - age, 0.5]))).__int__(),) * 3 _draw_text(img, x.text, origin, color) # Regions: for annotation in annotations: _draw_annotation(img, annotation) APPSRC_LIMIT_BYTES = 100 * 1024 * 1024 # 100MB if self.appsrc.props.current_level_bytes > APPSRC_LIMIT_BYTES: # appsrc is backed-up, perhaps something's gone wrong. We don't # want to use up all RAM, so let's drop the buffer on the floor. if not self._appsrc_was_full: warn("sink pipeline appsrc is full, dropping buffers from now " "on") self._appsrc_was_full = True return elif self._appsrc_was_full: debug("sink pipeline appsrc no longer full, pushing buffers again") self._appsrc_was_full = False self.appsrc.props.caps = sample.get_caps() self.appsrc.emit("push-buffer", sample.get_buffer()) self._sample_count += 1
def tap(self, position): """Tap on a particular location. :param position: A `stbt.Region`, or an (x,y) tuple. Example:: d.tap((100, 20)) d.tap(stbt.match(...).region) """ x, y = _centre_point(position) debug("AdbDevice.tap((%d,%d))" % (x, y)) x, y = self._to_native_coordinates(x, y) self.adb(["shell", "input", "tap", str(x), str(y)], timeout_secs=10)
def press(self, key): """Send a keypress. :param str key: An Android keycode as listed in <https://developer.android.com/reference/android/view/KeyEvent.html>. Particularly useful key codes are "KEYCODE_HOME" and "KEYCODE_BACK", which are physical buttons on some phones so you can't hit them with `AdbDevice.tap`. Also accepts standard Stb-tester key names like "KEY_HOME" and "KEY_BACK". """ # "adb shell input keyevent xxx" always returns success, so we need to # validate key names. if key in _KEYCODE_MAPPINGS: key = _KEYCODE_MAPPINGS[key] # Map Stb-tester names to Android ones if key not in _ANDROID_KEYCODES: raise ValueError("Unknown key code %r" % (key,)) debug("AdbDevice.press(%r)" % key) self.adb(["shell", "input", "keyevent", key], timeout_secs=10)
def frames(self, timeout_secs=None): if timeout_secs is not None: end_time = self._time.time() + timeout_secs timestamp = None first = True while True: ddebug("user thread: Getting sample at %s" % self._time.time()) frame = self._display.get_frame( max(10, timeout_secs), since=timestamp) ddebug("user thread: Got sample at %s" % self._time.time()) timestamp = frame.time if not first and timeout_secs is not None and timestamp > end_time: debug("timed out: %.3f > %.3f" % (timestamp, end_time)) return yield frame first = False
def is_screen_black(self, frame=None, mask=None, threshold=None, region=Region.ALL): if threshold is None: threshold = get_config('is_screen_black', 'threshold', type_=int) if frame is None: frame = self.get_frame() if mask is None: mask = _ImageFromUser(None, None, None) else: mask = _load_image(mask, cv2.IMREAD_GRAYSCALE) _region = Region.intersect(_image_region(frame), region) greyframe = cv2.cvtColor(crop(frame, _region), cv2.COLOR_BGR2GRAY) if mask.image is not None: cv2.bitwise_and(greyframe, mask.image, dst=greyframe) maxVal = greyframe.max() if logging.get_debug_level() > 1: imglog = logging.ImageLogger("is_screen_black") imglog.imwrite("source", frame) if mask.image is not None: imglog.imwrite('mask', mask.image) _, thresholded = cv2.threshold(greyframe, threshold, 255, cv2.THRESH_BINARY) imglog.imwrite('non-black-regions-after-masking', thresholded) result = _IsScreenBlackResult(bool(maxVal <= threshold), frame) debug("is_screen_black: {found} black screen using mask={mask}, " "threshold={threshold}, region={region}: " "{result}, maximum_intensity={maxVal}".format( found="Found" if result.black else "Didn't find", mask=mask.friendly_name, threshold=threshold, region=region, result=result, maxVal=maxVal)) return result
def __init__(self, user_source_pipeline, sink_pipeline, restart_source=False, source_teardown_eos=False): import time self._condition = threading.Condition() # Protects last_frame self.last_frame = None self.last_used_frame = None self.source_pipeline = None self.init_time = time.time() self.underrun_timeout = None self.tearing_down = False self.restart_source_enabled = restart_source self.source_teardown_eos = source_teardown_eos appsink = ( "appsink name=appsink max-buffers=1 drop=false sync=true " "emit-signals=true " "caps=video/x-raw,format=BGR") # Notes on the source pipeline: # * _stbt_raw_frames_queue is kept small to reduce the amount of slack # (and thus the latency) of the pipeline. # * _stbt_user_data_queue before the decodebin is large. We don't want # to drop encoded packets as this will cause significant image # artifacts in the decoded buffers. We make the assumption that we # have enough horse-power to decode the incoming stream and any delays # will be transient otherwise it could start filling up causing # increased latency. self.source_pipeline_description = " ! ".join([ user_source_pipeline, 'queue name=_stbt_user_data_queue max-size-buffers=0 ' ' max-size-bytes=0 max-size-time=10000000000', "decodebin", 'queue name=_stbt_raw_frames_queue max-size-buffers=2', 'videoconvert', 'video/x-raw,format=BGR', appsink]) self.create_source_pipeline() self._sink_pipeline = sink_pipeline debug("source pipeline: %s" % self.source_pipeline_description)
def main(argv): parser = argparse.ArgumentParser( epilog=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument( "--input", default="lircd", help="""The source of remote control presses. Values are the same as stbt record's --control-recorder.""") parser.add_argument("output", nargs="+", help="""One or more remote control configurations. Values are the same as stbt run's --control.""") argparser_add_verbose_argument(parser) args = parser.parse_args(argv[1:]) signal.signal(signal.SIGTERM, lambda _signo, _stack_frame: sys.exit(0)) r = MultiRemote(uri_to_remote(x) for x in args.output) listener = uri_to_remote_recorder(args.input) for key in listener: debug("Received %s" % key) try: r.press(key) except Exception as e: # pylint: disable=broad-except sys.stderr.write("Error pressing key %r: %s\n" % (key, e))
def swipe(self, start_position, end_position): """Swipe from one point to another point. :param start_position: A `stbt.Region` or (x, y) tuple of coordinates at which to start. :param end_position: A `stbt.Region` or (x, y) tuple of coordinates at which to stop. Example:: d.swipe((100, 100), (100, 400)) """ x1, y1 = _centre_point(start_position) x2, y2 = _centre_point(end_position) debug("AdbDevice.swipe((%d,%d), (%d,%d))" % (x1, y1, x2, y2)) x1, y1 = self._to_native_coordinates(x1, y1) x2, y2 = self._to_native_coordinates(x2, y2) command = ["shell", "input", "swipe", str(x1), str(y1), str(x2), str(y2)] self.adb(command, timeout_secs=10)
def main(argv): parser = argparse.ArgumentParser( epilog=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("--input", default="lircd", help="""The source of remote control presses. Values are the same as stbt record's --control-recorder.""") parser.add_argument("output", nargs="+", help="""One or more remote control configurations. Values are the same as stbt run's --control.""") argparser_add_verbose_argument(parser) args = parser.parse_args(argv[1:]) signal.signal(signal.SIGTERM, lambda _signo, _stack_frame: sys.exit(0)) r = MultiRemote(uri_to_remote(x) for x in args.output) listener = uri_to_remote_recorder(args.input) for key in listener: debug("Received %s" % key) try: r.press(key) except Exception as e: # pylint: disable=broad-except sys.stderr.write("Error pressing key %r: %s\n" % (key, e))
def wait_until(callable_, timeout_secs=10, interval_secs=0, predicate=None, stable_secs=0): """Wait until a condition becomes true, or until a timeout. Calls ``callable_`` repeatedly (with a delay of ``interval_secs`` seconds between successive calls) until it succeeds (that is, it returns a `truthy`_ value) or until ``timeout_secs`` seconds have passed. .. _truthy: https://docs.python.org/2/library/stdtypes.html#truth-value-testing :param callable_: any Python callable (such as a function or a lambda expression) with no arguments. :type timeout_secs: int or float, in seconds :param timeout_secs: After this timeout elapses, ``wait_until`` will return the last value that ``callable_`` returned, even if it's falsey. :type interval_secs: int or float, in seconds :param interval_secs: Delay between successive invocations of ``callable_``. :param predicate: A function that takes a single value. It will be given the return value from ``callable_``. The return value of *this* function will then be used to determine truthiness. If the predicate test succeeds, ``wait_until`` will still return the original value from ``callable_``, not the predicate value. :type stable_secs: int or float, in seconds :param stable_secs: Wait for ``callable_``'s return value to remain the same (as determined by ``==``) for this duration before returning. If ``predicate`` is also given, the values returned from ``predicate`` will be compared. :returns: The return value from ``callable_`` (which will be truthy if it succeeded, or falsey if ``wait_until`` timed out). If the value was truthy when the timeout was reached but it failed the ``predicate`` or ``stable_secs`` conditions (if any) then ``wait_until`` returns ``None``. After you send a remote-control signal to the device-under-test it usually takes a few frames to react, so a test script like this would probably fail:: press("KEY_EPG") assert match("guide.png") Instead, use this:: press("KEY_EPG") assert wait_until(lambda: match("guide.png")) Note that instead of the above ``assert wait_until(...)`` you could use ``wait_for_match("guide.png")``. ``wait_until`` is a generic solution that also works with stbt's other functions, like `match_text` and `is_screen_black`. ``wait_until`` allows composing more complex conditions, such as:: # Wait until something disappears: assert wait_until(lambda: not match("xyz.png")) # Assert that something doesn't appear within 10 seconds: assert not wait_until(lambda: match("xyz.png")) # Assert that two images are present at the same time: assert wait_until(lambda: match("a.png") and match("b.png")) # Wait but don't raise an exception: if not wait_until(lambda: match("xyz.png")): do_something_else() # Wait for a menu selection to change. Here ``Menu`` is a `FrameObject` # with a property called `selection` that returns a string with the # name of the currently-selected menu item: # The return value (``menu``) is an instance of ``Menu``. menu = wait_until(Menu, predicate=lambda x: x.selection == "Home") # Wait for a match to stabilise position, returning the first stable # match. Used in performance measurements, for example to wait for a # selection highlight to finish moving: press("KEY_DOWN") start_time = time.time() match_result = wait_until(lambda: stbt.match("selection.png"), predicate=lambda x: x and x.region, stable_secs=2) assert match_result end_time = match_result.time # this is the first stable frame print "Transition took %s seconds" % (end_time - start_time) Added in v28: The ``predicate`` and ``stable_secs`` parameters. """ import time if predicate is None: predicate = lambda x: x stable_value = None stable_predicate_value = None expiry_time = time.time() + timeout_secs while True: t = time.time() value = callable_() predicate_value = predicate(value) if stable_secs: if predicate_value != stable_predicate_value: stable_since = t stable_value = value stable_predicate_value = predicate_value if predicate_value and t - stable_since >= stable_secs: return stable_value else: if predicate_value: return value if t >= expiry_time: debug("wait_until timed out: %s" % _callable_description(callable_)) if not value: return value # it's falsey else: return None # must have failed stable_secs or predicate checks time.sleep(interval_secs)
def _on_eos_from_sink_pipeline(self, _bus, _message): debug("Got EOS from sink pipeline") self.received_eos.set()
def wait_until(callable_, timeout_secs=10, interval_secs=0, predicate=None, stable_secs=0): """Wait until a condition becomes true, or until a timeout. Calls ``callable_`` repeatedly (with a delay of ``interval_secs`` seconds between successive calls) until it succeeds (that is, it returns a `truthy`_ value) or until ``timeout_secs`` seconds have passed. .. _truthy: https://docs.python.org/2/library/stdtypes.html#truth-value-testing :param callable_: any Python callable (such as a function or a lambda expression) with no arguments. :type timeout_secs: int or float, in seconds :param timeout_secs: After this timeout elapses, ``wait_until`` will return the last value that ``callable_`` returned, even if it's falsey. :type interval_secs: int or float, in seconds :param interval_secs: Delay between successive invocations of ``callable_``. :param predicate: A function that takes a single value. It will be given the return value from ``callable_``. The return value of *this* function will then be used to determine truthiness. If the predicate test succeeds, ``wait_until`` will still return the original value from ``callable_``, not the predicate value. :type stable_secs: int or float, in seconds :param stable_secs: Wait for ``callable_``'s return value to remain the same (as determined by ``==``) for this duration before returning. If ``predicate`` is also given, the values returned from ``predicate`` will be compared. :returns: The return value from ``callable_`` (which will be truthy if it succeeded, or falsey if ``wait_until`` timed out). If the value was truthy when the timeout was reached but it failed the ``predicate`` or ``stable_secs`` conditions (if any) then ``wait_until`` returns ``None``. After you send a remote-control signal to the device-under-test it usually takes a few frames to react, so a test script like this would probably fail:: press("KEY_EPG") assert match("guide.png") Instead, use this:: press("KEY_EPG") assert wait_until(lambda: match("guide.png")) Note that instead of the above ``assert wait_until(...)`` you could use ``wait_for_match("guide.png")``. ``wait_until`` is a generic solution that also works with stbt's other functions, like `match_text` and `is_screen_black`. ``wait_until`` allows composing more complex conditions, such as:: # Wait until something disappears: assert wait_until(lambda: not match("xyz.png")) # Assert that something doesn't appear within 10 seconds: assert not wait_until(lambda: match("xyz.png")) # Assert that two images are present at the same time: assert wait_until(lambda: match("a.png") and match("b.png")) # Wait but don't raise an exception: if not wait_until(lambda: match("xyz.png")): do_something_else() # Wait for a menu selection to change. Here ``Menu`` is a `FrameObject` # with a property called `selection` that returns a string with the # name of the currently-selected menu item: # The return value (``menu``) is an instance of ``Menu``. menu = wait_until(Menu, predicate=lambda x: x.selection == "Home") # Wait for a match to stabilise position, returning the first stable # match. Used in performance measurements, for example to wait for a # selection highlight to finish moving: press("KEY_DOWN") start_time = time.time() match_result = wait_until(lambda: stbt.match("selection.png"), predicate=lambda x: x and x.region, stable_secs=2) assert match_result end_time = match_result.time # this is the first stable frame print "Transition took %s seconds" % (end_time - start_time) Added in v28: The ``predicate`` and ``stable_secs`` parameters. """ import time if predicate is None: predicate = lambda x: x stable_value = None stable_predicate_value = None expiry_time = time.time() + timeout_secs while True: t = time.time() value = callable_() predicate_value = predicate(value) if stable_secs: if predicate_value != stable_predicate_value: stable_since = t stable_value = value stable_predicate_value = predicate_value if predicate_value and t - stable_since >= stable_secs: debug("wait_until succeeded: %s" % _callable_description(callable_)) return stable_value else: if predicate_value: debug("wait_until succeeded: %s" % _callable_description(callable_)) return value if t >= expiry_time: debug("wait_until timed out after %s seconds: %s" % (timeout_secs, _callable_description(callable_))) if not value: return value # it's falsey else: return None # must have failed stable_secs or predicate checks time.sleep(interval_secs)