aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJordan Bayles <jophba@chromium.org>2021-07-29 12:56:45 -0700
committerOpenscreen LUCI CQ <openscreen-scoped@luci-project-accounts.iam.gserviceaccount.com>2021-07-29 21:06:01 +0000
commit0a7aefe8037a448db69f9314b360e3e10f4cab74 (patch)
treef56bc1b1847fbf5c4e6d5e3632a06365e6e84929
parentd816f4dcef882eaa8d1e4a6de5f1ea2d8fdd7d8a (diff)
downloadopenscreen-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>
-rwxr-xr-xcast/standalone_e2e.py302
-rw-r--r--cast/standalone_receiver/streaming_playback_controller.cc2
-rw-r--r--cast/streaming/remoting_capabilities.h3
-rw-r--r--cast/streaming/sender_session.cc3
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;