Source release 17.1.0
This commit is contained in:
730
core/test/reboot_test.cpp
Normal file
730
core/test/reboot_test.cpp
Normal file
@@ -0,0 +1,730 @@
|
||||
// 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 <sstream>
|
||||
|
||||
#include "create_test_file_system.h"
|
||||
#include "license_holder.h"
|
||||
#include "log.h"
|
||||
#include "test_sleep.h"
|
||||
|
||||
using wvutil::a2b_hex;
|
||||
using wvutil::FileSystem;
|
||||
using wvutil::TestSleep;
|
||||
using wvutil::unlimited_b2a_hex;
|
||||
|
||||
namespace wvcdm {
|
||||
FileSystem* RebootTest::file_system_;
|
||||
|
||||
namespace {
|
||||
// How much fudge or round off error do we allow in license durations for reboot
|
||||
// tests.
|
||||
constexpr int64_t kFudge = 10;
|
||||
|
||||
// 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" + unlimited_b2a_hex(data) + ",";
|
||||
}
|
||||
}
|
||||
// If we left any braces open, then use hex.
|
||||
if (braces_count != 0) return "0x" + unlimited_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" + unlimited_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" + unlimited_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<uint8_t> result = 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<uint8_t> result = 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<std::string, std::string>& 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<std::string, std::string>* 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_ = CreateTestFileSystem();
|
||||
|
||||
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<ssize_t>(dump.length()));
|
||||
WvCdmTestBase::TearDown();
|
||||
}
|
||||
|
||||
int64_t RebootTest::LoadTime(const std::string& key) {
|
||||
int64_t value = 0;
|
||||
std::istringstream input(persistent_data_[key]);
|
||||
input >> value;
|
||||
if (input.fail()) {
|
||||
LOGE("Could not parse time '%s'", persistent_data_[key].c_str());
|
||||
}
|
||||
if (!input.eof()) {
|
||||
LOGE("Extra text at end of time '%s'", persistent_data_[key].c_str());
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void RebootTest::SaveTime(const std::string& key, int64_t time) {
|
||||
persistent_data_[key] = std::to_string(time);
|
||||
}
|
||||
|
||||
/** 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<std::string, std::string> map1;
|
||||
const std::string dump = DumpData(map1);
|
||||
std::map<std::string, std::string> 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";
|
||||
std::string big_string = "start with something {binary";
|
||||
// Double big_string 8 times, i.e. times 256, so it's bigger than 2k:
|
||||
for (int i = 0; i < 8; i++) big_string = big_string + big_string;
|
||||
map1["big_file"] = big_string;
|
||||
const std::string dump2 = DumpData(map1);
|
||||
std::map<std::string, std::string> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/** Verify that the clock moves forward over a reboot. */
|
||||
TEST_F(RebootTest, TimeMovesForward) {
|
||||
wvutil::TestSleep::Sleep(2);
|
||||
const int64_t start = wvutil::Clock().GetCurrentTime();
|
||||
wvutil::TestSleep::Sleep(2);
|
||||
const int64_t end = wvutil::Clock().GetCurrentTime();
|
||||
EXPECT_NEAR(end - start, 2.0, 1.0);
|
||||
const std::string key = "end_time";
|
||||
if (test_pass() == 0) {
|
||||
// Save off the end of pass 1.
|
||||
SaveTime(key, end);
|
||||
} else {
|
||||
int64_t previous_end = LoadTime(key);
|
||||
EXPECT_LT(previous_end, start);
|
||||
}
|
||||
}
|
||||
|
||||
/** Test offline license durations work correctly after reboot. For time
|
||||
constraints, this runs four sets of tests in parallel. For each set of tests
|
||||
we load a list of licenses with a range of rental or playback durations so
|
||||
that we can be confident that a short wait will find at least one duration
|
||||
in the range that has expired and at least one duration that has not
|
||||
expired.
|
||||
|
||||
RDa. Load an offline license, reboot the device, reload the license and
|
||||
verify that the rental duration is enforced from the time the license was
|
||||
requested.
|
||||
|
||||
RDb. Load an offline license, begin playback, reboot the device, reload the
|
||||
license and verify that the rental duration is enforced from the time the
|
||||
license was requested.
|
||||
|
||||
PDa. Load an offline license, reboot the device, reload the license and
|
||||
verify that the playback duration is enforced from the time initial playback
|
||||
started.
|
||||
|
||||
PDb. Load an offline license, begin playback, reboot the device, reload the
|
||||
license and verify that the playback duration is enforced from the time
|
||||
initial playback started.
|
||||
|
||||
Each of these four sets contains licenses with various durations so that the
|
||||
test can run with a reasonable reboot time. We will assume that a reboot can
|
||||
take anywhere from 10 seconds to almost an hour. With this in mind, we will
|
||||
create a license that has a 10 second duration, one with a 20 second
|
||||
duration, ... and one with an hour duration. All of these licenses will be
|
||||
loaded before the reboot. Then after the reboot we should be in a situation
|
||||
like this:
|
||||
:-----------------------------------------> time axis.
|
||||
: start of test.
|
||||
:: licenses loaded.
|
||||
:: : reboot starts
|
||||
:: : : reboot finished. test resumes.
|
||||
:: : :
|
||||
:[---] 10s - expired :
|
||||
:[-------] 20s expired :
|
||||
: ... :
|
||||
:[---------------------------] this license has not yet expired
|
||||
:[----------------------------------------------] license not expired.
|
||||
: ... :
|
||||
:[---------------------------------------------------] 1 hour. not expired
|
||||
|
||||
After the test resumes, we will have at least one license that has not yet
|
||||
expired. We will then sleep we are near the expiration time of that
|
||||
license. We can then carefully test that this one license from the set
|
||||
enforces its duration.
|
||||
|
||||
This is complicated by the fact that we are testing four different sets. So
|
||||
what we really have is something like this:
|
||||
:-----------------------------------------> time axis.
|
||||
: start of test.
|
||||
:: licenses loaded.
|
||||
:: : reboot starts
|
||||
:: : : reboot finished. test resumes.
|
||||
:: : :
|
||||
:[-----------------------------] first RDa license that has not exipred.
|
||||
:[--------------------------] first RDb license that has not exipred.
|
||||
:[---------------------------] first PDa license that has not exipred.
|
||||
:[-------------------------------] first PDb license that has not exipred.
|
||||
|
||||
Since we want to test all four of these licenses, we will compute the
|
||||
interesting times for each license. These are the times just before, and
|
||||
just after the expiration time. After sorting these interesting times, we
|
||||
will test each one in order.
|
||||
*/
|
||||
class OfflineLicense {
|
||||
public:
|
||||
OfflineLicense(const std::string& test_type, CdmEngine* cdm_engine,
|
||||
const ConfigTestEnv& config, int64_t duration,
|
||||
bool play_before_reboot)
|
||||
: content_id_("CDM_Reboot_" + test_type +
|
||||
(play_before_reboot ? "b_" : "a_") +
|
||||
std::to_string(duration)),
|
||||
duration_(duration),
|
||||
play_before_reboot_(play_before_reboot),
|
||||
license_holder_(content_id_, cdm_engine, config) {
|
||||
license_holder_.set_can_persist(true);
|
||||
}
|
||||
|
||||
virtual ~OfflineLicense() {}
|
||||
|
||||
// Fetch and load the license. The session is left open.
|
||||
void LoadLicense() {
|
||||
license_holder_.OpenSession();
|
||||
start_of_rental_clock_ = wvutil::Clock().GetCurrentTime();
|
||||
license_holder_.FetchLicense();
|
||||
license_holder_.LoadLicense();
|
||||
}
|
||||
|
||||
// Reload the license. The session is left open.
|
||||
void ReloadLicense() {
|
||||
license_holder_.OpenSession();
|
||||
license_holder_.ReloadLicense();
|
||||
}
|
||||
|
||||
// Action to be taken after loading the license, but before the reboot.
|
||||
virtual void BeforeReboot() {
|
||||
if (play_before_reboot_) {
|
||||
Decrypt();
|
||||
}
|
||||
}
|
||||
|
||||
// Action to be taken after reloading the license.
|
||||
virtual void AfterReboot() {}
|
||||
|
||||
void CloseSession() { license_holder_.CloseSession(); }
|
||||
|
||||
// The time this license should be cutoff. Decrypt should succeed before this
|
||||
// time and should fail after this time.
|
||||
virtual int64_t cutoff() = 0;
|
||||
|
||||
// Verify that the license may be used to decrypt content.
|
||||
void Decrypt() {
|
||||
if (start_of_playback_ == 0) {
|
||||
start_of_playback_ = wvutil::Clock().GetCurrentTime();
|
||||
}
|
||||
const KeyId key_id = "0000000000000000";
|
||||
EXPECT_EQ(NO_ERROR, license_holder_.Decrypt(key_id))
|
||||
<< "Failed for " << content_id_
|
||||
<< ", now = " << wvutil::Clock().GetCurrentTime() << ", rental_clock="
|
||||
<< (wvutil::Clock().GetCurrentTime() - start_of_rental_clock_)
|
||||
<< ", playback_clock = "
|
||||
<< (wvutil::Clock().GetCurrentTime() - start_of_playback_)
|
||||
<< ", delta to cutoff = "
|
||||
<< (wvutil::Clock().GetCurrentTime() - cutoff());
|
||||
}
|
||||
|
||||
// Verify that the license has expired, and may not be used to decrypt
|
||||
// content.
|
||||
void FailDecrypt() {
|
||||
const KeyId key_id = "0000000000000000";
|
||||
EXPECT_EQ(NEED_KEY, license_holder_.Decrypt(key_id))
|
||||
<< "Decrypt should have failed for " << content_id_
|
||||
<< ", now = " << wvutil::Clock().GetCurrentTime() << ", rental_clock="
|
||||
<< (wvutil::Clock().GetCurrentTime() - start_of_rental_clock_)
|
||||
<< ", playback_clock = "
|
||||
<< (wvutil::Clock().GetCurrentTime() - start_of_playback_)
|
||||
<< ", delta to cutoff = "
|
||||
<< (wvutil::Clock().GetCurrentTime() - cutoff());
|
||||
}
|
||||
|
||||
// Save times and the key set id to persistent data.
|
||||
void SaveData(RebootTest* reboot_test,
|
||||
std::map<std::string, std::string>* persistent_data) {
|
||||
reboot_test->SaveTime("start_of_rental_" + content_id_,
|
||||
start_of_rental_clock_);
|
||||
reboot_test->SaveTime("start_of_playback_" + content_id_,
|
||||
start_of_playback_);
|
||||
(*persistent_data)["key_set_id_" + content_id_] =
|
||||
license_holder_.key_set_id();
|
||||
}
|
||||
|
||||
// Load times and the key set id from persistent data.
|
||||
void LoadData(RebootTest* reboot_test,
|
||||
std::map<std::string, std::string>* persistent_data) {
|
||||
start_of_rental_clock_ =
|
||||
reboot_test->LoadTime("start_of_rental_" + content_id_);
|
||||
start_of_playback_ =
|
||||
reboot_test->LoadTime("start_of_playback_" + content_id_);
|
||||
license_holder_.set_key_set_id(
|
||||
(*persistent_data)["key_set_id_" + content_id_]);
|
||||
}
|
||||
|
||||
const std::string& content_id() const { return content_id_; }
|
||||
|
||||
protected:
|
||||
const std::string content_id_;
|
||||
int64_t duration_;
|
||||
int64_t start_of_rental_clock_ = 0;
|
||||
int64_t start_of_playback_ = 0;
|
||||
bool play_before_reboot_;
|
||||
LicenseHolder license_holder_;
|
||||
};
|
||||
|
||||
// Holds an offline license that has a limit on the rental duration.
|
||||
class RentalDurationLicense : public OfflineLicense {
|
||||
public:
|
||||
RentalDurationLicense(CdmEngine* cdm_engine, const ConfigTestEnv& config,
|
||||
int64_t duration, bool play_before_reboot)
|
||||
: OfflineLicense("RD", cdm_engine, config, duration, play_before_reboot) {
|
||||
}
|
||||
|
||||
int64_t cutoff() override { return start_of_rental_clock_ + duration_; }
|
||||
};
|
||||
|
||||
// Holds an offline license that has a limit on the playback duration.
|
||||
class PlaybackDurationLicense : public OfflineLicense {
|
||||
public:
|
||||
PlaybackDurationLicense(CdmEngine* cdm_engine, const ConfigTestEnv& config,
|
||||
int64_t duration, bool play_before_reboot)
|
||||
: OfflineLicense("PD", cdm_engine, config, duration, play_before_reboot) {
|
||||
}
|
||||
|
||||
// If we did not start playback before the reboot, we will start playback
|
||||
// just after reloading the license, post-reboot.
|
||||
void AfterReboot() override {
|
||||
if (!play_before_reboot_) {
|
||||
Decrypt();
|
||||
}
|
||||
}
|
||||
|
||||
int64_t cutoff() override { return start_of_playback_ + duration_; }
|
||||
};
|
||||
|
||||
// Test that the rental and playback durations are enforced across a reboot.
|
||||
class OfflineLicenseTest : public RebootTest {
|
||||
public:
|
||||
void SetUp() override {
|
||||
RebootTest::SetUp();
|
||||
EnsureProvisioned();
|
||||
// Run each of the following test cases in parallel so that we don't have to
|
||||
// sleep separately for each one. These durations need to match the polices
|
||||
// specified on the UAT license server.
|
||||
test_case_.resize(4);
|
||||
const std::vector<int64_t> duration_range = {10, 20, 30, 45,
|
||||
60, 300, 900, 3600};
|
||||
for (size_t i = 0; i < duration_range.size(); i++) {
|
||||
test_case_[0].push_back(
|
||||
std::unique_ptr<OfflineLicense>(new RentalDurationLicense(
|
||||
&cdm_engine_, config_, duration_range[i], false)));
|
||||
test_case_[1].push_back(
|
||||
std::unique_ptr<OfflineLicense>(new RentalDurationLicense(
|
||||
&cdm_engine_, config_, duration_range[i], true)));
|
||||
test_case_[2].push_back(
|
||||
std::unique_ptr<OfflineLicense>(new PlaybackDurationLicense(
|
||||
&cdm_engine_, config_, duration_range[i], false)));
|
||||
test_case_[3].push_back(
|
||||
std::unique_ptr<OfflineLicense>(new PlaybackDurationLicense(
|
||||
&cdm_engine_, config_, duration_range[i], true)));
|
||||
}
|
||||
}
|
||||
|
||||
// Load all of the licenses. For the tests that require playback before the
|
||||
// reboot, we start playback here.
|
||||
void LoadAllLicenses() {
|
||||
DeleteAllLicenses();
|
||||
// For each test case, load an offline license and save the data.
|
||||
for (size_t n = 0; n < test_case_.size(); n++) {
|
||||
for (size_t i = 0; i < test_case_[n].size(); i++) {
|
||||
ASSERT_NO_FATAL_FAILURE(test_case_[n][i]->LoadLicense());
|
||||
test_case_[n][i]->BeforeReboot(); // For some tests, we decrypt here.
|
||||
test_case_[n][i]->SaveData(this, &persistent_data_);
|
||||
test_case_[n][i]->CloseSession();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reload all of the licenses. We also go through each license and figure out
|
||||
// which ones have already expired, or close to expire. We then find the first
|
||||
// license from each set that will be next to expire, so that we can test
|
||||
// those more carefully.
|
||||
void ReloadAllLicense() {
|
||||
// first_valid is the index of the first license that has not expired and is
|
||||
// not just about to expire.
|
||||
first_valid_.resize(test_case_.size());
|
||||
for (size_t n = 0; n < test_case_.size(); n++) {
|
||||
for (size_t i = 0; i < test_case_[n].size(); i++) {
|
||||
OfflineLicense* license = test_case_[n][i].get();
|
||||
license->LoadData(this, &persistent_data_);
|
||||
ASSERT_NO_FATAL_FAILURE(license->ReloadLicense());
|
||||
license->AfterReboot();
|
||||
// if past cutoff. make sure decrypt fails.
|
||||
if (license->cutoff() + kFudge < wvutil::Clock().GetCurrentTime()) {
|
||||
license->FailDecrypt();
|
||||
}
|
||||
// If past cutoff, or near cutoff, then we don't want to use it
|
||||
// as our first valid.
|
||||
if (license->cutoff() - 2 * kFudge < wvutil::Clock().GetCurrentTime()) {
|
||||
first_valid_[n] = i + 1;
|
||||
}
|
||||
license->CloseSession();
|
||||
}
|
||||
// We expect there to be at least one license that has not expired yet.
|
||||
// If this is not true, then the reboot time was probably longer than
|
||||
// expected. If it is important to run these tests with a very long reboot
|
||||
// time, then please ask a Widevine engineer to generate more license
|
||||
// policies with longer reboot times.
|
||||
ASSERT_LT(first_valid_[n], test_case_[n].size())
|
||||
<< "For n=" << n
|
||||
<< ", content_id = " << test_case_[n][0]->content_id()
|
||||
<< ", time=" << wvutil::Clock().GetCurrentTime() << "\n"
|
||||
<< "This is an indication that the reboot took a very long time.\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Take the first_valid_ array, which tells us which licenses will expire
|
||||
// soon, and compute the times we are interested in: a little before and a
|
||||
// little after the cutoff. We want to test that the license is valid just
|
||||
// before the cutoff and that the license is not valid just after the
|
||||
// cutoff.
|
||||
//
|
||||
// The interesting_times_ is a std::set, so that we may iterate through the
|
||||
// times in order.
|
||||
void FindInterestingTimes() {
|
||||
for (size_t n = 0; n < test_case_.size(); n++) {
|
||||
OfflineLicense* license = test_case_[n][first_valid_[n]].get();
|
||||
interesting_times_.insert(license->cutoff() - kFudge);
|
||||
interesting_times_.insert(license->cutoff() + kFudge);
|
||||
}
|
||||
}
|
||||
|
||||
// Test at each of the interesting times. These are the times just before and
|
||||
// after the cutoff of the next license to expire from each list of test
|
||||
// cases.
|
||||
void TestInterestingTimes() {
|
||||
int decrypt_count = 0;
|
||||
int fail_count = 0;
|
||||
for (auto time : interesting_times_) {
|
||||
int64_t now = wvutil::Clock().GetCurrentTime();
|
||||
int64_t delta = (time - now);
|
||||
// It is not necessarily an error for the delta to be negative. But it is
|
||||
// an indication that the we are near an error condition. If the current
|
||||
// time is a few seconds past the interesting time, then the license
|
||||
// should still be valid or expired. If delta is very large relative to
|
||||
// kFudge, then the test might fail. This is still an indication that
|
||||
// something is wrong: reloading a license and decrypting some content
|
||||
// should not take multiple seconds.
|
||||
if (delta < 0) LOGW("Sleep delta would be negative: %ld", delta);
|
||||
if (delta > 0) TestSleep::Sleep(static_cast<unsigned int>(delta));
|
||||
// We look at each of the four license that we used to generate the
|
||||
// interesting times. It's possible that two licenses share the same
|
||||
// interesting time, so we have to check each of the four against each
|
||||
// time instead of keeping track of which license generated which time.
|
||||
for (size_t n = 0; n < test_case_.size(); n++) {
|
||||
OfflineLicense* license = test_case_[n][first_valid_[n]].get();
|
||||
ASSERT_NO_FATAL_FAILURE(license->ReloadLicense());
|
||||
if (time == license->cutoff() - kFudge) {
|
||||
license->Decrypt();
|
||||
decrypt_count++;
|
||||
}
|
||||
if (time == license->cutoff() + kFudge) {
|
||||
license->FailDecrypt();
|
||||
fail_count++;
|
||||
}
|
||||
license->CloseSession();
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(decrypt_count, 4) << "Test error. I missed a cutoff";
|
||||
EXPECT_EQ(fail_count, 4) << "Test error. I missed a cutoff";
|
||||
}
|
||||
|
||||
// After we have tested one license from each list of test cases carefully,
|
||||
// all the rest of the licenses can be tested. We do not sleep in this
|
||||
// function, we only test each license that is still valid, or has already
|
||||
// expired. We ignore licenses that are near the cutoff because we already
|
||||
// tested those license in the previous function.
|
||||
void TestAfterInterestingTimes() {
|
||||
// Make sure that all the rest of the licenses are still valid.
|
||||
for (size_t n = 0; n < test_case_.size(); n++) {
|
||||
for (size_t i = first_valid_[n] + 1; i < test_case_[n].size(); i++) {
|
||||
OfflineLicense* license = test_case_[n][i].get();
|
||||
ASSERT_NO_FATAL_FAILURE(license->ReloadLicense());
|
||||
int64_t now = wvutil::Clock().GetCurrentTime();
|
||||
if (now <= license->cutoff() - kFudge) {
|
||||
license->Decrypt();
|
||||
}
|
||||
if (now >= license->cutoff() + kFudge) {
|
||||
license->FailDecrypt();
|
||||
}
|
||||
license->CloseSession();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
// Clean up licenses if any test failed.
|
||||
if (::testing::Test::HasFailure() || test_pass() == 1) {
|
||||
DeleteAllLicenses();
|
||||
}
|
||||
RebootTest::TearDown();
|
||||
}
|
||||
|
||||
void DeleteAllLicenses() {
|
||||
std::vector<std::string> key_set_ids;
|
||||
EXPECT_EQ(NO_ERROR,
|
||||
cdm_engine_.ListStoredLicenses(kSecurityLevelL1, &key_set_ids));
|
||||
for (auto key_set : key_set_ids) {
|
||||
cdm_engine_.RemoveOfflineLicense(key_set, kSecurityLevelL1);
|
||||
}
|
||||
// TODO(b/215230202): Is this necessary? It doesn't seem to work.
|
||||
std::vector<std::string> ksids;
|
||||
std::vector<std::string> pst;
|
||||
std::string app_id = "";
|
||||
EXPECT_EQ(NO_ERROR,
|
||||
cdm_engine_.ListUsageIds(app_id, kSecurityLevelL1, &ksids, &pst));
|
||||
for (auto k : ksids) {
|
||||
EXPECT_EQ(NO_ERROR,
|
||||
cdm_engine_.DeleteUsageRecord(app_id, kSecurityLevelL1, k));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::vector<std::unique_ptr<OfflineLicense>>> test_case_;
|
||||
std::set<int64_t> interesting_times_;
|
||||
std::vector<size_t> first_valid_;
|
||||
};
|
||||
|
||||
TEST_F(OfflineLicenseTest, VariousTests) {
|
||||
if (test_pass() == 0) {
|
||||
LoadAllLicenses();
|
||||
} else if (test_pass() == 1) {
|
||||
ReloadAllLicense();
|
||||
FindInterestingTimes();
|
||||
TestInterestingTimes();
|
||||
TestAfterInterestingTimes();
|
||||
}
|
||||
}
|
||||
} // namespace wvcdm
|
||||
Reference in New Issue
Block a user