From 9cab445e2cce0a7850574661cacc1c8dbbc8cd1c Mon Sep 17 00:00:00 2001 From: Fred Gylys-Colwell Date: Wed, 13 Oct 2021 21:45:04 +0000 Subject: [PATCH] Add initial reboot test infrastructure Merge from Widevine repo of http://go/wvgerrit/130469 Parse and decode persistent data for reboot tests Merge from Widevine repo of http://go/wvgerrit/130468 Save and restore persistent test data Merge from Widevine repo of http://go/wvgerrit/130467 Saving and restore the test host's file system Merge from Widevine repo of http://go/wvgerrit/130466 Add reboot test class Test: android/run_reboot_test.sh and jenkins/run_fake_l1_tests Bug: 194342751 Bug: 194342800 Change-Id: Id2f3d9850cb75cb286f7863738aa8fd38a1a5301 --- libwvdrmengine/build_all_unit_tests.sh | 1 + .../cdm/core/test/config_test_env.h | 8 + libwvdrmengine/cdm/core/test/reboot_test.cpp | 274 ++++++++++++++++++ libwvdrmengine/cdm/core/test/reboot_test.h | 54 ++++ libwvdrmengine/cdm/core/test/test_base.cpp | 14 + libwvdrmengine/cdm/test/Android.mk | 5 + libwvdrmengine/run_reboot_test.sh | 95 ++++++ 7 files changed, 451 insertions(+) create mode 100644 libwvdrmengine/cdm/core/test/reboot_test.cpp create mode 100644 libwvdrmengine/cdm/core/test/reboot_test.h create mode 100755 libwvdrmengine/run_reboot_test.sh diff --git a/libwvdrmengine/build_all_unit_tests.sh b/libwvdrmengine/build_all_unit_tests.sh index 2892ca04..0ff0a34e 100755 --- a/libwvdrmengine/build_all_unit_tests.sh +++ b/libwvdrmengine/build_all_unit_tests.sh @@ -65,6 +65,7 @@ WV_TEST_TARGETS="base64_test \ policy_engine_constraints_unittest \ policy_engine_unittest \ policy_integration_test \ + reboot_test \ request_license_test \ rw_lock_test \ service_certificate_unittest \ diff --git a/libwvdrmengine/cdm/core/test/config_test_env.h b/libwvdrmengine/cdm/core/test/config_test_env.h index 50900bf3..1bd6e0de 100644 --- a/libwvdrmengine/cdm/core/test/config_test_env.h +++ b/libwvdrmengine/cdm/core/test/config_test_env.h @@ -90,6 +90,8 @@ class ConfigTestEnv { const std::string& provisioning_service_certificate() const { return provisioning_service_certificate_; } + int test_pass() const { return test_pass_; } + const std::string& test_data_path() const { return test_data_path_; } static const CdmInitData GetInitData(ContentId content_id); static const std::string& GetLicenseServerUrl( @@ -116,6 +118,10 @@ class ConfigTestEnv { const std::string& provisioning_service_certificate) { provisioning_service_certificate_.assign(provisioning_service_certificate); } + void set_test_pass(int test_pass) { test_pass_ = test_pass; } + void set_test_data_path(const std::string& test_data_path) { + test_data_path_ = test_data_path; + } // The QA service certificate, used for a local provisioning server. static std::string QAProvisioningServiceCertificate(); @@ -131,6 +137,8 @@ class ConfigTestEnv { std::string provisioning_server_; std::string license_service_certificate_; std::string provisioning_service_certificate_; + int test_pass_; + std::string test_data_path_; // Where to store test data for reboot tests. }; // The default provisioning server URL for a default provisioning request. diff --git a/libwvdrmengine/cdm/core/test/reboot_test.cpp b/libwvdrmengine/cdm/core/test/reboot_test.cpp new file mode 100644 index 00000000..c32e00bb --- /dev/null +++ b/libwvdrmengine/cdm/core/test/reboot_test.cpp @@ -0,0 +1,274 @@ +// Copyright 2021 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine License +// Agreement. + +// These tests perform various end-to-end actions similar to what an application +// would do. They verify that policies specified on UAT are honored on the +// device. + +#include "reboot_test.h" + +#include + +#include "log.h" + +namespace wvcdm { +FileSystem* RebootTest::file_system_; + +namespace { +// We will encode a value string by wrapping it in braces, or as hex. +// If the string is not printable, or if it has unmatched braces, then we use +// hex. Otherwise, we surround the whole string with braces. +std::string EncodeString(const std::string& data) { + int braces_count = 0; + for (size_t i = 0; i < data.length(); i++) { + if (data[i] == '{') braces_count++; + if (data[i] == '}') braces_count--; + // If printable or whitespace (because '\n' is not printable?!?). + bool printable = isprint(data[i]) || isspace(data[i]); + // If there are any unprintable characters, except whitespace, or if we + // close a brace before we open it, then just use hex. + if (!printable || braces_count < 0) { + return "0x" + wvcdm::b2a_hex(data) + ","; + } + } + // If we left any braces open, then use hex. + if (braces_count != 0) return "0x" + wvcdm::b2a_hex(data) + ","; + return "{" + data + "},"; +} + +// Encode a map key for dumping. When we encode a map, we expect the keys to be +// like filenames, so we can separate them with colons and whitespace. If the +// key has these special characters, we will encode as hex. +std::string EncodeKey(const std::string& data) { + if (data.length() == 0) { + LOGE("Encoding empty string as key!"); + return "EMPTY:"; + } + // When decoding, we assume that a key starting with "0x" is in hex. So we + // can't have any keys that start with "0x". + if (data.substr(0, 2) == "0x") return "0x" + wvcdm::b2a_hex(data) + ":"; + // If the key is just is not printable, or if it has unmatched braces, then + // we use hex. Otherwise, we surround the whole string with braces. + for (size_t i = 0; i < data.length(); i++) { + if (!isprint(data[i]) || (data[i] == ':')) { + return "0x" + wvcdm::b2a_hex(data) + ":"; + } + } + return data + ":"; +} + +// In between keys and values, we will ignore whitespace. This allows a human to +// edit the persistent data a little bit without breaking anything. +void SkipSpace(const std::string& encoded, size_t* index) { + if (!index) return; + while (*index < encoded.length() && isspace(encoded[*index])) (*index)++; +} + +// Decode a string that was encoded using EncodeString. +std::string DecodeString(const std::string& encoded, size_t* index) { + if (!index) return ""; + SkipSpace(encoded, index); + if (*index + 2 >= + encoded.length()) { // Encoded string has at least 3 characters. + LOGE("Error decoding short string from %s at %zd", encoded.c_str(), *index); + *index = encoded.length(); + return ""; + } + if (encoded[*index] == '{') { + (*index)++; + size_t start = *index; + int braces_count = 1; + while (*index < encoded.length()) { + if (encoded[*index] == '{') braces_count++; + if (encoded[*index] == '}') braces_count--; + if (braces_count == 0) { + size_t end = *index; + (*index) += 2; // absorb the comma and the '}', too. + return encoded.substr(start, end - start); + } + (*index)++; + } + std::string tail = encoded.substr(start); + LOGE("Non-terminated brace %s at %zd: %s", encoded.c_str(), start, + tail.c_str()); + *index = encoded.length(); + return ""; + } + if (encoded[*index] != '0' || encoded[*index + 1] != 'x') { + std::string tail = encoded.substr(*index); + LOGE("Hex should start with 0x in %s at %zd: %s", encoded.c_str(), *index, + tail.c_str()); + *index = encoded.length(); + return ""; + } + *index += 2; + size_t start = *index; + while (*index < encoded.length()) { + if (encoded[*index] == ',') { + size_t end = *index; + std::vector result = + wvcdm::a2b_hex(encoded.substr(start, end - start)); + (*index)++; // absorb the comma. + return std::string(result.begin(), result.end()); + } + (*index)++; + } + std::string tail = encoded.substr(start); + LOGE("Bad encoding in %s at %zd: %s", encoded.c_str(), start, tail.c_str()); + *index = encoded.length(); + return ""; +} + +// Decode a string that was encoded with EncodeKey. +std::string DecodeKey(const std::string& encoded, size_t* index) { + if (!index) return ""; + SkipSpace(encoded, index); + if (*index + 1 >= encoded.length()) { + LOGE("Error decoding key from %s at %zd", encoded.c_str(), *index); + *index = encoded.length(); + return ""; + } + // If it starts with 0x, then it is in hex. + if (encoded[*index] == '0' && encoded[*index + 1] == 'x') { + size_t start = *index + 2; + while (*index < encoded.length() && encoded[*index] != ':') (*index)++; + size_t end = *index; + std::vector result = + wvcdm::a2b_hex(encoded.substr(start, end - start)); + (*index)++; // skip the colon. + return std::string(result.begin(), result.end()); + } + size_t start = *index; + while (*index < encoded.length() && encoded[*index] != ':') (*index)++; + size_t end = *index; + (*index)++; // skip the colon. + return encoded.substr(start, end - start); +} +} // namespace + +std::string RebootTest::DumpData( + const std::map& data) { + std::ostringstream output; + output << "{\n"; + for (const auto& entry : data) { + output << " " << EncodeKey(entry.first) << " " + << EncodeString(entry.second) + "\n"; + } + output << "}\n"; + return output.str(); +} + +bool RebootTest::ParseDump(const std::string& dump, + std::map* data) { + size_t index = 0; + SkipSpace(dump, &index); + if (index >= dump.length()) return false; + if (dump[index] != '{') { + LOGE("Dump does not start with '{'"); + return false; + } + index++; // absorb '{' + while (true) { + SkipSpace(dump, &index); + if (index >= dump.length()) return false; + if (dump[index] == '}') { + index++; // absorb '}' + SkipSpace(dump, &index); + if (index != dump.length()) { + std::string tail = dump.substr(index); + LOGE("Trailing data in dump. %s at %zd: %s", dump.c_str(), index, + tail.c_str()); + return false; + } + return true; + } + std::string tail = dump.substr(index); + std::string key = DecodeKey(dump, &index); + std::string value = DecodeString(dump, &index); + (*data)[key] = value; + } +} + +void RebootTest::SetUp() { + WvCdmTestBase::SetUp(); + if (!file_system_) file_system_ = new FileSystem(); + + const ::testing::TestInfo* const test_info = + ::testing::UnitTest::GetInstance()->current_test_info(); + std::string test_name = + std::string(test_info->test_case_name()) + "-" + test_info->name(); + persistent_data_filename_ = + config_.test_data_path() + "/" + test_name + ".dat"; + LOGD("Running test pass %d for %s", test_pass(), test_name.c_str()); + // Don't read data on the first pass, but do read data for all other passes. + if (test_pass() > 0) { + EXPECT_TRUE(file_system_->Exists(persistent_data_filename_)); + ssize_t file_size = file_system_->FileSize(persistent_data_filename_); + auto file = + file_system_->Open(persistent_data_filename_, file_system_->kReadOnly); + ASSERT_TRUE(file); + std::string dump(file_size, ' '); + ssize_t read = file->Read(&dump[0], dump.size()); + EXPECT_EQ(read, file_size) << "Error reading persistent data file."; + EXPECT_TRUE(ParseDump(dump, &persistent_data_)); + } +} + +void RebootTest::TearDown() { + auto file = file_system_->Open(persistent_data_filename_, + FileSystem::kCreate | FileSystem::kTruncate); + ASSERT_TRUE(file) << "Failed to open file: " << persistent_data_filename_; + std::string dump = DumpData(persistent_data_); + const ssize_t bytes_written = file->Write(dump.data(), dump.length()); + EXPECT_EQ(bytes_written, static_cast(dump.length())); + WvCdmTestBase::TearDown(); +} + +/** Test the dump and restore functions above. This does not test CDM + functionality. */ +TEST_F(RebootTest, TestDumpUtil) { + // Check that an empty map can be saved. + std::map map1; + const std::string dump = DumpData(map1); + std::map map2; + EXPECT_TRUE(ParseDump(dump, &map2)); + EXPECT_EQ(map1, map2); + // Now fill it with some data and try again. + map1["key1"] = "this is a string. "; + map1["key2"] = "mismatch } {"; + map1["key3"] = "mismatch } "; + map1["key4"] = "mismatch {"; + map1["key5"] = "this: { has { matched } } braces { /.,)(**&^$&^% }"; + map1["key6"] = ""; + map1["00 whitespace in key 00"] = "value is ok"; + // This key looks like it might be hex. It should show up as hex in the + // save file. + map1["0x_bad_key_00"] = "value is ok"; + const std::string dump2 = DumpData(map1); + std::map map3; + EXPECT_TRUE(ParseDump(dump2, &map3)); + EXPECT_EQ(map1, map3); + if (test_pass() == 0) { + persistent_data_ = map1; + } else { + EXPECT_EQ(persistent_data_, map1); + } +} + +/** Verify that the file system stores files from one test pass to the next. */ +TEST_F(RebootTest, FilesArePersistent) { + const std::string key = "saved_value"; + const std::string value = "the string that is saved"; + if (test_pass() == 0) { + // There should be no persistent data on the first pass. + EXPECT_NE(persistent_data_[key], value); + // We will save some data for the next pass. + persistent_data_[key] = value; + } else { + // There should be persistent data set in first pass. + EXPECT_EQ(persistent_data_[key], value); + } +} + +} // namespace wvcdm diff --git a/libwvdrmengine/cdm/core/test/reboot_test.h b/libwvdrmengine/cdm/core/test/reboot_test.h new file mode 100644 index 00000000..605d3703 --- /dev/null +++ b/libwvdrmengine/cdm/core/test/reboot_test.h @@ -0,0 +1,54 @@ +// Copyright 2021 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine License +// Agreement. + +#ifndef WVCDM_CORE_REBOOT_TEST_H_ +#define WVCDM_CORE_REBOOT_TEST_H_ + +#include +#include +#include + +#include + +#include "file_store.h" +#include "test_base.h" + +namespace wvcdm { +class RebootTest : public WvCdmTestBaseWithEngine { + public: + // The main test driver may inject the file system for saving persistent test + // data. + static void set_file_system(FileSystem* file_system) { + file_system_ = file_system; + } + // Dump a map to a std string in an almost human readable way so that the map + // can be rebuilt using ParseDump below. The keys in the map must be standard + // identifier strings, which means no special characters or whitespace. By + // "almost human readable", we mean that a human debugging the dump will be + // able to find the keys, and see the values if they are printable or see a + // hex dump of the values if they are not. + static std::string DumpData(const std::map& data); + // Parse a dump generated by DumpData and recreate the original data map. + // Returns true on success. + static bool ParseDump(const std::string& dump, + std::map* data); + + static int test_pass() { return default_config_.test_pass(); } + + protected: + void SetUp() override; + void TearDown() override; + + // This is used to store each test's persistent data. + static FileSystem* file_system_; + + // The persistent data for the current test. + std::map persistent_data_; + // Where to store and restore the persistent data for a single test. + std::string persistent_data_filename_; +}; + +} // namespace wvcdm + +#endif // WVCDM_CORE_REBOOT_TEST_H_ diff --git a/libwvdrmengine/cdm/core/test/test_base.cpp b/libwvdrmengine/cdm/core/test/test_base.cpp index 9ba2ff02..430778ce 100644 --- a/libwvdrmengine/cdm/core/test/test_base.cpp +++ b/libwvdrmengine/cdm/core/test/test_base.cpp @@ -122,6 +122,14 @@ void show_menu(const char* prog_name, const std::string& extra_help_text) { << " be used with a real OEMCrypto." << std::endl << std::endl; + std::cout << " --pass=" << std::endl; + std::cout << " Run test pass N. This is used for reboot tests that " + << "require several passes." << std::endl + << std::endl; + + std::cout << " --test_data_path=" << std::endl; + std::cout << " Where to store test data for reboot tests." << std::endl; + std::cout << extra_help_text << std::endl; } @@ -509,6 +517,12 @@ bool WvCdmTestBase::Initialize(int argc, const char* const argv[], default_config_.set_renewal_server(arg_value); } else if (arg_prefix == "--provisioning_server_url") { default_config_.set_provisioning_server(arg_value); + } else if (arg_prefix == "--pass") { + default_config_.set_test_pass(std::stoi(arg_value)); + std::cout << "Running test pass " << default_config_.test_pass() + << std::endl; + } else if (arg_prefix == "--test_data_path") { + default_config_.set_test_data_path(arg_value); } else { std::cerr << "Unknown argument " << arg_prefix << std::endl; show_usage = true; diff --git a/libwvdrmengine/cdm/test/Android.mk b/libwvdrmengine/cdm/test/Android.mk index 20d4c522..7c2e0a24 100644 --- a/libwvdrmengine/cdm/test/Android.mk +++ b/libwvdrmengine/cdm/test/Android.mk @@ -145,6 +145,11 @@ test_src_dir := . test_main := ../core/test/test_main.cpp include $(LOCAL_PATH)/integration-test.mk +test_name := reboot_test +test_src_dir := ../core/test +test_main := ../core/test/test_main.cpp +include $(LOCAL_PATH)/integration-test.mk + test_name := rw_lock_test test_src_dir := ../core/test include $(LOCAL_PATH)/integration-test.mk diff --git a/libwvdrmengine/run_reboot_test.sh b/libwvdrmengine/run_reboot_test.sh new file mode 100755 index 00000000..1736723c --- /dev/null +++ b/libwvdrmengine/run_reboot_test.sh @@ -0,0 +1,95 @@ +#!/bin/sh + +# Read arguments in case the user wants to copy files to a specific +# android device by providing a serial number +SERIAL_NUM="" +while getopts "j:s:" opt; do + case $opt in + s) + SERIAL_NUM="-s $OPTARG" + ;; + esac +done + +final_result=0 +failed_tests=() + +# Below, we will append filters to the exclusion portion of GTEST_FILTER, so we +# need to guarantee it has one. +if [ -z "$GTEST_FILTER" ]; then + # If it wasn't set, make it add all tests, and remove none. + GTEST_FILTER="*-" +# if GTEST_FILTER already has a negative sign, we leave it alone. +elif [ 0 -eq `expr index "$GTEST_FILTER" "-"` ]; then + # If GTEST_FILTER was set, but does not have a negative sign, add one. This + # gives gtest an empty list of tests to skip. + GTEST_FILTER="$GTEST_FILTER-" +fi + +# The Android supplement allows for installation in these paths: +OEC_PATHS=/vendor/lib64:/vendor/lib:/system/lib64/vndk-R:/system/lib/vndk-R + +# Execute reboot_test in "adb shell" and capture the result. +run_one_pass() { + local test_file=$1 + shift + local test_pass=$1 + shift + if adb $SERIAL_NUM shell ls /data/nativetest/$test_file &> /dev/null; then + test_file=/data/nativetest/$test_file + else + echo "Please install the test on the device in /data/nativetest, " + echo "or begin execution by running ./build_and_run_all_unit_tests.sh" + exit 1 + fi + echo "------ Starting: $test_file" + local tmp_log="$OUT/mediadrmtest.log" + local adb_error="[ADB SHELL] $@ $test_file failed" + # The liboemcrypto.so looks for other shared libraries using the + # LD_LIBRARY_PATH. It is possible that even though the 64-bit liboemcrypto.so + # does not exist, there are 64-bit versions of the shared libraries it tries + # to load. We must reverse the library path in this case so we don't attempt + # to load 64-bit libraries with the 32-bit liboemcrypto.so. + if ! adb $SERIAL_NUM shell ls /vendor/lib64/liboemcrypto.so &> /dev/null; then + OEC_PATHS=/vendor/lib:/vendor/lib64 + fi + TEST_ENV="LD_LIBRARY_PATH=$OEC_PATHS GTEST_FILTER=$GTEST_FILTER" + COMMAND="$test_file --pass=$test_pass --test_data_path=/data/vendor/mediadrm" + echo "adb $SERIAL_NUM shell \"$TEST_ENV $COMMAND\"" + adb $SERIAL_NUM shell "$TEST_ENV $COMMAND" \|\| echo "$adb_error" | tee "$tmp_log" + ! grep -Fq "$adb_error" "$tmp_log" + local result=$? + if [ $result -ne 0 ]; then + final_result=$result + failed_tests+=("$adb_error") + fi +} + +if [ -z "$ANDROID_BUILD_TOP" ]; then + echo "Android build environment not set" + exit -1 +fi + +echo "waiting for device" +ADB_OUTPUT=`adb $SERIAL_NUM root && echo ". " && adb $SERIAL_NUM wait-for-device remount` +echo $ADB_OUTPUT +if echo $ADB_OUTPUT | grep -qi "verity"; then + echo + echo "ERROR: This device has Verity enabled. $0 does not " + echo "work if Verity is enabled. Please disable Verity with" + echo "\"adb $SERIAL_NUM disable-verity\" and try again." + exit -1 +fi + +run_one_pass reboot_test 0 || exit -1 + +echo "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ Reboot device." +sleep 1 +adb reboot +sleep 1 +adb wait-for-device root +sleep 1 +adb wait-for-device remount +sleep 1 + +run_one_pass reboot_test 1 || exit -1