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
This commit is contained in:
@@ -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 \
|
||||
|
||||
@@ -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.
|
||||
|
||||
274
libwvdrmengine/cdm/core/test/reboot_test.cpp
Normal file
274
libwvdrmengine/cdm/core/test/reboot_test.cpp
Normal file
@@ -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 <sstream>
|
||||
|
||||
#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<uint8_t> 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<uint8_t> 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<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_ = 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<ssize_t>(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<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";
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace wvcdm
|
||||
54
libwvdrmengine/cdm/core/test/reboot_test.h
Normal file
54
libwvdrmengine/cdm/core/test/reboot_test.h
Normal file
@@ -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 <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#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<std::string, std::string>& 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<std::string, std::string>* 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<std::string, std::string> 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_
|
||||
@@ -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=<N>" << 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=<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;
|
||||
|
||||
@@ -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
|
||||
|
||||
95
libwvdrmengine/run_reboot_test.sh
Executable file
95
libwvdrmengine/run_reboot_test.sh
Executable file
@@ -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
|
||||
Reference in New Issue
Block a user