diff options
author | Jordan Bayles <jophba@chromium.org> | 2021-07-29 12:56:45 -0700 |
---|---|---|
committer | Openscreen LUCI CQ <openscreen-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2021-07-29 21:06:01 +0000 |
commit | 0a7aefe8037a448db69f9314b360e3e10f4cab74 (patch) | |
tree | f56bc1b1847fbf5c4e6d5e3632a06365e6e84929 /cast | |
parent | d816f4dcef882eaa8d1e4a6de5f1ea2d8fdd7d8a (diff) | |
download | openscreen-0a7aefe8037a448db69f9314b360e3e10f4cab74.tar.gz |
[Cast Streaming] Add standalone python test script
This patch adds standalone_e2e.py, a simple-ish script that exercises
the standalone sender and receiver executables.
Change-Id: Ibf8c8580ebf6759f03fa17c41e9034bb91f93513
Reviewed-on: https://chromium-review.googlesource.com/c/openscreen/+/3001104
Commit-Queue: Jordan Bayles <jophba@chromium.org>
Reviewed-by: mark a. foltz <mfoltz@chromium.org>
Diffstat (limited to 'cast')
-rwxr-xr-x | cast/standalone_e2e.py | 302 | ||||
-rw-r--r-- | cast/standalone_receiver/streaming_playback_controller.cc | 2 | ||||
-rw-r--r-- | cast/streaming/remoting_capabilities.h | 3 | ||||
-rw-r--r-- | cast/streaming/sender_session.cc | 3 |
4 files changed, 309 insertions, 1 deletions
diff --git a/cast/standalone_e2e.py b/cast/standalone_e2e.py new file mode 100755 index 00000000..b27e3c65 --- /dev/null +++ b/cast/standalone_e2e.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python3 +# Copyright 2021 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +""" +This script is intended to cover end to end testing for the standalone sender +and receiver executables in cast. This ensures that the basic functionality of +these executables is not impaired, such as the TLS/UDP connections and encoding +and decoding video. +""" + +import argparse +import os +import pathlib +import logging +import subprocess +import sys +import time +import unittest +import ssl +from collections import namedtuple + +from enum import IntFlag +from urllib import request + +# Environment variables that can be overridden to set test properties. +ROOT_ENVVAR = 'OPENSCREEN_ROOT_DIR' +BUILD_ENVVAR = 'OPENSCREEN_BUILD_DIR' + +TEST_VIDEO_NAME = 'Contador_Glam.mp4' +# NOTE: we use the HTTP protocol instead of HTTPS due to certificate issues +# in the legacy urllib.request API. +TEST_VIDEO_URL = ('https://storage.googleapis.com/openscreen_standalone/' + + TEST_VIDEO_NAME) + +PROCESS_TIMEOUT = 15 # seconds + +# Open Screen test certificates expire after 3 days. We crop this slightly (by +# 8 hours) to account for potential errors in time calculations. +CERT_EXPIRY_AGE = (3 * 24 - 8) * 60 * 60 + +# These properties are based on compiled settings in Open Screen, and should +# not change without updating this file. +TEST_CERT_NAME = 'generated_root_cast_receiver.crt' +TEST_KEY_NAME = 'generated_root_cast_receiver.key' +SENDER_BINARY_NAME = 'cast_sender' +RECEIVER_BINARY_NAME = 'cast_receiver' + +EXPECTED_RECEIVER_MESSAGES = [ + "CastService is running.", "Found codec: opus (known to FFMPEG as opus)", + "Found codec: vp8 (known to FFMPEG as vp8)", + "Successfully negotiated a session, creating SDL players.", + "Receivers are currently destroying, resetting SDL players." +] + +EXPECTED_SENDER_MESSAGES = [ + "Launching Mirroring App on the Cast Receiver", + "Max allowed media bitrate (audio + video) will be", + "Contador_Glam.mp4 (starts in one second)...", + "The video capturer has reached the end of the media stream.", + "The audio capturer has reached the end of the media stream.", + "Video complete. Exiting...", "Shutting down..." +] + +MISSING_LOG_MESSAGE = """Missing an expected message from either the sender +or receiver. This either means that one of the binaries misbehaved, or you +changed or deleted one of the log messages used for validation. Please ensure +that the necessary log messages are left unchanged, or update this +test suite's expectations.""" + +DESCRIPTION = """Runs end to end tests for the standalone Cast Streaming sender +and receiver. By default, this script assumes it is being ran from a current +working directory inside Open Screen's source directory, and uses +<root_dir>/out/Default as the build directory. To override these, set the +OPENSCREEN_ROOT_DIR and OPENSCREEN_BUILD_DIR environment variables. If the root +directory is set and the build directory is not, +<OPENSCREEN_ROOT_DIR>/out/Default will be used. + +See below for the the help output generated by the `unittest` package.""" + + +def _set_log_level(is_verbose): + """Sets the logging level, either DEBUG or ERROR as appropriate.""" + level = logging.DEBUG if is_verbose else logging.INFO + logging.basicConfig(stream=sys.stdout, level=level) + + +def _get_loopback_adapter_name(): + """Retrieves the name of the loopback adapter (lo on Linux/lo0 on Mac).""" + if sys.platform == 'linux' or sys.platform == 'linux2': + return 'lo' + if sys.platform == 'darwin': + return 'lo0' + return None + + +def _get_file_age_in_seconds(path): + """Get the age of a given file in seconds""" + # Time is stored in seconds since epoch + file_last_modified = 0 + if path.exists(): + file_last_modified = path.stat().st_mtime + return time.time() - file_last_modified + + +def _get_build_paths(): + """Gets the root and build paths (either default or from the environment + variables), and sets related paths to binaries and files.""" + root_path = pathlib.Path( + os.environ[ROOT_ENVVAR] if os.getenv(ROOT_ENVVAR) else subprocess. + getoutput('git rev-parse --show-toplevel')) + assert root_path.exists(), 'Could not find openscreen root!' + + build_path = pathlib.Path(os.environ[BUILD_ENVVAR]) if os.getenv( + BUILD_ENVVAR) else root_path.joinpath('out', + 'Default').resolve() + assert build_path.exists(), 'Could not find openscreen build!' + + BuildPaths = namedtuple("BuildPaths", + "root build test_video cast_receiver cast_sender") + return BuildPaths(root = root_path, + build = build_path, + test_video = build_path.joinpath(TEST_VIDEO_NAME).resolve(), + cast_receiver = build_path.joinpath(RECEIVER_BINARY_NAME).resolve(), + cast_sender = build_path.joinpath(SENDER_BINARY_NAME).resolve() + ) + + +class TestFlags(IntFlag): + """ + Test flags, primarily used to control sender and receiver configuration + to test different features of the standalone libraries. + """ + UseRemoting = 1 + UseAndroidHack = 2 + + +class StandaloneCastTest(unittest.TestCase): + """ + Test class for setting up and running end to end tests on the + standalone sender and receiver binaries. This class uses the unittest + package, so methods that are executed as tests all have named prefixed + with "test_". + + This suite sets the current working directory to the root of the Open + Screen repository, and references all files from the root directory. + Generated certificates should always be in |cls.build_paths.root|. + """ + + @classmethod + def setUpClass(cls): + """Shared setup method for all tests, handles one-time updates.""" + cls.build_paths = _get_build_paths() + os.chdir(cls.build_paths.root) + cls.download_video() + cls.generate_certificates() + + @classmethod + def download_video(cls): + """Downloads the test video from Google storage.""" + if os.path.exists(cls.build_paths.test_video): + logging.debug('Video already exists, skipping download...') + return + + logging.debug('Downloading video from %s', TEST_VIDEO_URL) + with request.urlopen(TEST_VIDEO_URL, context=ssl.SSLContext()) as url: + with open(cls.build_paths.test_video, 'wb') as file: + file.write(url.read()) + + @classmethod + def generate_certificates(cls): + """Generates test certificates using the cast receiver.""" + cert_age = _get_file_age_in_seconds(pathlib.Path(TEST_CERT_NAME)) + key_age = _get_file_age_in_seconds(pathlib.Path(TEST_KEY_NAME)) + if cert_age < CERT_EXPIRY_AGE and key_age < CERT_EXPIRY_AGE: + logging.debug('Credentials are up to date...') + return + + logging.debug('Credentials out of date, generating new ones...') + subprocess.check_output( + [ + cls.build_paths.cast_receiver, + '-g', # Generate certificate and private key. + '-v' # Enable verbose logging. + ], + stderr=subprocess.STDOUT) + + def launch_receiver(self): + """Launches the receiver process with discovery disabled.""" + logging.debug('Launching the receiver application...') + loopback = _get_loopback_adapter_name() + self.assertTrue(loopback) + + #pylint: disable = consider-using-with + return subprocess.Popen( + [ + self.build_paths.cast_receiver, + '-d', + TEST_CERT_NAME, + '-p', + TEST_KEY_NAME, + '-x', # Skip discovery, only necessary on Mac OS X. + '-v', # Enable verbose logging. + loopback + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + def launch_sender(self, flags): + """Launches the sender process, running the test video file once.""" + logging.debug('Launching the sender application...') + command = [ + self.build_paths.cast_sender, + '127.0.0.1:8010', + self.build_paths.test_video, + '-d', + TEST_CERT_NAME, + '-n' # Only play the video once, and then exit. + ] + if TestFlags.UseAndroidHack in flags: + command.append('-a') + if TestFlags.UseRemoting in flags: + command.append('-r') + + #pylint: disable = consider-using-with + return subprocess.Popen(command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + def check_logs(self, logs): + """Checks that the outputted logs contain expected behavior.""" + for message in EXPECTED_RECEIVER_MESSAGES: + self.assertTrue( + message in logs[0], + 'Missing log message: {}.\n{}'.format(message, + MISSING_LOG_MESSAGE)) + for message in EXPECTED_SENDER_MESSAGES: + self.assertTrue( + message in logs[1], + 'Missing log message: {}.\n{}'.format(message, + MISSING_LOG_MESSAGE)) + for log, prefix in logs, ["[ERROR:", "[FATAL:"]: + self.assertTrue(prefix not in log, "Logs contained an error") + logging.debug('Finished validating log output') + + def get_output(self, flags): + """Launches the sender and receiver, and handles exit output.""" + receiver_process = self.launch_receiver() + logging.debug('Letting the receiver start up...') + time.sleep(3) + sender_process = self.launch_sender(flags) + + logging.debug('Launched sender PID %i and receiver PID %i...', + sender_process.pid, receiver_process.pid) + logging.debug('collating output...') + output = (receiver_process.communicate( + timeout=PROCESS_TIMEOUT)[1].decode('utf-8'), + sender_process.communicate( + timeout=PROCESS_TIMEOUT)[1].decode('utf-8')) + + # TODO(issuetracker.google.com/194292855): standalones should exit zero. + # Remoting causes the sender to exit with code -4. + if not TestFlags.UseRemoting in flags: + self.assertEqual(sender_process.returncode, 0, + 'sender had non-zero exit code') + return output + + def test_golden_case(self): + """Tests that when settings are normal, things work end to end.""" + output = self.get_output([]) + self.check_logs(output) + + def test_remoting(self): + """Tests that basic remoting works.""" + output = self.get_output(TestFlags.UseRemoting) + self.check_logs(output) + + def test_with_android_hack(self): + """Tests that things work when the Android RTP hack is enabled.""" + output = self.get_output(TestFlags.UseAndroidHack) + self.check_logs(output) + + +def parse_args(): + """Parses the command line arguments and sets up the logging module.""" + # NOTE for future developers: the `unittest` module will complain if it is + # passed any args that it doesn't understand. If any Open Screen-specific + # command line arguments are added in the future, they should be cropped + # from sys.argv before |unittest.main()| is called. + parser = argparse.ArgumentParser(description=DESCRIPTION) + parser.add_argument('-v', + '--verbose', + help='enable debug logging', + action='store_true') + + parsed_args = parser.parse_args(sys.argv[1:]) + _set_log_level(parsed_args.verbose) + + +if __name__ == '__main__': + parse_args() + unittest.main() diff --git a/cast/standalone_receiver/streaming_playback_controller.cc b/cast/standalone_receiver/streaming_playback_controller.cc index 4a81c7ed..5f6412a4 100644 --- a/cast/standalone_receiver/streaming_playback_controller.cc +++ b/cast/standalone_receiver/streaming_playback_controller.cc @@ -89,6 +89,7 @@ void StreamingPlaybackController::OnRemotingNegotiated( void StreamingPlaybackController::OnReceiversDestroying( const ReceiverSession* session, ReceiversDestroyingReason reason) { + OSP_LOG_INFO << "Receivers are currently destroying, resetting SDL players."; audio_player_.reset(); video_player_.reset(); } @@ -101,6 +102,7 @@ void StreamingPlaybackController::OnError(const ReceiverSession* session, void StreamingPlaybackController::Initialize( ReceiverSession::ConfiguredReceivers receivers) { #if defined(CAST_STANDALONE_RECEIVER_HAVE_EXTERNAL_LIBS) + OSP_LOG_INFO << "Successfully negotiated a session, creating SDL players."; if (receivers.audio_receiver) { audio_player_ = std::make_unique<SDLAudioPlayer>( &Clock::now, task_runner_, receivers.audio_receiver, diff --git a/cast/streaming/remoting_capabilities.h b/cast/streaming/remoting_capabilities.h index 6957028f..a2f7b173 100644 --- a/cast/streaming/remoting_capabilities.h +++ b/cast/streaming/remoting_capabilities.h @@ -37,7 +37,8 @@ enum class VideoCapability { kH264, kVp8, kVp9, - kHevc + kHevc, + kAv1 }; // This class is similar to the RemotingSinkMetadata in Chrome, however diff --git a/cast/streaming/sender_session.cc b/cast/streaming/sender_session.cc index cae6f3e4..c47e9667 100644 --- a/cast/streaming/sender_session.cc +++ b/cast/streaming/sender_session.cc @@ -186,6 +186,9 @@ RemotingCapabilities ToCapabilities(const ReceiverCapability& capability) { case MediaCapability::kHevc: out.video.push_back(VideoCapability::kHevc); break; + case MediaCapability::kAv1: + out.video.push_back(VideoCapability::kAv1); + break; case MediaCapability::kVideo: // noop, as "video" is ignored by Chrome remoting. break; |