diff options
Diffstat (limited to 'tests')
28 files changed, 3014 insertions, 1366 deletions
diff --git a/tests/Android.bp b/tests/Android.bp new file mode 100644 index 000000000..2cfc32572 --- /dev/null +++ b/tests/Android.bp @@ -0,0 +1,205 @@ +// Copyright (C) 2018 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +cc_defaults { + name: "recovery_test_defaults", + + defaults: [ + "recovery_defaults", + ], + + include_dirs: [ + "bootable/recovery", + ], + + shared_libs: [ + "libbase", + "libcrypto", + "libcutils", + "liblog", + "libpng", + "libselinux", + "libz", + "libziparchive", + ], + + target: { + android: { + shared_libs: [ + "libutils", + ], + }, + + host: { + static_libs: [ + "libutils", + ], + }, + }, +} + +// libapplypatch, libapplypatch_modes, libimgdiff, libimgpatch +libapplypatch_static_libs = [ + "libapplypatch_modes", + "libapplypatch", + "libedify", + "libimgdiff", + "libimgpatch", + "libotautil", + "libbsdiff", + "libbspatch", + "libdivsufsort", + "libdivsufsort64", + "libutils", + "libbase", + "libbrotli", + "libbz", + "libcrypto", + "libz", + "libziparchive", +] + +// librecovery_defaults uses many shared libs that we want to avoid using in tests (e.g. we don't +// have 32-bit android.hardware.health@2.0.so or libbootloader_message.so on marlin). +librecovery_static_libs = [ + "librecovery", + "librecovery_fastboot", + "libminui", + "libverifier", + "libotautil", + + "libhealthhalutils", + "libvintf_recovery", + "libvintf", + + "android.hardware.health@2.0", + "android.hardware.health@1.0", + "libbootloader_message", + "libext4_utils", + "libfs_mgr", + "libfusesideload", + "libhidl-gen-utils", + "libhidlbase", + "libhidltransport", + "libhwbinder", + "libbinderthreadstate", + "libvndksupport", + "libtinyxml2", +] + +cc_test { + name: "recovery_unit_test", + isolated: true, + + defaults: [ + "recovery_test_defaults", + ], + + test_suites: ["device-tests"], + + srcs: [ + "unit/*.cpp", + ], + + static_libs: libapplypatch_static_libs + [ + "librecovery_ui", + "libminui", + "libverifier", + "libotautil", + "libupdater", + "libgtest_prod", + ], + + data: ["testdata/*"], +} + +cc_test { + name: "recovery_manual_test", + isolated: true, + + defaults: [ + "recovery_test_defaults", + ], + + test_suites: ["device-tests"], + + srcs: [ + "manual/recovery_test.cpp", + ], +} + +cc_test { + name: "recovery_component_test", + isolated: true, + + defaults: [ + "recovery_test_defaults", + "libupdater_defaults", + ], + + test_suites: ["device-tests"], + + srcs: [ + "component/*.cpp", + ], + + static_libs: libapplypatch_static_libs + librecovery_static_libs + [ + "libupdater", + "libupdate_verifier", + "libprotobuf-cpp-lite", + ], + + data: [ + "testdata/*", + ":res-testdata", + ], +} + +cc_test_host { + name: "recovery_host_test", + isolated: true, + + defaults: [ + "recovery_test_defaults", + ], + + srcs: [ + "component/imgdiff_test.cpp", + ], + + static_libs: [ + "libimgdiff", + "libimgpatch", + "libotautil", + "libbsdiff", + "libbspatch", + "libziparchive", + "libutils", + "libcrypto", + "libbrotli", + "libbz", + "libdivsufsort64", + "libdivsufsort", + "libz", + ], + + data: ["testdata/*"], + + target: { + darwin: { + // libimgdiff is not available on the Mac. + enabled: false, + }, + }, +} diff --git a/tests/Android.mk b/tests/Android.mk deleted file mode 100644 index b3584fe87..000000000 --- a/tests/Android.mk +++ /dev/null @@ -1,223 +0,0 @@ -# -# Copyright (C) 2014 The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -LOCAL_PATH := $(call my-dir) - -# Unit tests -include $(CLEAR_VARS) -LOCAL_CFLAGS := -Wall -Werror -LOCAL_MODULE := recovery_unit_test -LOCAL_COMPATIBILITY_SUITE := device-tests -LOCAL_STATIC_LIBRARIES := \ - libverifier \ - libminui \ - libotautil \ - libupdater \ - libziparchive \ - libutils \ - libz \ - libselinux \ - libbase \ - libBionicGtestMain - -LOCAL_SRC_FILES := \ - unit/asn1_decoder_test.cpp \ - unit/dirutil_test.cpp \ - unit/locale_test.cpp \ - unit/rangeset_test.cpp \ - unit/sysutil_test.cpp \ - unit/zip_test.cpp \ - -LOCAL_C_INCLUDES := bootable/recovery -LOCAL_SHARED_LIBRARIES := liblog -include $(BUILD_NATIVE_TEST) - -# Manual tests -include $(CLEAR_VARS) -LOCAL_CFLAGS := -Wall -Werror -LOCAL_MODULE := recovery_manual_test -LOCAL_STATIC_LIBRARIES := \ - libminui \ - libbase \ - libBionicGtestMain - -LOCAL_SRC_FILES := manual/recovery_test.cpp -LOCAL_SHARED_LIBRARIES := \ - liblog \ - libpng - -resource_files := $(call find-files-in-subdirs, bootable/recovery, \ - "*_text.png", \ - res-mdpi/images \ - res-hdpi/images \ - res-xhdpi/images \ - res-xxhdpi/images \ - res-xxxhdpi/images \ - ) - -# The resource image files that will go to $OUT/data/nativetest/recovery. -testimage_out_path := $(TARGET_OUT_DATA)/nativetest/recovery -GEN := $(addprefix $(testimage_out_path)/, $(resource_files)) - -$(GEN): PRIVATE_PATH := $(LOCAL_PATH) -$(GEN): PRIVATE_CUSTOM_TOOL = cp $< $@ -$(GEN): $(testimage_out_path)/% : bootable/recovery/% - $(transform-generated-source) -LOCAL_GENERATED_SOURCES += $(GEN) - -include $(BUILD_NATIVE_TEST) - -# Component tests -include $(CLEAR_VARS) -LOCAL_CFLAGS := \ - -Wall \ - -Werror \ - -D_FILE_OFFSET_BITS=64 - -ifeq ($(AB_OTA_UPDATER),true) -LOCAL_CFLAGS += -DAB_OTA_UPDATER=1 -endif - -ifeq ($(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_SUPPORTS_VERITY),true) -LOCAL_CFLAGS += -DPRODUCT_SUPPORTS_VERITY=1 -endif - -ifeq ($(BOARD_AVB_ENABLE),true) -LOCAL_CFLAGS += -DBOARD_AVB_ENABLE=1 -endif - -LOCAL_MODULE := recovery_component_test -LOCAL_COMPATIBILITY_SUITE := device-tests -LOCAL_C_INCLUDES := bootable/recovery -LOCAL_SRC_FILES := \ - component/applypatch_test.cpp \ - component/bootloader_message_test.cpp \ - component/edify_test.cpp \ - component/imgdiff_test.cpp \ - component/install_test.cpp \ - component/sideload_test.cpp \ - component/uncrypt_test.cpp \ - component/updater_test.cpp \ - component/update_verifier_test.cpp \ - component/verifier_test.cpp - -LOCAL_SHARED_LIBRARIES := \ - libhidlbase - -tune2fs_static_libraries := \ - libext2_com_err \ - libext2_blkid \ - libext2_quota \ - libext2_uuid \ - libext2_e2p \ - libext2fs - -LOCAL_STATIC_LIBRARIES := \ - libapplypatch_modes \ - libapplypatch \ - libedify \ - libimgdiff \ - libimgpatch \ - libbsdiff \ - libbspatch \ - libfusesideload \ - libotafault \ - librecovery \ - libupdater \ - libbootloader_message \ - libverifier \ - libotautil \ - libmounts \ - libupdate_verifier \ - libdivsufsort \ - libdivsufsort64 \ - libfs_mgr \ - libvintf_recovery \ - libvintf \ - libhidl-gen-utils \ - libtinyxml2 \ - libselinux \ - libext4_utils \ - libsparse \ - libcrypto_utils \ - libcrypto \ - libbz \ - libziparchive \ - liblog \ - libutils \ - libz \ - libbase \ - libtune2fs \ - libfec \ - libfec_rs \ - libsquashfs_utils \ - libcutils \ - libbrotli \ - libBionicGtestMain \ - $(tune2fs_static_libraries) - -testdata_files := $(call find-subdir-files, testdata/*) - -# The testdata files that will go to $OUT/data/nativetest/recovery. -testdata_out_path := $(TARGET_OUT_DATA)/nativetest/recovery -GEN := $(addprefix $(testdata_out_path)/, $(testdata_files)) -$(GEN): PRIVATE_PATH := $(LOCAL_PATH) -$(GEN): PRIVATE_CUSTOM_TOOL = cp $< $@ -$(GEN): $(testdata_out_path)/% : $(LOCAL_PATH)/% - $(transform-generated-source) -LOCAL_GENERATED_SOURCES += $(GEN) - -# A copy of the testdata to be packed into continuous_native_tests.zip. -testdata_continuous_zip_prefix := \ - $(call intermediates-dir-for,PACKAGING,recovery_component_test)/DATA -testdata_continuous_zip_path := $(testdata_continuous_zip_prefix)/nativetest/recovery -GEN := $(addprefix $(testdata_continuous_zip_path)/, $(testdata_files)) -$(GEN): PRIVATE_PATH := $(LOCAL_PATH) -$(GEN): PRIVATE_CUSTOM_TOOL = cp $< $@ -$(GEN): $(testdata_continuous_zip_path)/% : $(LOCAL_PATH)/% - $(transform-generated-source) -LOCAL_GENERATED_SOURCES += $(GEN) -LOCAL_PICKUP_FILES := $(testdata_continuous_zip_prefix) - -include $(BUILD_NATIVE_TEST) - -# Host tests -include $(CLEAR_VARS) -LOCAL_CFLAGS := -Wall -Werror -LOCAL_MODULE := recovery_host_test -LOCAL_MODULE_HOST_OS := linux -LOCAL_C_INCLUDES := bootable/recovery -LOCAL_SRC_FILES := \ - component/imgdiff_test.cpp -LOCAL_STATIC_LIBRARIES := \ - libimgdiff \ - libimgpatch \ - libotautil \ - libbsdiff \ - libbspatch \ - libziparchive \ - libutils \ - libbase \ - libcrypto \ - libbrotli \ - libbz \ - libdivsufsort64 \ - libdivsufsort \ - libz \ - libBionicGtestMain -LOCAL_SHARED_LIBRARIES := \ - liblog -include $(BUILD_HOST_NATIVE_TEST) diff --git a/tests/AndroidTest.xml b/tests/AndroidTest.xml index 3999aa57d..6b86085aa 100644 --- a/tests/AndroidTest.xml +++ b/tests/AndroidTest.xml @@ -16,16 +16,18 @@ <configuration description="Config for recovery_component_test and recovery_unit_test"> <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> <option name="cleanup" value="true" /> - <option name="push" value="recovery_component_test->/data/local/tmp/recovery_component_test" /> - <option name="push" value="recovery_unit_test->/data/local/tmp/recovery_unit_test" /> + <option name="push" value="recovery_component_test->/data/local/tmp/recovery_component_test/recovery_component_test" /> + <option name="push" value="testdata->/data/local/tmp/recovery_component_test/testdata" /> + <option name="push" value="recovery_unit_test->/data/local/tmp/recovery_unit_test/recovery_unit_test" /> + <option name="push" value="testdata->/data/local/tmp/recovery_unit_test/testdata" /> </target_preparer> <option name="test-suite-tag" value="apct" /> <test class="com.android.tradefed.testtype.GTest" > - <option name="native-test-device-path" value="/data/local/tmp" /> + <option name="native-test-device-path" value="/data/local/tmp/recovery_component_test" /> <option name="module-name" value="recovery_component_test" /> </test> <test class="com.android.tradefed.testtype.GTest" > - <option name="native-test-device-path" value="/data/local/tmp" /> + <option name="native-test-device-path" value="/data/local/tmp/recovery_unit_test" /> <option name="module-name" value="recovery_unit_test" /> </test> </configuration> diff --git a/tests/common/test_constants.h b/tests/common/test_constants.h index 514818e0a..b6c27a754 100644 --- a/tests/common/test_constants.h +++ b/tests/common/test_constants.h @@ -17,10 +17,10 @@ #ifndef _OTA_TEST_CONSTANTS_H #define _OTA_TEST_CONSTANTS_H -#include <stdlib.h> - #include <string> +#include <android-base/file.h> + // Zip entries in ziptest_valid.zip. static const std::string kATxtContents("abcdefghabcdefgh\n"); static const std::string kBTxtContents("abcdefgh\n"); @@ -32,14 +32,9 @@ static const std::string kATxtSha1Sum("32c96a03dc8cd20097940f351bca6261ee5a1643" // echo -n -e "abcdefgh\n" | sha1sum static const std::string kBTxtSha1Sum("e414af7161c9554089f4106d6f1797ef14a73666"); -static std::string from_testdata_base(const std::string& fname) { -#ifdef __ANDROID__ - static std::string data_root = getenv("ANDROID_DATA"); -#else - static std::string data_root = std::string(getenv("ANDROID_PRODUCT_OUT")) + "/data"; -#endif - - return data_root + "/nativetest/recovery/testdata/" + fname; +[[maybe_unused]] static std::string from_testdata_base(const std::string& fname) { + static std::string exec_dir = android::base::GetExecutableDirectory(); + return exec_dir + "/testdata/" + fname; } #endif // _OTA_TEST_CONSTANTS_H diff --git a/tests/component/applypatch_modes_test.cpp b/tests/component/applypatch_modes_test.cpp new file mode 100644 index 000000000..ce01f4fd5 --- /dev/null +++ b/tests/component/applypatch_modes_test.cpp @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agree to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <stdio.h> +#include <stdlib.h> + +#include <string> +#include <vector> + +#include <android-base/file.h> +#include <android-base/logging.h> +#include <android-base/strings.h> +#include <android-base/test_utils.h> +#include <bsdiff/bsdiff.h> +#include <gtest/gtest.h> +#include <openssl/sha.h> + +#include "applypatch/applypatch_modes.h" +#include "common/test_constants.h" +#include "otautil/paths.h" +#include "otautil/print_sha1.h" +#include "otautil/sysutil.h" + +using namespace std::string_literals; + +// Loads a given partition and returns a string of form "EMMC:name:size:hash". +static std::string GetEmmcTargetString(const std::string& filename, + const std::string& display_name = "") { + std::string data; + if (!android::base::ReadFileToString(filename, &data)) { + PLOG(ERROR) << "Failed to read " << filename; + return {}; + } + + uint8_t digest[SHA_DIGEST_LENGTH]; + SHA1(reinterpret_cast<const uint8_t*>(data.c_str()), data.size(), digest); + + return "EMMC:"s + (display_name.empty() ? filename : display_name) + ":" + + std::to_string(data.size()) + ":" + print_sha1(digest); +} + +class ApplyPatchModesTest : public ::testing::Test { + protected: + void SetUp() override { + source = GetEmmcTargetString(from_testdata_base("boot.img")); + ASSERT_FALSE(source.empty()); + + std::string recovery_file = from_testdata_base("recovery.img"); + recovery = GetEmmcTargetString(recovery_file); + ASSERT_FALSE(recovery.empty()); + + ASSERT_TRUE(android::base::WriteStringToFile("", patched_file_.path)); + target = GetEmmcTargetString(recovery_file, patched_file_.path); + ASSERT_FALSE(target.empty()); + + Paths::Get().set_cache_temp_source(cache_source_.path); + } + + std::string source; + std::string target; + std::string recovery; + + private: + TemporaryFile cache_source_; + TemporaryFile patched_file_; +}; + +static int InvokeApplyPatchModes(const std::vector<std::string>& args) { + auto args_to_call = StringVectorToNullTerminatedArray(args); + return applypatch_modes(args_to_call.size() - 1, args_to_call.data()); +} + +static void VerifyPatchedTarget(const std::string& target) { + std::vector<std::string> pieces = android::base::Split(target, ":"); + ASSERT_EQ(4, pieces.size()); + ASSERT_EQ("EMMC", pieces[0]); + + std::string patched_emmc = GetEmmcTargetString(pieces[1]); + ASSERT_FALSE(patched_emmc.empty()); + ASSERT_EQ(target, patched_emmc); +} + +TEST_F(ApplyPatchModesTest, InvalidArgs) { + // At least two args (including the filename). + ASSERT_EQ(2, InvokeApplyPatchModes({ "applypatch" })); + + // Unrecognized args. + ASSERT_EQ(2, InvokeApplyPatchModes({ "applypatch", "-x" })); +} + +TEST_F(ApplyPatchModesTest, PatchModeEmmcTarget) { + std::vector<std::string> args{ + "applypatch", + "--bonus", + from_testdata_base("bonus.file"), + "--patch", + from_testdata_base("recovery-from-boot.p"), + "--target", + target, + "--source", + source, + }; + ASSERT_EQ(0, InvokeApplyPatchModes(args)); + VerifyPatchedTarget(target); +} + +// Tests patching an eMMC target without a separate bonus file (i.e. recovery-from-boot patch has +// everything). +TEST_F(ApplyPatchModesTest, PatchModeEmmcTargetWithoutBonusFile) { + std::vector<std::string> args{ + "applypatch", "--patch", from_testdata_base("recovery-from-boot-with-bonus.p"), + "--target", target, "--source", + source, + }; + + ASSERT_EQ(0, InvokeApplyPatchModes(args)); + VerifyPatchedTarget(target); +} + +// Ensures that applypatch works with a bsdiff based recovery-from-boot.p. +TEST_F(ApplyPatchModesTest, PatchModeEmmcTargetWithBsdiffPatch) { + // Generate the bsdiff patch of recovery-from-boot.p. + std::string src_content; + ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("boot.img"), &src_content)); + + std::string tgt_content; + ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("recovery.img"), &tgt_content)); + + TemporaryFile patch_file; + ASSERT_EQ(0, + bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(src_content.data()), src_content.size(), + reinterpret_cast<const uint8_t*>(tgt_content.data()), tgt_content.size(), + patch_file.path, nullptr)); + + std::vector<std::string> args{ + "applypatch", "--patch", patch_file.path, "--target", target, "--source", source, + }; + ASSERT_EQ(0, InvokeApplyPatchModes(args)); + VerifyPatchedTarget(target); +} + +TEST_F(ApplyPatchModesTest, PatchModeInvalidArgs) { + // Invalid bonus file. + std::vector<std::string> args{ + "applypatch", "--bonus", "/doesntexist", "--patch", from_testdata_base("recovery-from-boot.p"), + "--target", target, "--source", source, + }; + ASSERT_NE(0, InvokeApplyPatchModes(args)); + + // With bonus file, but missing args. + ASSERT_NE(0, + InvokeApplyPatchModes({ "applypatch", "--bonus", from_testdata_base("bonus.file") })); +} + +TEST_F(ApplyPatchModesTest, FlashMode) { + std::vector<std::string> args{ + "applypatch", "--flash", from_testdata_base("recovery.img"), "--target", target, + }; + ASSERT_EQ(0, InvokeApplyPatchModes(args)); + VerifyPatchedTarget(target); +} + +TEST_F(ApplyPatchModesTest, FlashModeInvalidArgs) { + std::vector<std::string> args{ + "applypatch", "--bonus", from_testdata_base("bonus.file"), "--flash", source, + "--target", target, + }; + ASSERT_NE(0, InvokeApplyPatchModes(args)); +} + +TEST_F(ApplyPatchModesTest, CheckMode) { + ASSERT_EQ(0, InvokeApplyPatchModes({ "applypatch", "--check", recovery })); + ASSERT_EQ(0, InvokeApplyPatchModes({ "applypatch", "--check", source })); +} + +TEST_F(ApplyPatchModesTest, CheckModeInvalidArgs) { + ASSERT_EQ(2, InvokeApplyPatchModes({ "applypatch", "--check" })); +} + +TEST_F(ApplyPatchModesTest, CheckModeNonEmmcTarget) { + ASSERT_NE(0, InvokeApplyPatchModes({ "applypatch", "--check", from_testdata_base("boot.img") })); +} + +TEST_F(ApplyPatchModesTest, ShowLicenses) { + ASSERT_EQ(0, InvokeApplyPatchModes({ "applypatch", "--license" })); +} diff --git a/tests/component/applypatch_test.cpp b/tests/component/applypatch_test.cpp deleted file mode 100644 index 61e06adb6..000000000 --- a/tests/component/applypatch_test.cpp +++ /dev/null @@ -1,387 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agree to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include <fcntl.h> -#include <gtest/gtest.h> -#include <stdio.h> -#include <stdlib.h> -#include <sys/stat.h> -#include <sys/statvfs.h> -#include <sys/types.h> -#include <time.h> - -#include <memory> -#include <string> -#include <vector> - -#include <android-base/file.h> -#include <android-base/stringprintf.h> -#include <android-base/test_utils.h> -#include <bsdiff/bsdiff.h> -#include <openssl/sha.h> - -#include "applypatch/applypatch.h" -#include "applypatch/applypatch_modes.h" -#include "common/test_constants.h" -#include "otautil/cache_location.h" -#include "otautil/print_sha1.h" - -using namespace std::string_literals; - -static void sha1sum(const std::string& fname, std::string* sha1, size_t* fsize = nullptr) { - ASSERT_NE(nullptr, sha1); - - std::string data; - ASSERT_TRUE(android::base::ReadFileToString(fname, &data)); - - if (fsize != nullptr) { - *fsize = data.size(); - } - - uint8_t digest[SHA_DIGEST_LENGTH]; - SHA1(reinterpret_cast<const uint8_t*>(data.c_str()), data.size(), digest); - *sha1 = print_sha1(digest); -} - -static void mangle_file(const std::string& fname) { - std::string content(1024, '\0'); - for (size_t i = 0; i < 1024; i++) { - content[i] = rand() % 256; - } - ASSERT_TRUE(android::base::WriteStringToFile(content, fname)); -} - -class ApplyPatchTest : public ::testing::Test { - public: - virtual void SetUp() override { - // set up files - old_file = from_testdata_base("old.file"); - new_file = from_testdata_base("new.file"); - nonexistent_file = from_testdata_base("nonexistent.file"); - - // set up SHA constants - sha1sum(old_file, &old_sha1, &old_size); - sha1sum(new_file, &new_sha1, &new_size); - srand(time(nullptr)); - bad_sha1_a = android::base::StringPrintf("%040x", rand()); - bad_sha1_b = android::base::StringPrintf("%040x", rand()); - } - - std::string old_file; - std::string new_file; - std::string nonexistent_file; - - std::string old_sha1; - std::string new_sha1; - std::string bad_sha1_a; - std::string bad_sha1_b; - - size_t old_size; - size_t new_size; -}; - -class ApplyPatchCacheTest : public ApplyPatchTest { - protected: - void SetUp() override { - ApplyPatchTest::SetUp(); - CacheLocation::location().set_cache_temp_source(old_file); - } -}; - -class ApplyPatchModesTest : public ::testing::Test { - protected: - void SetUp() override { - CacheLocation::location().set_cache_temp_source(cache_source.path); - } - - TemporaryFile cache_source; -}; - -TEST_F(ApplyPatchTest, CheckModeSkip) { - std::vector<std::string> sha1s; - ASSERT_EQ(0, applypatch_check(&old_file[0], sha1s)); -} - -TEST_F(ApplyPatchTest, CheckModeSingle) { - std::vector<std::string> sha1s = { old_sha1 }; - ASSERT_EQ(0, applypatch_check(&old_file[0], sha1s)); -} - -TEST_F(ApplyPatchTest, CheckModeMultiple) { - std::vector<std::string> sha1s = { bad_sha1_a, old_sha1, bad_sha1_b }; - ASSERT_EQ(0, applypatch_check(&old_file[0], sha1s)); -} - -TEST_F(ApplyPatchTest, CheckModeFailure) { - std::vector<std::string> sha1s = { bad_sha1_a, bad_sha1_b }; - ASSERT_NE(0, applypatch_check(&old_file[0], sha1s)); -} - -TEST_F(ApplyPatchTest, CheckModeEmmcTarget) { - // EMMC:old_file:size:sha1 should pass the check. - std::string src_file = - "EMMC:" + old_file + ":" + std::to_string(old_size) + ":" + old_sha1; - std::vector<std::string> sha1s; - ASSERT_EQ(0, applypatch_check(src_file.c_str(), sha1s)); - - // EMMC:old_file:(size-1):sha1:(size+1):sha1 should fail the check. - src_file = "EMMC:" + old_file + ":" + std::to_string(old_size - 1) + ":" + old_sha1 + ":" + - std::to_string(old_size + 1) + ":" + old_sha1; - ASSERT_EQ(1, applypatch_check(src_file.c_str(), sha1s)); - - // EMMC:old_file:(size-1):sha1:size:sha1:(size+1):sha1 should pass the check. - src_file = "EMMC:" + old_file + ":" + - std::to_string(old_size - 1) + ":" + old_sha1 + ":" + - std::to_string(old_size) + ":" + old_sha1 + ":" + - std::to_string(old_size + 1) + ":" + old_sha1; - ASSERT_EQ(0, applypatch_check(src_file.c_str(), sha1s)); - - // EMMC:old_file:(size+1):sha1:(size-1):sha1:size:sha1 should pass the check. - src_file = "EMMC:" + old_file + ":" + - std::to_string(old_size + 1) + ":" + old_sha1 + ":" + - std::to_string(old_size - 1) + ":" + old_sha1 + ":" + - std::to_string(old_size) + ":" + old_sha1; - ASSERT_EQ(0, applypatch_check(src_file.c_str(), sha1s)); - - // EMMC:new_file:(size+1):old_sha1:(size-1):old_sha1:size:old_sha1:size:new_sha1 - // should pass the check. - src_file = "EMMC:" + new_file + ":" + - std::to_string(old_size + 1) + ":" + old_sha1 + ":" + - std::to_string(old_size - 1) + ":" + old_sha1 + ":" + - std::to_string(old_size) + ":" + old_sha1 + ":" + - std::to_string(new_size) + ":" + new_sha1; - ASSERT_EQ(0, applypatch_check(src_file.c_str(), sha1s)); -} - -TEST_F(ApplyPatchCacheTest, CheckCacheCorruptedSourceSingle) { - TemporaryFile temp_file; - mangle_file(temp_file.path); - std::vector<std::string> sha1s_single = { old_sha1 }; - ASSERT_EQ(0, applypatch_check(temp_file.path, sha1s_single)); - ASSERT_EQ(0, applypatch_check(nonexistent_file.c_str(), sha1s_single)); -} - -TEST_F(ApplyPatchCacheTest, CheckCacheCorruptedSourceMultiple) { - TemporaryFile temp_file; - mangle_file(temp_file.path); - std::vector<std::string> sha1s_multiple = { bad_sha1_a, old_sha1, bad_sha1_b }; - ASSERT_EQ(0, applypatch_check(temp_file.path, sha1s_multiple)); - ASSERT_EQ(0, applypatch_check(nonexistent_file.c_str(), sha1s_multiple)); -} - -TEST_F(ApplyPatchCacheTest, CheckCacheCorruptedSourceFailure) { - TemporaryFile temp_file; - mangle_file(temp_file.path); - std::vector<std::string> sha1s_failure = { bad_sha1_a, bad_sha1_b }; - ASSERT_NE(0, applypatch_check(temp_file.path, sha1s_failure)); - ASSERT_NE(0, applypatch_check(nonexistent_file.c_str(), sha1s_failure)); -} - -TEST_F(ApplyPatchModesTest, InvalidArgs) { - // At least two args (including the filename). - ASSERT_EQ(2, applypatch_modes(1, (const char* []){ "applypatch" })); - - // Unrecognized args. - ASSERT_EQ(2, applypatch_modes(2, (const char* []){ "applypatch", "-x" })); -} - -TEST_F(ApplyPatchModesTest, PatchModeEmmcTarget) { - std::string boot_img = from_testdata_base("boot.img"); - size_t boot_img_size; - std::string boot_img_sha1; - sha1sum(boot_img, &boot_img_sha1, &boot_img_size); - - std::string recovery_img = from_testdata_base("recovery.img"); - size_t size; - std::string recovery_img_sha1; - sha1sum(recovery_img, &recovery_img_sha1, &size); - std::string recovery_img_size = std::to_string(size); - - std::string bonus_file = from_testdata_base("bonus.file"); - - // applypatch -b <bonus-file> <src-file> <tgt-file> <tgt-sha1> <tgt-size> <src-sha1>:<patch> - TemporaryFile tmp1; - std::string src_file = - "EMMC:" + boot_img + ":" + std::to_string(boot_img_size) + ":" + boot_img_sha1; - std::string tgt_file = "EMMC:" + std::string(tmp1.path); - std::string patch = boot_img_sha1 + ":" + from_testdata_base("recovery-from-boot.p"); - std::vector<const char*> args = { - "applypatch", - "-b", - bonus_file.c_str(), - src_file.c_str(), - tgt_file.c_str(), - recovery_img_sha1.c_str(), - recovery_img_size.c_str(), - patch.c_str() - }; - ASSERT_EQ(0, applypatch_modes(args.size(), args.data())); - - // applypatch <src-file> <tgt-file> <tgt-sha1> <tgt-size> <src-sha1>:<patch> - TemporaryFile tmp2; - patch = boot_img_sha1 + ":" + from_testdata_base("recovery-from-boot-with-bonus.p"); - tgt_file = "EMMC:" + std::string(tmp2.path); - std::vector<const char*> args2 = { - "applypatch", - src_file.c_str(), - tgt_file.c_str(), - recovery_img_sha1.c_str(), - recovery_img_size.c_str(), - patch.c_str() - }; - ASSERT_EQ(0, applypatch_modes(args2.size(), args2.data())); - - // applypatch -b <bonus-file> <src-file> <tgt-file> <tgt-sha1> <tgt-size> \ - // <src-sha1-fake>:<patch1> <src-sha1>:<patch2> - TemporaryFile tmp3; - tgt_file = "EMMC:" + std::string(tmp3.path); - std::string bad_sha1_a = android::base::StringPrintf("%040x", rand()); - std::string bad_sha1_b = android::base::StringPrintf("%040x", rand()); - std::string patch1 = bad_sha1_a + ":" + from_testdata_base("recovery-from-boot.p"); - std::string patch2 = boot_img_sha1 + ":" + from_testdata_base("recovery-from-boot.p"); - std::string patch3 = bad_sha1_b + ":" + from_testdata_base("recovery-from-boot.p"); - std::vector<const char*> args3 = { - "applypatch", - "-b", - bonus_file.c_str(), - src_file.c_str(), - tgt_file.c_str(), - recovery_img_sha1.c_str(), - recovery_img_size.c_str(), - patch1.c_str(), - patch2.c_str(), - patch3.c_str() - }; - ASSERT_EQ(0, applypatch_modes(args3.size(), args3.data())); -} - -// Ensures that applypatch works with a bsdiff based recovery-from-boot.p. -TEST_F(ApplyPatchModesTest, PatchModeEmmcTargetWithBsdiffPatch) { - std::string boot_img_file = from_testdata_base("boot.img"); - std::string boot_img_sha1; - size_t boot_img_size; - sha1sum(boot_img_file, &boot_img_sha1, &boot_img_size); - - std::string recovery_img_file = from_testdata_base("recovery.img"); - std::string recovery_img_sha1; - size_t recovery_img_size; - sha1sum(recovery_img_file, &recovery_img_sha1, &recovery_img_size); - - // Generate the bsdiff patch of recovery-from-boot.p. - std::string src_content; - ASSERT_TRUE(android::base::ReadFileToString(boot_img_file, &src_content)); - - std::string tgt_content; - ASSERT_TRUE(android::base::ReadFileToString(recovery_img_file, &tgt_content)); - - TemporaryFile patch_file; - ASSERT_EQ(0, - bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(src_content.data()), src_content.size(), - reinterpret_cast<const uint8_t*>(tgt_content.data()), tgt_content.size(), - patch_file.path, nullptr)); - - // applypatch <src-file> <tgt-file> <tgt-sha1> <tgt-size> <src-sha1>:<patch> - std::string src_file_arg = - "EMMC:" + boot_img_file + ":" + std::to_string(boot_img_size) + ":" + boot_img_sha1; - TemporaryFile tgt_file; - std::string tgt_file_arg = "EMMC:"s + tgt_file.path; - std::string recovery_img_size_arg = std::to_string(recovery_img_size); - std::string patch_arg = boot_img_sha1 + ":" + patch_file.path; - std::vector<const char*> args = { "applypatch", - src_file_arg.c_str(), - tgt_file_arg.c_str(), - recovery_img_sha1.c_str(), - recovery_img_size_arg.c_str(), - patch_arg.c_str() }; - ASSERT_EQ(0, applypatch_modes(args.size(), args.data())); - - // Double check the patched recovery image. - std::string tgt_file_sha1; - size_t tgt_file_size; - sha1sum(tgt_file.path, &tgt_file_sha1, &tgt_file_size); - ASSERT_EQ(recovery_img_size, tgt_file_size); - ASSERT_EQ(recovery_img_sha1, tgt_file_sha1); -} - -TEST_F(ApplyPatchModesTest, PatchModeInvalidArgs) { - // Invalid bonus file. - ASSERT_NE(0, applypatch_modes(3, (const char* []){ "applypatch", "-b", "/doesntexist" })); - - std::string bonus_file = from_testdata_base("bonus.file"); - // With bonus file, but missing args. - ASSERT_EQ(2, applypatch_modes(3, (const char* []){ "applypatch", "-b", bonus_file.c_str() })); - - std::string boot_img = from_testdata_base("boot.img"); - size_t boot_img_size; - std::string boot_img_sha1; - sha1sum(boot_img, &boot_img_sha1, &boot_img_size); - - std::string recovery_img = from_testdata_base("recovery.img"); - size_t size; - std::string recovery_img_sha1; - sha1sum(recovery_img, &recovery_img_sha1, &size); - std::string recovery_img_size = std::to_string(size); - - // Bonus file is not supported in flash mode. - // applypatch -b <bonus-file> <src-file> <tgt-file> <tgt-sha1> <tgt-size> - TemporaryFile tmp4; - std::vector<const char*> args4 = { - "applypatch", - "-b", - bonus_file.c_str(), - boot_img.c_str(), - tmp4.path, - recovery_img_sha1.c_str(), - recovery_img_size.c_str() - }; - ASSERT_NE(0, applypatch_modes(args4.size(), args4.data())); - - // Failed to parse patch args. - TemporaryFile tmp5; - std::string bad_arg1 = - "invalid-sha1:filename" + from_testdata_base("recovery-from-boot-with-bonus.p"); - std::vector<const char*> args5 = { - "applypatch", - boot_img.c_str(), - tmp5.path, - recovery_img_sha1.c_str(), - recovery_img_size.c_str(), - bad_arg1.c_str() - }; - ASSERT_NE(0, applypatch_modes(args5.size(), args5.data())); - - // Target size cannot be zero. - TemporaryFile tmp6; - std::string patch = boot_img_sha1 + ":" + from_testdata_base("recovery-from-boot-with-bonus.p"); - std::vector<const char*> args6 = { - "applypatch", - boot_img.c_str(), - tmp6.path, - recovery_img_sha1.c_str(), - "0", // target size - patch.c_str() - }; - ASSERT_NE(0, applypatch_modes(args6.size(), args6.data())); -} - -TEST_F(ApplyPatchModesTest, CheckModeInvalidArgs) { - // Insufficient args. - ASSERT_EQ(2, applypatch_modes(2, (const char* []){ "applypatch", "-c" })); -} - -TEST_F(ApplyPatchModesTest, ShowLicenses) { - ASSERT_EQ(0, applypatch_modes(2, (const char* []){ "applypatch", "-l" })); -} diff --git a/tests/component/edify_test.cpp b/tests/component/edify_test.cpp index 61a1e6b64..8397bd38e 100644 --- a/tests/component/edify_test.cpp +++ b/tests/component/edify_test.cpp @@ -21,30 +21,29 @@ #include "edify/expr.h" -static void expect(const char* expr_str, const char* expected) { - std::unique_ptr<Expr> e; - int error_count = 0; - EXPECT_EQ(0, parse_string(expr_str, &e, &error_count)); - EXPECT_EQ(0, error_count); - - State state(expr_str, nullptr); - - std::string result; - bool status = Evaluate(&state, e, &result); - - if (expected == nullptr) { - EXPECT_FALSE(status); - } else { - EXPECT_STREQ(expected, result.c_str()); - } - +static void expect(const std::string& expr_str, const char* expected) { + std::unique_ptr<Expr> e; + int error_count = 0; + EXPECT_EQ(0, ParseString(expr_str, &e, &error_count)); + EXPECT_EQ(0, error_count); + + State state(expr_str, nullptr); + + std::string result; + bool status = Evaluate(&state, e, &result); + + if (expected == nullptr) { + EXPECT_FALSE(status); + } else { + EXPECT_STREQ(expected, result.c_str()); + } } class EdifyTest : public ::testing::Test { - protected: - virtual void SetUp() { - RegisterBuiltins(); - } + protected: + void SetUp() { + RegisterBuiltins(); + } }; TEST_F(EdifyTest, parsing) { @@ -146,25 +145,23 @@ TEST_F(EdifyTest, comparison) { } TEST_F(EdifyTest, big_string) { - // big string - expect(std::string(8192, 's').c_str(), std::string(8192, 's').c_str()); + expect(std::string(8192, 's'), std::string(8192, 's').c_str()); } TEST_F(EdifyTest, unknown_function) { - // unknown function - const char* script1 = "unknown_function()"; - std::unique_ptr<Expr> expr; - int error_count = 0; - EXPECT_EQ(1, parse_string(script1, &expr, &error_count)); - EXPECT_EQ(1, error_count); - - const char* script2 = "abc; unknown_function()"; - error_count = 0; - EXPECT_EQ(1, parse_string(script2, &expr, &error_count)); - EXPECT_EQ(1, error_count); - - const char* script3 = "unknown_function1() || yes"; - error_count = 0; - EXPECT_EQ(1, parse_string(script3, &expr, &error_count)); - EXPECT_EQ(1, error_count); + const char* script1 = "unknown_function()"; + std::unique_ptr<Expr> expr; + int error_count = 0; + EXPECT_EQ(1, ParseString(script1, &expr, &error_count)); + EXPECT_EQ(1, error_count); + + const char* script2 = "abc; unknown_function()"; + error_count = 0; + EXPECT_EQ(1, ParseString(script2, &expr, &error_count)); + EXPECT_EQ(1, error_count); + + const char* script3 = "unknown_function1() || yes"; + error_count = 0; + EXPECT_EQ(1, ParseString(script3, &expr, &error_count)); + EXPECT_EQ(1, error_count); } diff --git a/tests/component/imgdiff_test.cpp b/tests/component/imgdiff_test.cpp index 6c23def01..cb4868a4a 100644 --- a/tests/component/imgdiff_test.cpp +++ b/tests/component/imgdiff_test.cpp @@ -197,12 +197,17 @@ TEST(ImgdiffTest, zip_mode_smoke_store) { } TEST(ImgdiffTest, zip_mode_smoke_compressed) { + // Generate 1 block of random data. + std::string random_data; + random_data.reserve(4096); + generate_n(back_inserter(random_data), 4096, []() { return rand() % 256; }); + // Construct src and tgt zip files. TemporaryFile src_file; FILE* src_file_ptr = fdopen(src_file.release(), "wb"); ZipWriter src_writer(src_file_ptr); ASSERT_EQ(0, src_writer.StartEntry("file1.txt", ZipWriter::kCompress)); - const std::string src_content("abcdefg"); + const std::string src_content = random_data; ASSERT_EQ(0, src_writer.WriteBytes(src_content.data(), src_content.size())); ASSERT_EQ(0, src_writer.FinishEntry()); ASSERT_EQ(0, src_writer.Finish()); @@ -212,7 +217,7 @@ TEST(ImgdiffTest, zip_mode_smoke_compressed) { FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb"); ZipWriter tgt_writer(tgt_file_ptr); ASSERT_EQ(0, tgt_writer.StartEntry("file1.txt", ZipWriter::kCompress)); - const std::string tgt_content("abcdefgxyz"); + const std::string tgt_content = random_data + "extra contents"; ASSERT_EQ(0, tgt_writer.WriteBytes(tgt_content.data(), tgt_content.size())); ASSERT_EQ(0, tgt_writer.FinishEntry()); ASSERT_EQ(0, tgt_writer.Finish()); @@ -245,13 +250,57 @@ TEST(ImgdiffTest, zip_mode_smoke_compressed) { verify_patched_image(src, patch, tgt); } +TEST(ImgdiffTest, zip_mode_empty_target) { + TemporaryFile src_file; + FILE* src_file_ptr = fdopen(src_file.release(), "wb"); + ZipWriter src_writer(src_file_ptr); + ASSERT_EQ(0, src_writer.StartEntry("file1.txt", ZipWriter::kCompress)); + const std::string src_content = "abcdefg"; + ASSERT_EQ(0, src_writer.WriteBytes(src_content.data(), src_content.size())); + ASSERT_EQ(0, src_writer.FinishEntry()); + ASSERT_EQ(0, src_writer.Finish()); + ASSERT_EQ(0, fclose(src_file_ptr)); + + // Construct a empty entry in the target zip. + TemporaryFile tgt_file; + FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb"); + ZipWriter tgt_writer(tgt_file_ptr); + ASSERT_EQ(0, tgt_writer.StartEntry("file1.txt", ZipWriter::kCompress)); + const std::string tgt_content; + ASSERT_EQ(0, tgt_writer.WriteBytes(tgt_content.data(), tgt_content.size())); + ASSERT_EQ(0, tgt_writer.FinishEntry()); + ASSERT_EQ(0, tgt_writer.Finish()); + + // Compute patch. + TemporaryFile patch_file; + std::vector<const char*> args = { + "imgdiff", "-z", src_file.path, tgt_file.path, patch_file.path, + }; + ASSERT_EQ(0, imgdiff(args.size(), args.data())); + + // Verify. + std::string tgt; + ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt)); + std::string src; + ASSERT_TRUE(android::base::ReadFileToString(src_file.path, &src)); + std::string patch; + ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch)); + + verify_patched_image(src, patch, tgt); +} + TEST(ImgdiffTest, zip_mode_smoke_trailer_zeros) { + // Generate 1 block of random data. + std::string random_data; + random_data.reserve(4096); + generate_n(back_inserter(random_data), 4096, []() { return rand() % 256; }); + // Construct src and tgt zip files. TemporaryFile src_file; FILE* src_file_ptr = fdopen(src_file.release(), "wb"); ZipWriter src_writer(src_file_ptr); ASSERT_EQ(0, src_writer.StartEntry("file1.txt", ZipWriter::kCompress)); - const std::string src_content("abcdefg"); + const std::string src_content = random_data; ASSERT_EQ(0, src_writer.WriteBytes(src_content.data(), src_content.size())); ASSERT_EQ(0, src_writer.FinishEntry()); ASSERT_EQ(0, src_writer.Finish()); @@ -261,7 +310,7 @@ TEST(ImgdiffTest, zip_mode_smoke_trailer_zeros) { FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb"); ZipWriter tgt_writer(tgt_file_ptr); ASSERT_EQ(0, tgt_writer.StartEntry("file1.txt", ZipWriter::kCompress)); - const std::string tgt_content("abcdefgxyz"); + const std::string tgt_content = random_data + "abcdefg"; ASSERT_EQ(0, tgt_writer.WriteBytes(tgt_content.data(), tgt_content.size())); ASSERT_EQ(0, tgt_writer.FinishEntry()); ASSERT_EQ(0, tgt_writer.Finish()); @@ -298,23 +347,19 @@ TEST(ImgdiffTest, zip_mode_smoke_trailer_zeros) { } TEST(ImgdiffTest, image_mode_simple) { - // src: "abcdefgh" + gzipped "xyz" (echo -n "xyz" | gzip -f | hd). - const std::vector<char> src_data = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', - 'h', '\x1f', '\x8b', '\x08', '\x00', '\xc4', '\x1e', - '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xac', - '\x02', '\x00', '\x67', '\xba', '\x8e', '\xeb', '\x03', - '\x00', '\x00', '\x00' }; - const std::string src(src_data.cbegin(), src_data.cend()); + std::string gzipped_source_path = from_testdata_base("gzipped_source"); + std::string gzipped_source; + ASSERT_TRUE(android::base::ReadFileToString(gzipped_source_path, &gzipped_source)); + + const std::string src = "abcdefg" + gzipped_source; TemporaryFile src_file; ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path)); - // tgt: "abcdefgxyz" + gzipped "xxyyzz". - const std::vector<char> tgt_data = { - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z', '\x1f', '\x8b', - '\x08', '\x00', '\x62', '\x1f', '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xa8', '\xac', - '\xac', '\xaa', '\x02', '\x00', '\x96', '\x30', '\x06', '\xb7', '\x06', '\x00', '\x00', '\x00' - }; - const std::string tgt(tgt_data.cbegin(), tgt_data.cend()); + std::string gzipped_target_path = from_testdata_base("gzipped_target"); + std::string gzipped_target; + ASSERT_TRUE(android::base::ReadFileToString(gzipped_target_path, &gzipped_target)); + const std::string tgt = "abcdefgxyz" + gzipped_target; + TemporaryFile tgt_file; ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path)); @@ -404,23 +449,21 @@ TEST(ImgdiffTest, image_mode_different_num_chunks) { } TEST(ImgdiffTest, image_mode_merge_chunks) { - // src: "abcdefgh" + gzipped "xyz" (echo -n "xyz" | gzip -f | hd). - const std::vector<char> src_data = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', - 'h', '\x1f', '\x8b', '\x08', '\x00', '\xc4', '\x1e', - '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xac', - '\x02', '\x00', '\x67', '\xba', '\x8e', '\xeb', '\x03', - '\x00', '\x00', '\x00' }; - const std::string src(src_data.cbegin(), src_data.cend()); + // src: "abcdefg" + gzipped_source. + std::string gzipped_source_path = from_testdata_base("gzipped_source"); + std::string gzipped_source; + ASSERT_TRUE(android::base::ReadFileToString(gzipped_source_path, &gzipped_source)); + + const std::string src = "abcdefg" + gzipped_source; TemporaryFile src_file; ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path)); - // tgt: gzipped "xyz" + "abcdefgh". - const std::vector<char> tgt_data = { - '\x1f', '\x8b', '\x08', '\x00', '\x62', '\x1f', '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', - '\xa8', '\xac', '\xac', '\xaa', '\x02', '\x00', '\x96', '\x30', '\x06', '\xb7', '\x06', '\x00', - '\x00', '\x00', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z' - }; - const std::string tgt(tgt_data.cbegin(), tgt_data.cend()); + // tgt: gzipped_target + "abcdefgxyz". + std::string gzipped_target_path = from_testdata_base("gzipped_target"); + std::string gzipped_target; + ASSERT_TRUE(android::base::ReadFileToString(gzipped_target_path, &gzipped_target)); + + const std::string tgt = gzipped_target + "abcdefgxyz"; TemporaryFile tgt_file; ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path)); diff --git a/tests/component/install_test.cpp b/tests/component/install_test.cpp index d19d788e4..08b429000 100644 --- a/tests/component/install_test.cpp +++ b/tests/component/install_test.cpp @@ -33,6 +33,7 @@ #include <ziparchive/zip_writer.h> #include "install.h" +#include "otautil/paths.h" #include "private/install.h" TEST(InstallTest, verify_package_compatibility_no_entry) { @@ -199,8 +200,73 @@ TEST(InstallTest, verify_package_compatibility_with_libvintf_system_manifest_xml CloseArchive(zip); } -#ifdef AB_OTA_UPDATER -static void VerifyAbUpdateBinaryCommand(const std::string& serialno, bool success = true) { +TEST(InstallTest, SetUpNonAbUpdateCommands) { + TemporaryFile temp_file; + FILE* zip_file = fdopen(temp_file.release(), "w"); + ZipWriter writer(zip_file); + static constexpr const char* UPDATE_BINARY_NAME = "META-INF/com/google/android/update-binary"; + ASSERT_EQ(0, writer.StartEntry(UPDATE_BINARY_NAME, kCompressStored)); + ASSERT_EQ(0, writer.FinishEntry()); + ASSERT_EQ(0, writer.Finish()); + ASSERT_EQ(0, fclose(zip_file)); + + ZipArchiveHandle zip; + ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); + int status_fd = 10; + std::string package = "/path/to/update.zip"; + TemporaryDir td; + std::string binary_path = std::string(td.path) + "/update_binary"; + Paths::Get().set_temporary_update_binary(binary_path); + std::vector<std::string> cmd; + ASSERT_EQ(0, SetUpNonAbUpdateCommands(package, zip, 0, status_fd, &cmd)); + ASSERT_EQ(4U, cmd.size()); + ASSERT_EQ(binary_path, cmd[0]); + ASSERT_EQ("3", cmd[1]); // RECOVERY_API_VERSION + ASSERT_EQ(std::to_string(status_fd), cmd[2]); + ASSERT_EQ(package, cmd[3]); + struct stat sb; + ASSERT_EQ(0, stat(binary_path.c_str(), &sb)); + ASSERT_EQ(static_cast<mode_t>(0755), sb.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO)); + + // With non-zero retry count. update_binary will be removed automatically. + cmd.clear(); + ASSERT_EQ(0, SetUpNonAbUpdateCommands(package, zip, 2, status_fd, &cmd)); + ASSERT_EQ(5U, cmd.size()); + ASSERT_EQ(binary_path, cmd[0]); + ASSERT_EQ("3", cmd[1]); // RECOVERY_API_VERSION + ASSERT_EQ(std::to_string(status_fd), cmd[2]); + ASSERT_EQ(package, cmd[3]); + ASSERT_EQ("retry", cmd[4]); + sb = {}; + ASSERT_EQ(0, stat(binary_path.c_str(), &sb)); + ASSERT_EQ(static_cast<mode_t>(0755), sb.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO)); + + CloseArchive(zip); +} + +TEST(InstallTest, SetUpNonAbUpdateCommands_MissingUpdateBinary) { + TemporaryFile temp_file; + FILE* zip_file = fdopen(temp_file.release(), "w"); + ZipWriter writer(zip_file); + // The archive must have something to be opened correctly. + ASSERT_EQ(0, writer.StartEntry("dummy_entry", 0)); + ASSERT_EQ(0, writer.FinishEntry()); + ASSERT_EQ(0, writer.Finish()); + ASSERT_EQ(0, fclose(zip_file)); + + // Missing update binary. + ZipArchiveHandle zip; + ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); + int status_fd = 10; + std::string package = "/path/to/update.zip"; + TemporaryDir td; + Paths::Get().set_temporary_update_binary(std::string(td.path) + "/update_binary"); + std::vector<std::string> cmd; + ASSERT_EQ(INSTALL_CORRUPT, SetUpNonAbUpdateCommands(package, zip, 0, status_fd, &cmd)); + CloseArchive(zip); +} + +static void VerifyAbUpdateCommands(const std::string& serialno, bool success = true) { TemporaryFile temp_file; FILE* zip_file = fdopen(temp_file.release(), "w"); ZipWriter writer(zip_file); @@ -235,73 +301,27 @@ static void VerifyAbUpdateBinaryCommand(const std::string& serialno, bool succes ASSERT_EQ(0, FindEntry(zip, payload_name, &payload_entry)); int status_fd = 10; std::string package = "/path/to/update.zip"; - std::string binary_path = "/sbin/update_engine_sideload"; std::vector<std::string> cmd; if (success) { - ASSERT_EQ(0, update_binary_command(package, zip, binary_path, 0, status_fd, &cmd)); + ASSERT_EQ(0, SetUpAbUpdateCommands(package, zip, status_fd, &cmd)); ASSERT_EQ(5U, cmd.size()); - ASSERT_EQ(binary_path, cmd[0]); + ASSERT_EQ("/system/bin/update_engine_sideload", cmd[0]); ASSERT_EQ("--payload=file://" + package, cmd[1]); ASSERT_EQ("--offset=" + std::to_string(payload_entry.offset), cmd[2]); ASSERT_EQ("--headers=" + properties, cmd[3]); ASSERT_EQ("--status_fd=" + std::to_string(status_fd), cmd[4]); } else { - ASSERT_EQ(INSTALL_ERROR, update_binary_command(package, zip, binary_path, 0, status_fd, &cmd)); + ASSERT_EQ(INSTALL_ERROR, SetUpAbUpdateCommands(package, zip, status_fd, &cmd)); } CloseArchive(zip); } -#endif // AB_OTA_UPDATER -TEST(InstallTest, update_binary_command_smoke) { -#ifdef AB_OTA_UPDATER +TEST(InstallTest, SetUpAbUpdateCommands) { // Empty serialno will pass the verification. - VerifyAbUpdateBinaryCommand({}); -#else - TemporaryFile temp_file; - FILE* zip_file = fdopen(temp_file.release(), "w"); - ZipWriter writer(zip_file); - static constexpr const char* UPDATE_BINARY_NAME = "META-INF/com/google/android/update-binary"; - ASSERT_EQ(0, writer.StartEntry(UPDATE_BINARY_NAME, kCompressStored)); - ASSERT_EQ(0, writer.FinishEntry()); - ASSERT_EQ(0, writer.Finish()); - ASSERT_EQ(0, fclose(zip_file)); - - ZipArchiveHandle zip; - ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); - int status_fd = 10; - std::string package = "/path/to/update.zip"; - TemporaryDir td; - std::string binary_path = std::string(td.path) + "/update_binary"; - std::vector<std::string> cmd; - ASSERT_EQ(0, update_binary_command(package, zip, binary_path, 0, status_fd, &cmd)); - ASSERT_EQ(4U, cmd.size()); - ASSERT_EQ(binary_path, cmd[0]); - ASSERT_EQ("3", cmd[1]); // RECOVERY_API_VERSION - ASSERT_EQ(std::to_string(status_fd), cmd[2]); - ASSERT_EQ(package, cmd[3]); - struct stat sb; - ASSERT_EQ(0, stat(binary_path.c_str(), &sb)); - ASSERT_EQ(static_cast<mode_t>(0755), sb.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO)); - - // With non-zero retry count. update_binary will be removed automatically. - cmd.clear(); - ASSERT_EQ(0, update_binary_command(package, zip, binary_path, 2, status_fd, &cmd)); - ASSERT_EQ(5U, cmd.size()); - ASSERT_EQ(binary_path, cmd[0]); - ASSERT_EQ("3", cmd[1]); // RECOVERY_API_VERSION - ASSERT_EQ(std::to_string(status_fd), cmd[2]); - ASSERT_EQ(package, cmd[3]); - ASSERT_EQ("retry", cmd[4]); - sb = {}; - ASSERT_EQ(0, stat(binary_path.c_str(), &sb)); - ASSERT_EQ(static_cast<mode_t>(0755), sb.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO)); - - CloseArchive(zip); -#endif // AB_OTA_UPDATER + VerifyAbUpdateCommands({}); } -TEST(InstallTest, update_binary_command_invalid) { -#ifdef AB_OTA_UPDATER +TEST(InstallTest, SetUpAbUpdateCommands_MissingPayloadPropertiesTxt) { TemporaryFile temp_file; FILE* zip_file = fdopen(temp_file.release(), "w"); ZipWriter writer(zip_file); @@ -328,60 +348,36 @@ TEST(InstallTest, update_binary_command_invalid) { ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); int status_fd = 10; std::string package = "/path/to/update.zip"; - std::string binary_path = "/sbin/update_engine_sideload"; - std::vector<std::string> cmd; - ASSERT_EQ(INSTALL_CORRUPT, update_binary_command(package, zip, binary_path, 0, status_fd, &cmd)); - CloseArchive(zip); -#else - TemporaryFile temp_file; - FILE* zip_file = fdopen(temp_file.release(), "w"); - ZipWriter writer(zip_file); - // The archive must have something to be opened correctly. - ASSERT_EQ(0, writer.StartEntry("dummy_entry", 0)); - ASSERT_EQ(0, writer.FinishEntry()); - ASSERT_EQ(0, writer.Finish()); - ASSERT_EQ(0, fclose(zip_file)); - - // Missing update binary. - ZipArchiveHandle zip; - ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); - int status_fd = 10; - std::string package = "/path/to/update.zip"; - TemporaryDir td; - std::string binary_path = std::string(td.path) + "/update_binary"; std::vector<std::string> cmd; - ASSERT_EQ(INSTALL_CORRUPT, update_binary_command(package, zip, binary_path, 0, status_fd, &cmd)); + ASSERT_EQ(INSTALL_CORRUPT, SetUpAbUpdateCommands(package, zip, status_fd, &cmd)); CloseArchive(zip); -#endif // AB_OTA_UPDATER } -#ifdef AB_OTA_UPDATER -TEST(InstallTest, update_binary_command_multiple_serialno) { +TEST(InstallTest, SetUpAbUpdateCommands_MultipleSerialnos) { std::string serialno = android::base::GetProperty("ro.serialno", ""); ASSERT_NE("", serialno); // Single matching serialno will pass the verification. - VerifyAbUpdateBinaryCommand(serialno); + VerifyAbUpdateCommands(serialno); static constexpr char alphabet[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; auto generator = []() { return alphabet[rand() % (sizeof(alphabet) - 1)]; }; // Generate 900 random serial numbers. - std::string random_serial; + std::string random_serialno; for (size_t i = 0; i < 900; i++) { - generate_n(back_inserter(random_serial), serialno.size(), generator); - random_serial.append("|"); + generate_n(back_inserter(random_serialno), serialno.size(), generator); + random_serialno.append("|"); } // Random serialnos should fail the verification. - VerifyAbUpdateBinaryCommand(random_serial, false); + VerifyAbUpdateCommands(random_serialno, false); - std::string long_serial = random_serial + serialno + "|"; + std::string long_serialno = random_serialno + serialno + "|"; for (size_t i = 0; i < 99; i++) { - generate_n(back_inserter(long_serial), serialno.size(), generator); - long_serial.append("|"); + generate_n(back_inserter(long_serialno), serialno.size(), generator); + long_serialno.append("|"); } // String with the matching serialno should pass the verification. - VerifyAbUpdateBinaryCommand(long_serial); + VerifyAbUpdateCommands(long_serialno); } -#endif // AB_OTA_UPDATER diff --git a/tests/component/resources_test.cpp b/tests/component/resources_test.cpp new file mode 100644 index 000000000..54329db22 --- /dev/null +++ b/tests/component/resources_test.cpp @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <dirent.h> +#include <stdio.h> +#include <stdlib.h> + +#include <memory> +#include <string> +#include <vector> + +#include <android-base/file.h> +#include <android-base/strings.h> +#include <gtest/gtest.h> +#include <png.h> + +#include "minui/minui.h" +#include "private/resources.h" + +static const std::string kLocale = "zu"; + +static const std::vector<std::string> kResourceImagesDirs{ + "res-mdpi/images/", "res-hdpi/images/", "res-xhdpi/images/", + "res-xxhdpi/images/", "res-xxxhdpi/images/", +}; + +static int png_filter(const dirent* de) { + if (de->d_type != DT_REG || !android::base::EndsWith(de->d_name, "_text.png")) { + return 0; + } + return 1; +} + +// Finds out all the PNG files to test, which stay under the same dir with the executabl.. +static std::vector<std::string> add_files() { + std::vector<std::string> files; + for (const std::string& images_dir : kResourceImagesDirs) { + static std::string exec_dir = android::base::GetExecutableDirectory(); + std::string dir_path = exec_dir + "/" + images_dir; + dirent** namelist; + int n = scandir(dir_path.c_str(), &namelist, png_filter, alphasort); + if (n == -1) { + printf("Failed to scandir %s: %s\n", dir_path.c_str(), strerror(errno)); + continue; + } + if (n == 0) { + printf("No file is added for test in %s\n", dir_path.c_str()); + } + + while (n--) { + std::string file_path = dir_path + namelist[n]->d_name; + files.push_back(file_path); + free(namelist[n]); + } + free(namelist); + } + return files; +} + +class ResourcesTest : public testing::TestWithParam<std::string> { + public: + static std::vector<std::string> png_list; + + protected: + void SetUp() override { + png_ = std::make_unique<PngHandler>(GetParam()); + ASSERT_TRUE(png_); + + ASSERT_EQ(PNG_COLOR_TYPE_GRAY, png_->color_type()) << "Recovery expects grayscale PNG file."; + ASSERT_LT(static_cast<png_uint_32>(5), png_->width()); + ASSERT_LT(static_cast<png_uint_32>(0), png_->height()); + ASSERT_EQ(1, png_->channels()) << "Recovery background text images expects 1-channel PNG file."; + } + + std::unique_ptr<PngHandler> png_{ nullptr }; +}; + +// Parses a png file and tests if it's qualified for the background text image under recovery. +TEST_P(ResourcesTest, ValidateLocale) { + std::vector<unsigned char> row(png_->width()); + for (png_uint_32 y = 0; y < png_->height(); ++y) { + png_read_row(png_->png_ptr(), row.data(), nullptr); + int w = (row[1] << 8) | row[0]; + int h = (row[3] << 8) | row[2]; + int len = row[4]; + EXPECT_LT(0, w); + EXPECT_LT(0, h); + EXPECT_LT(0, len) << "Locale string should be non-empty."; + EXPECT_NE(0, row[5]) << "Locale string is missing."; + + ASSERT_GT(png_->height(), y + 1 + h) << "Locale: " << kLocale << " is not found in the file."; + char* loc = reinterpret_cast<char*>(&row[5]); + if (matches_locale(loc, kLocale.c_str())) { + EXPECT_TRUE(android::base::StartsWith(loc, kLocale)); + break; + } + for (int i = 0; i < h; ++i, ++y) { + png_read_row(png_->png_ptr(), row.data(), nullptr); + } + } +} + +std::vector<std::string> ResourcesTest::png_list = add_files(); + +INSTANTIATE_TEST_CASE_P(BackgroundTextValidation, ResourcesTest, + ::testing::ValuesIn(ResourcesTest::png_list.cbegin(), + ResourcesTest::png_list.cend())); diff --git a/tests/component/update_verifier_test.cpp b/tests/component/update_verifier_test.cpp index 1544bb2a4..2420c27fe 100644 --- a/tests/component/update_verifier_test.cpp +++ b/tests/component/update_verifier_test.cpp @@ -14,29 +14,89 @@ * limitations under the License. */ +#include <update_verifier/update_verifier.h> + +#include <functional> #include <string> +#include <unordered_map> +#include <vector> #include <android-base/file.h> +#include <android-base/properties.h> +#include <android-base/strings.h> #include <android-base/test_utils.h> +#include <google/protobuf/repeated_field.h> #include <gtest/gtest.h> -#include <update_verifier/update_verifier.h> + +#include "care_map.pb.h" + +using namespace std::string_literals; class UpdateVerifierTest : public ::testing::Test { protected: void SetUp() override { -#if defined(PRODUCT_SUPPORTS_VERITY) || defined(BOARD_AVB_ENABLE) - verity_supported = true; -#else - verity_supported = false; -#endif + std::string verity_mode = android::base::GetProperty("ro.boot.veritymode", ""); + verity_supported = android::base::EqualsIgnoreCase(verity_mode, "enforcing"); + + care_map_prefix_ = care_map_dir_.path + "/care_map"s; + care_map_pb_ = care_map_dir_.path + "/care_map.pb"s; + care_map_txt_ = care_map_dir_.path + "/care_map.txt"s; + // Overrides the the care_map_prefix. + verifier_.set_care_map_prefix(care_map_prefix_); + + property_id_ = "ro.build.fingerprint"; + fingerprint_ = android::base::GetProperty(property_id_, ""); + // Overrides the property_reader if we cannot read the given property on the device. + if (fingerprint_.empty()) { + fingerprint_ = "mock_fingerprint"; + verifier_.set_property_reader([](const std::string& /* id */) { return "mock_fingerprint"; }); + } + } + + void TearDown() override { + unlink(care_map_pb_.c_str()); + unlink(care_map_txt_.c_str()); + } + + // Returns a serialized string of the proto3 message according to the given partition info. + std::string ConstructProto( + std::vector<std::unordered_map<std::string, std::string>>& partitions) { + recovery_update_verifier::CareMap result; + for (const auto& partition : partitions) { + recovery_update_verifier::CareMap::PartitionInfo info; + if (partition.find("name") != partition.end()) { + info.set_name(partition.at("name")); + } + if (partition.find("ranges") != partition.end()) { + info.set_ranges(partition.at("ranges")); + } + if (partition.find("id") != partition.end()) { + info.set_id(partition.at("id")); + } + if (partition.find("fingerprint") != partition.end()) { + info.set_fingerprint(partition.at("fingerprint")); + } + + *result.add_partitions() = info; + } + + return result.SerializeAsString(); } bool verity_supported; + UpdateVerifier verifier_; + + TemporaryDir care_map_dir_; + std::string care_map_prefix_; + std::string care_map_pb_; + std::string care_map_txt_; + + std::string property_id_; + std::string fingerprint_; }; TEST_F(UpdateVerifierTest, verify_image_no_care_map) { - // Non-existing care_map is allowed. - ASSERT_TRUE(verify_image("/doesntexist")); + ASSERT_FALSE(verifier_.ParseCareMap()); } TEST_F(UpdateVerifierTest, verify_image_smoke) { @@ -46,26 +106,28 @@ TEST_F(UpdateVerifierTest, verify_image_smoke) { return; } - TemporaryFile temp_file; std::string content = "system\n2,0,1"; - ASSERT_TRUE(android::base::WriteStringToFile(content, temp_file.path)); - ASSERT_TRUE(verify_image(temp_file.path)); + ASSERT_TRUE(android::base::WriteStringToFile(content, care_map_txt_)); + ASSERT_TRUE(verifier_.ParseCareMap()); + ASSERT_TRUE(verifier_.VerifyPartitions()); // Leading and trailing newlines should be accepted. - ASSERT_TRUE(android::base::WriteStringToFile("\n" + content + "\n\n", temp_file.path)); - ASSERT_TRUE(verify_image(temp_file.path)); + ASSERT_TRUE(android::base::WriteStringToFile("\n" + content + "\n\n", care_map_txt_)); + ASSERT_TRUE(verifier_.ParseCareMap()); + ASSERT_TRUE(verifier_.VerifyPartitions()); +} + +TEST_F(UpdateVerifierTest, verify_image_empty_care_map) { + ASSERT_FALSE(verifier_.ParseCareMap()); } TEST_F(UpdateVerifierTest, verify_image_wrong_lines) { // The care map file can have only 2 / 4 / 6 lines. - TemporaryFile temp_file; - ASSERT_FALSE(verify_image(temp_file.path)); - - ASSERT_TRUE(android::base::WriteStringToFile("line1", temp_file.path)); - ASSERT_FALSE(verify_image(temp_file.path)); + ASSERT_TRUE(android::base::WriteStringToFile("line1", care_map_txt_)); + ASSERT_FALSE(verifier_.ParseCareMap()); - ASSERT_TRUE(android::base::WriteStringToFile("line1\nline2\nline3", temp_file.path)); - ASSERT_FALSE(verify_image(temp_file.path)); + ASSERT_TRUE(android::base::WriteStringToFile("line1\nline2\nline3", care_map_txt_)); + ASSERT_FALSE(verifier_.ParseCareMap()); } TEST_F(UpdateVerifierTest, verify_image_malformed_care_map) { @@ -75,10 +137,9 @@ TEST_F(UpdateVerifierTest, verify_image_malformed_care_map) { return; } - TemporaryFile temp_file; std::string content = "system\n2,1,0"; - ASSERT_TRUE(android::base::WriteStringToFile(content, temp_file.path)); - ASSERT_FALSE(verify_image(temp_file.path)); + ASSERT_TRUE(android::base::WriteStringToFile(content, care_map_txt_)); + ASSERT_FALSE(verifier_.ParseCareMap()); } TEST_F(UpdateVerifierTest, verify_image_legacy_care_map) { @@ -88,8 +149,110 @@ TEST_F(UpdateVerifierTest, verify_image_legacy_care_map) { return; } - TemporaryFile temp_file; std::string content = "/dev/block/bootdevice/by-name/system\n2,1,0"; - ASSERT_TRUE(android::base::WriteStringToFile(content, temp_file.path)); - ASSERT_TRUE(verify_image(temp_file.path)); + ASSERT_TRUE(android::base::WriteStringToFile(content, care_map_txt_)); + ASSERT_FALSE(verifier_.ParseCareMap()); +} + +TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_smoke) { + // This test relies on dm-verity support. + if (!verity_supported) { + GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support."; + return; + } + + std::vector<std::unordered_map<std::string, std::string>> partitions = { + { + { "name", "system" }, + { "ranges", "2,0,1" }, + { "id", property_id_ }, + { "fingerprint", fingerprint_ }, + }, + }; + + std::string proto = ConstructProto(partitions); + ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_)); + ASSERT_TRUE(verifier_.ParseCareMap()); + ASSERT_TRUE(verifier_.VerifyPartitions()); +} + +TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_missing_name) { + // This test relies on dm-verity support. + if (!verity_supported) { + GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support."; + return; + } + + std::vector<std::unordered_map<std::string, std::string>> partitions = { + { + { "ranges", "2,0,1" }, + { "id", property_id_ }, + { "fingerprint", fingerprint_ }, + }, + }; + + std::string proto = ConstructProto(partitions); + ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_)); + ASSERT_FALSE(verifier_.ParseCareMap()); +} + +TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_bad_ranges) { + // This test relies on dm-verity support. + if (!verity_supported) { + GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support."; + return; + } + + std::vector<std::unordered_map<std::string, std::string>> partitions = { + { + { "name", "system" }, + { "ranges", "3,0,1" }, + { "id", property_id_ }, + { "fingerprint", fingerprint_ }, + }, + }; + + std::string proto = ConstructProto(partitions); + ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_)); + ASSERT_FALSE(verifier_.ParseCareMap()); +} + +TEST_F(UpdateVerifierTest, verify_image_protobuf_empty_fingerprint) { + // This test relies on dm-verity support. + if (!verity_supported) { + GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support."; + return; + } + + std::vector<std::unordered_map<std::string, std::string>> partitions = { + { + { "name", "system" }, + { "ranges", "2,0,1" }, + }, + }; + + std::string proto = ConstructProto(partitions); + ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_)); + ASSERT_FALSE(verifier_.ParseCareMap()); +} + +TEST_F(UpdateVerifierTest, verify_image_protobuf_fingerprint_mismatch) { + // This test relies on dm-verity support. + if (!verity_supported) { + GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support."; + return; + } + + std::vector<std::unordered_map<std::string, std::string>> partitions = { + { + { "name", "system" }, + { "ranges", "2,0,1" }, + { "id", property_id_ }, + { "fingerprint", "unsupported_fingerprint" }, + }, + }; + + std::string proto = ConstructProto(partitions); + ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_)); + ASSERT_FALSE(verifier_.ParseCareMap()); } diff --git a/tests/component/updater_test.cpp b/tests/component/updater_test.cpp index 5bfd7cb40..24c63e776 100644 --- a/tests/component/updater_test.cpp +++ b/tests/component/updater_test.cpp @@ -27,6 +27,8 @@ #include <vector> #include <android-base/file.h> +#include <android-base/logging.h> +#include <android-base/parseint.h> #include <android-base/properties.h> #include <android-base/stringprintf.h> #include <android-base/strings.h> @@ -35,26 +37,33 @@ #include <brotli/encode.h> #include <bsdiff/bsdiff.h> #include <gtest/gtest.h> +#include <verity/hash_tree_builder.h> #include <ziparchive/zip_archive.h> #include <ziparchive/zip_writer.h> +#include "applypatch/applypatch.h" #include "common/test_constants.h" #include "edify/expr.h" -#include "otautil/SysUtil.h" -#include "otautil/cache_location.h" #include "otautil/error_code.h" +#include "otautil/paths.h" #include "otautil/print_sha1.h" +#include "otautil/sysutil.h" +#include "private/commands.h" #include "updater/blockimg.h" #include "updater/install.h" #include "updater/updater.h" -struct selabel_handle *sehandle = nullptr; +using namespace std::string_literals; -static void expect(const char* expected, const char* expr_str, CauseCode cause_code, +using PackageEntries = std::unordered_map<std::string, std::string>; + +struct selabel_handle* sehandle = nullptr; + +static void expect(const char* expected, const std::string& expr_str, CauseCode cause_code, UpdaterInfo* info = nullptr) { std::unique_ptr<Expr> e; int error_count = 0; - ASSERT_EQ(0, parse_string(expr_str, &e, &error_count)); + ASSERT_EQ(0, ParseString(expr_str, &e, &error_count)); ASSERT_EQ(0, error_count); State state(expr_str, info); @@ -65,7 +74,7 @@ static void expect(const char* expected, const char* expr_str, CauseCode cause_c if (expected == nullptr) { ASSERT_FALSE(status); } else { - ASSERT_TRUE(status); + ASSERT_TRUE(status) << "Evaluate() finished with error message: " << state.errmsg; ASSERT_STREQ(expected, result.c_str()); } @@ -76,12 +85,12 @@ static void expect(const char* expected, const char* expr_str, CauseCode cause_c ASSERT_EQ(cause_code, state.cause_code); } -static void BuildUpdatePackage(const std::unordered_map<std::string, std::string>& entries, - int fd) { +static void BuildUpdatePackage(const PackageEntries& entries, int fd) { FILE* zip_file_ptr = fdopen(fd, "wb"); ZipWriter zip_writer(zip_file_ptr); for (const auto& entry : entries) { + // All the entries are written as STORED. ASSERT_EQ(0, zip_writer.StartEntry(entry.first.c_str(), 0)); if (!entry.second.empty()) { ASSERT_EQ(0, zip_writer.WriteBytes(entry.second.data(), entry.second.size())); @@ -93,28 +102,103 @@ static void BuildUpdatePackage(const std::unordered_map<std::string, std::string ASSERT_EQ(0, fclose(zip_file_ptr)); } +static void RunBlockImageUpdate(bool is_verify, const PackageEntries& entries, + const std::string& image_file, const std::string& result, + CauseCode cause_code = kNoCause) { + CHECK(entries.find("transfer_list") != entries.end()); + + // Build the update package. + TemporaryFile zip_file; + BuildUpdatePackage(entries, zip_file.release()); + + MemMapping map; + ASSERT_TRUE(map.MapFile(zip_file.path)); + ZipArchiveHandle handle; + ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle)); + + // Set up the handler, command_pipe, patch offset & length. + UpdaterInfo updater_info; + updater_info.package_zip = handle; + TemporaryFile temp_pipe; + updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe"); + updater_info.package_zip_addr = map.addr; + updater_info.package_zip_len = map.length; + + std::string new_data = entries.find("new_data.br") != entries.end() ? "new_data.br" : "new_data"; + std::string script = is_verify ? "block_image_verify" : "block_image_update"; + script += R"((")" + image_file + R"(", package_extract_file("transfer_list"), ")" + new_data + + R"(", "patch_data"))"; + expect(result.c_str(), script, cause_code, &updater_info); + + ASSERT_EQ(0, fclose(updater_info.cmd_pipe)); + CloseArchive(handle); +} + static std::string get_sha1(const std::string& content) { uint8_t digest[SHA_DIGEST_LENGTH]; SHA1(reinterpret_cast<const uint8_t*>(content.c_str()), content.size(), digest); return print_sha1(digest); } +static Value* BlobToString(const char* name, State* state, + const std::vector<std::unique_ptr<Expr>>& argv) { + if (argv.size() != 1) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects 1 arg, got %zu", name, argv.size()); + } + + std::vector<std::unique_ptr<Value>> args; + if (!ReadValueArgs(state, argv, &args)) { + return nullptr; + } + + if (args[0]->type != Value::Type::BLOB) { + return ErrorAbort(state, kArgsParsingFailure, "%s() expects a BLOB argument", name); + } + + args[0]->type = Value::Type::STRING; + return args[0].release(); +} + class UpdaterTest : public ::testing::Test { protected: - virtual void SetUp() override { + void SetUp() override { RegisterBuiltins(); RegisterInstallFunctions(); RegisterBlockImageFunctions(); - // Mock the location of last_command_file. - CacheLocation::location().set_cache_temp_source(temp_saved_source_.path); - CacheLocation::location().set_last_command_file(temp_last_command_.path); - CacheLocation::location().set_stash_directory_base(temp_stash_base_.path); + RegisterFunction("blob_to_string", BlobToString); + + // Each test is run in a separate process (isolated mode). Shared temporary files won't cause + // conflicts. + Paths::Get().set_cache_temp_source(temp_saved_source_.path); + Paths::Get().set_last_command_file(temp_last_command_.path); + Paths::Get().set_stash_directory_base(temp_stash_base_.path); + + // Enable a special command "abort" to simulate interruption. + Command::abort_allowed_ = true; + + last_command_file_ = temp_last_command_.path; + image_file_ = image_temp_file_.path; + } + + void TearDown() override { + // Clean up the last_command_file if any. + ASSERT_TRUE(android::base::RemoveFileIfExists(last_command_file_)); + + // Clear partition updated marker if any. + std::string updated_marker{ temp_stash_base_.path }; + updated_marker += "/" + get_sha1(image_temp_file_.path) + ".UPDATED"; + ASSERT_TRUE(android::base::RemoveFileIfExists(updated_marker)); } TemporaryFile temp_saved_source_; - TemporaryFile temp_last_command_; TemporaryDir temp_stash_base_; + std::string last_command_file_; + std::string image_file_; + + private: + TemporaryFile temp_last_command_; + TemporaryFile image_temp_file_; }; TEST_F(UpdaterTest, getprop) { @@ -131,80 +215,47 @@ TEST_F(UpdaterTest, getprop) { expect(nullptr, "getprop(\"arg1\", \"arg2\")", kArgsParsingFailure); } -TEST_F(UpdaterTest, sha1_check) { - // sha1_check(data) returns the SHA-1 of the data. - expect("81fe8bfe87576c3ecb22426f8e57847382917acf", "sha1_check(\"abcd\")", kNoCause); - expect("da39a3ee5e6b4b0d3255bfef95601890afd80709", "sha1_check(\"\")", kNoCause); - - // sha1_check(data, sha1_hex, [sha1_hex, ...]) returns the matched SHA-1. - expect("81fe8bfe87576c3ecb22426f8e57847382917acf", - "sha1_check(\"abcd\", \"81fe8bfe87576c3ecb22426f8e57847382917acf\")", - kNoCause); - - expect("81fe8bfe87576c3ecb22426f8e57847382917acf", - "sha1_check(\"abcd\", \"wrong_sha1\", \"81fe8bfe87576c3ecb22426f8e57847382917acf\")", - kNoCause); - - // Or "" if there's no match. - expect("", - "sha1_check(\"abcd\", \"wrong_sha1\")", - kNoCause); - - expect("", - "sha1_check(\"abcd\", \"wrong_sha1\", \"wrong_sha2\")", - kNoCause); - - // sha1_check() expects at least one argument. - expect(nullptr, "sha1_check()", kArgsParsingFailure); -} - -TEST_F(UpdaterTest, apply_patch_check) { - // Zero-argument is not valid. - expect(nullptr, "apply_patch_check()", kArgsParsingFailure); - - // File not found. - expect("", "apply_patch_check(\"/doesntexist\")", kNoCause); - - std::string src_file = from_testdata_base("old.file"); - std::string src_content; - ASSERT_TRUE(android::base::ReadFileToString(src_file, &src_content)); - size_t src_size = src_content.size(); - std::string src_hash = get_sha1(src_content); - - // One-argument with EMMC:file:size:sha1 should pass the check. - std::string filename = android::base::Join( - std::vector<std::string>{ "EMMC", src_file, std::to_string(src_size), src_hash }, ":"); - std::string cmd = "apply_patch_check(\"" + filename + "\")"; - expect("t", cmd.c_str(), kNoCause); - - // EMMC:file:(size-1):sha1:(size+1):sha1 should fail the check. - std::string filename_bad = android::base::Join( - std::vector<std::string>{ "EMMC", src_file, std::to_string(src_size - 1), src_hash, - std::to_string(src_size + 1), src_hash }, - ":"); - cmd = "apply_patch_check(\"" + filename_bad + "\")"; - expect("", cmd.c_str(), kNoCause); - - // EMMC:file:(size-1):sha1:size:sha1:(size+1):sha1 should pass the check. - filename_bad = - android::base::Join(std::vector<std::string>{ "EMMC", src_file, std::to_string(src_size - 1), - src_hash, std::to_string(src_size), src_hash, - std::to_string(src_size + 1), src_hash }, - ":"); - cmd = "apply_patch_check(\"" + filename_bad + "\")"; - expect("t", cmd.c_str(), kNoCause); - - // Multiple arguments. - cmd = "apply_patch_check(\"" + filename + "\", \"wrong_sha1\", \"wrong_sha2\")"; - expect("", cmd.c_str(), kNoCause); - - cmd = "apply_patch_check(\"" + filename + "\", \"wrong_sha1\", \"" + src_hash + - "\", \"wrong_sha2\")"; - expect("t", cmd.c_str(), kNoCause); - - cmd = "apply_patch_check(\"" + filename_bad + "\", \"wrong_sha1\", \"" + src_hash + - "\", \"wrong_sha2\")"; - expect("t", cmd.c_str(), kNoCause); +TEST_F(UpdaterTest, patch_partition_check) { + // Zero argument is not valid. + expect(nullptr, "patch_partition_check()", kArgsParsingFailure); + + std::string source_file = from_testdata_base("boot.img"); + std::string source_content; + ASSERT_TRUE(android::base::ReadFileToString(source_file, &source_content)); + size_t source_size = source_content.size(); + std::string source_hash = get_sha1(source_content); + Partition source(source_file, source_size, source_hash); + + std::string target_file = from_testdata_base("recovery.img"); + std::string target_content; + ASSERT_TRUE(android::base::ReadFileToString(target_file, &target_content)); + size_t target_size = target_content.size(); + std::string target_hash = get_sha1(target_content); + Partition target(target_file, target_size, target_hash); + + // One argument is not valid. + expect(nullptr, "patch_partition_check(\"" + source.ToString() + "\")", kArgsParsingFailure); + expect(nullptr, "patch_partition_check(\"" + target.ToString() + "\")", kArgsParsingFailure); + + // Both of the source and target have the desired checksum. + std::string cmd = + "patch_partition_check(\"" + source.ToString() + "\", \"" + target.ToString() + "\")"; + expect("t", cmd, kNoCause); + + // Only source partition has the desired checksum. + Partition bad_target(target_file, target_size - 1, target_hash); + cmd = "patch_partition_check(\"" + source.ToString() + "\", \"" + bad_target.ToString() + "\")"; + expect("t", cmd, kNoCause); + + // Only target partition has the desired checksum. + Partition bad_source(source_file, source_size + 1, source_hash); + cmd = "patch_partition_check(\"" + bad_source.ToString() + "\", \"" + target.ToString() + "\")"; + expect("t", cmd, kNoCause); + + // Neither of the source or target has the desired checksum. + cmd = + "patch_partition_check(\"" + bad_source.ToString() + "\", \"" + bad_target.ToString() + "\")"; + expect("", cmd, kNoCause); } TEST_F(UpdaterTest, file_getprop) { @@ -214,7 +265,7 @@ TEST_F(UpdaterTest, file_getprop) { expect(nullptr, "file_getprop(\"arg1\", \"arg2\", \"arg3\")", kArgsParsingFailure); // File doesn't exist. - expect(nullptr, "file_getprop(\"/doesntexist\", \"key1\")", kFileGetPropFailure); + expect(nullptr, "file_getprop(\"/doesntexist\", \"key1\")", kFreadFailure); // Reject too large files (current limit = 65536). TemporaryFile temp_file1; @@ -231,28 +282,28 @@ TEST_F(UpdaterTest, file_getprop) { std::string script1("file_getprop(\"" + std::string(temp_file2.path) + "\", \"ro.product.name\")"); - expect("tardis", script1.c_str(), kNoCause); + expect("tardis", script1, kNoCause); std::string script2("file_getprop(\"" + std::string(temp_file2.path) + "\", \"ro.product.board\")"); - expect("magic", script2.c_str(), kNoCause); + expect("magic", script2, kNoCause); // No match. std::string script3("file_getprop(\"" + std::string(temp_file2.path) + "\", \"ro.product.wrong\")"); - expect("", script3.c_str(), kNoCause); + expect("", script3, kNoCause); std::string script4("file_getprop(\"" + std::string(temp_file2.path) + "\", \"ro.product.name=\")"); - expect("", script4.c_str(), kNoCause); + expect("", script4, kNoCause); std::string script5("file_getprop(\"" + std::string(temp_file2.path) + "\", \"ro.product.nam\")"); - expect("", script5.c_str(), kNoCause); + expect("", script5, kNoCause); std::string script6("file_getprop(\"" + std::string(temp_file2.path) + "\", \"ro.product.model\")"); - expect("", script6.c_str(), kNoCause); + expect("", script6, kNoCause); } // TODO: Test extracting to block device. @@ -272,7 +323,7 @@ TEST_F(UpdaterTest, package_extract_file) { // Two-argument version. TemporaryFile temp_file1; std::string script("package_extract_file(\"a.txt\", \"" + std::string(temp_file1.path) + "\")"); - expect("t", script.c_str(), kNoCause, &updater_info); + expect("t", script, kNoCause, &updater_info); // Verify the extracted entry. std::string data; @@ -281,33 +332,135 @@ TEST_F(UpdaterTest, package_extract_file) { // Now extract another entry to the same location, which should overwrite. script = "package_extract_file(\"b.txt\", \"" + std::string(temp_file1.path) + "\")"; - expect("t", script.c_str(), kNoCause, &updater_info); + expect("t", script, kNoCause, &updater_info); ASSERT_TRUE(android::base::ReadFileToString(temp_file1.path, &data)); ASSERT_EQ(kBTxtContents, data); // Missing zip entry. The two-argument version doesn't abort. script = "package_extract_file(\"doesntexist\", \"" + std::string(temp_file1.path) + "\")"; - expect("", script.c_str(), kNoCause, &updater_info); + expect("", script, kNoCause, &updater_info); // Extract to /dev/full should fail. script = "package_extract_file(\"a.txt\", \"/dev/full\")"; - expect("", script.c_str(), kNoCause, &updater_info); + expect("", script, kNoCause, &updater_info); - // One-argument version. - script = "sha1_check(package_extract_file(\"a.txt\"))"; - expect(kATxtSha1Sum.c_str(), script.c_str(), kNoCause, &updater_info); + // One-argument version. package_extract_file() gives a VAL_BLOB, which needs to be converted to + // VAL_STRING for equality test. + script = "blob_to_string(package_extract_file(\"a.txt\")) == \"" + kATxtContents + "\""; + expect("t", script, kNoCause, &updater_info); - script = "sha1_check(package_extract_file(\"b.txt\"))"; - expect(kBTxtSha1Sum.c_str(), script.c_str(), kNoCause, &updater_info); + script = "blob_to_string(package_extract_file(\"b.txt\")) == \"" + kBTxtContents + "\""; + expect("t", script, kNoCause, &updater_info); // Missing entry. The one-argument version aborts the evaluation. script = "package_extract_file(\"doesntexist\")"; - expect(nullptr, script.c_str(), kPackageExtractFileFailure, &updater_info); + expect(nullptr, script, kPackageExtractFileFailure, &updater_info); CloseArchive(handle); } +TEST_F(UpdaterTest, read_file) { + // read_file() expects one argument. + expect(nullptr, "read_file()", kArgsParsingFailure); + expect(nullptr, "read_file(\"arg1\", \"arg2\")", kArgsParsingFailure); + + // Write some value to file and read back. + TemporaryFile temp_file; + std::string script("write_value(\"foo\", \""s + temp_file.path + "\");"); + expect("t", script, kNoCause); + + script = "read_file(\""s + temp_file.path + "\") == \"foo\""; + expect("t", script, kNoCause); + + script = "read_file(\""s + temp_file.path + "\") == \"bar\""; + expect("", script, kNoCause); + + // It should fail gracefully when read fails. + script = "read_file(\"/doesntexist\")"; + expect("", script, kNoCause); +} + +TEST_F(UpdaterTest, compute_hash_tree_smoke) { + std::string data; + for (unsigned char i = 0; i < 128; i++) { + data += std::string(4096, i); + } + // Appends an additional block for verity data. + data += std::string(4096, 0); + ASSERT_EQ(129 * 4096, data.size()); + ASSERT_TRUE(android::base::WriteStringToFile(data, image_file_)); + + std::string salt = "aee087a5be3b982978c923f566a94613496b417f2af592639bc80d141e34dfe7"; + std::string expected_root_hash = + "7e0a8d8747f54384014ab996f5b2dc4eb7ff00c630eede7134c9e3f05c0dd8ca"; + // hash_tree_ranges, source_ranges, hash_algorithm, salt_hex, root_hash + std::vector<std::string> tokens{ "compute_hash_tree", "2,128,129", "2,0,128", "sha256", salt, + expected_root_hash }; + std::string hash_tree_command = android::base::Join(tokens, " "); + + std::vector<std::string> transfer_list{ + "4", "2", "0", "2", hash_tree_command, + }; + + PackageEntries entries{ + { "new_data", "" }, + { "patch_data", "" }, + { "transfer_list", android::base::Join(transfer_list, "\n") }, + }; + + RunBlockImageUpdate(false, entries, image_file_, "t"); + + std::string updated; + ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated)); + ASSERT_EQ(129 * 4096, updated.size()); + ASSERT_EQ(data.substr(0, 128 * 4096), updated.substr(0, 128 * 4096)); + + // Computes the SHA256 of the salt + hash_tree_data and expects the result to match with the + // root_hash. + std::vector<unsigned char> salt_bytes; + ASSERT_TRUE(HashTreeBuilder::ParseBytesArrayFromString(salt, &salt_bytes)); + std::vector<unsigned char> hash_tree = std::move(salt_bytes); + hash_tree.insert(hash_tree.end(), updated.begin() + 128 * 4096, updated.end()); + + std::vector<unsigned char> digest(SHA256_DIGEST_LENGTH); + SHA256(hash_tree.data(), hash_tree.size(), digest.data()); + ASSERT_EQ(expected_root_hash, HashTreeBuilder::BytesArrayToString(digest)); +} + +TEST_F(UpdaterTest, compute_hash_tree_root_mismatch) { + std::string data; + for (size_t i = 0; i < 128; i++) { + data += std::string(4096, i); + } + // Appends an additional block for verity data. + data += std::string(4096, 0); + ASSERT_EQ(129 * 4096, data.size()); + // Corrupts one bit + data[4096] = 'A'; + ASSERT_TRUE(android::base::WriteStringToFile(data, image_file_)); + + std::string salt = "aee087a5be3b982978c923f566a94613496b417f2af592639bc80d141e34dfe7"; + std::string expected_root_hash = + "7e0a8d8747f54384014ab996f5b2dc4eb7ff00c630eede7134c9e3f05c0dd8ca"; + // hash_tree_ranges, source_ranges, hash_algorithm, salt_hex, root_hash + std::vector<std::string> tokens{ "compute_hash_tree", "2,128,129", "2,0,128", "sha256", salt, + expected_root_hash }; + std::string hash_tree_command = android::base::Join(tokens, " "); + + std::vector<std::string> transfer_list{ + "4", "2", "0", "2", hash_tree_command, + }; + + PackageEntries entries{ + { "new_data", "" }, + { "patch_data", "" }, + { "transfer_list", android::base::Join(transfer_list, "\n") }, + }; + + RunBlockImageUpdate(false, entries, image_file_, "", kHashTreeComputationFailure); +} + TEST_F(UpdaterTest, write_value) { // write_value() expects two arguments. expect(nullptr, "write_value()", kArgsParsingFailure); @@ -321,7 +474,7 @@ TEST_F(UpdaterTest, write_value) { TemporaryFile temp_file; std::string value = "magicvalue"; std::string script("write_value(\"" + value + "\", \"" + std::string(temp_file.path) + "\")"); - expect("t", script.c_str(), kNoCause); + expect("t", script, kNoCause); // Verify the content. std::string content; @@ -330,7 +483,7 @@ TEST_F(UpdaterTest, write_value) { // Allow writing empty string. script = "write_value(\"\", \"" + std::string(temp_file.path) + "\")"; - expect("t", script.c_str(), kNoCause); + expect("t", script, kNoCause); // Verify the content. ASSERT_TRUE(android::base::ReadFileToString(temp_file.path, &content)); @@ -338,7 +491,7 @@ TEST_F(UpdaterTest, write_value) { // It should fail gracefully when write fails. script = "write_value(\"value\", \"/proc/0/file1\")"; - expect("", script.c_str(), kNoCause); + expect("", script, kNoCause); } TEST_F(UpdaterTest, get_stage) { @@ -357,11 +510,11 @@ TEST_F(UpdaterTest, get_stage) { // Can read the stage value. std::string script("get_stage(\"" + temp_file + "\")"); - expect("2/3", script.c_str(), kNoCause); + expect("2/3", script, kNoCause); // Bad BCB path. script = "get_stage(\"doesntexist\")"; - expect("", script.c_str(), kNoCause); + expect("", script, kNoCause); } TEST_F(UpdaterTest, set_stage) { @@ -381,7 +534,7 @@ TEST_F(UpdaterTest, set_stage) { // Write with set_stage(). std::string script("set_stage(\"" + temp_file + "\", \"1/3\")"); - expect(tf.path, script.c_str(), kNoCause); + expect(tf.path, script, kNoCause); // Verify. bootloader_message boot_verify; @@ -393,10 +546,10 @@ TEST_F(UpdaterTest, set_stage) { // Bad BCB path. script = "set_stage(\"doesntexist\", \"1/3\")"; - expect("", script.c_str(), kNoCause); + expect("", script, kNoCause); script = "set_stage(\"/dev/full\", \"1/3\")"; - expect("", script.c_str(), kNoCause); + expect("", script, kNoCause); } TEST_F(UpdaterTest, set_progress) { @@ -448,22 +601,42 @@ TEST_F(UpdaterTest, show_progress) { ASSERT_EQ(0, fclose(updater_info.cmd_pipe)); } +TEST_F(UpdaterTest, block_image_update_parsing_error) { + std::vector<std::string> transfer_list{ + // clang-format off + "4", + "2", + "0", + // clang-format on + }; + + PackageEntries entries{ + { "new_data", "" }, + { "patch_data", "" }, + { "transfer_list", android::base::Join(transfer_list, '\n') }, + }; + + RunBlockImageUpdate(false, entries, image_file_, "", kArgsParsingFailure); +} + TEST_F(UpdaterTest, block_image_update_patch_data) { std::string src_content = std::string(4096, 'a') + std::string(4096, 'c'); std::string tgt_content = std::string(4096, 'b') + std::string(4096, 'd'); // Generate the patch data. TemporaryFile patch_file; - ASSERT_EQ(0, bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(src_content.data()), - src_content.size(), reinterpret_cast<const uint8_t*>(tgt_content.data()), - tgt_content.size(), patch_file.path, nullptr)); + ASSERT_EQ(0, + bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(src_content.data()), src_content.size(), + reinterpret_cast<const uint8_t*>(tgt_content.data()), tgt_content.size(), + patch_file.path, nullptr)); std::string patch_content; ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch_content)); // Create the transfer list that contains a bsdiff. std::string src_hash = get_sha1(src_content); std::string tgt_hash = get_sha1(tgt_content); - std::vector<std::string> transfer_list = { + std::vector<std::string> transfer_list{ + // clang-format off "4", "2", "0", @@ -472,183 +645,108 @@ TEST_F(UpdaterTest, block_image_update_patch_data) { android::base::StringPrintf("bsdiff 0 %zu %s %s 2,0,2 2 - %s:2,0,2", patch_content.size(), src_hash.c_str(), tgt_hash.c_str(), src_hash.c_str()), "free " + src_hash, + // clang-format on }; - std::unordered_map<std::string, std::string> entries = { + PackageEntries entries{ { "new_data", "" }, { "patch_data", patch_content }, { "transfer_list", android::base::Join(transfer_list, '\n') }, }; - // Build the update package. - TemporaryFile zip_file; - BuildUpdatePackage(entries, zip_file.release()); - - MemMapping map; - ASSERT_TRUE(map.MapFile(zip_file.path)); - ZipArchiveHandle handle; - ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle)); + ASSERT_TRUE(android::base::WriteStringToFile(src_content, image_file_)); - // Set up the handler, command_pipe, patch offset & length. - UpdaterInfo updater_info; - updater_info.package_zip = handle; - TemporaryFile temp_pipe; - updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe"); - updater_info.package_zip_addr = map.addr; - updater_info.package_zip_len = map.length; + RunBlockImageUpdate(false, entries, image_file_, "t"); - // Execute the commands in the transfer list. - TemporaryFile update_file; - ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path)); - std::string script = "block_image_update(\"" + std::string(update_file.path) + - R"(", package_extract_file("transfer_list"), "new_data", "patch_data"))"; - expect("t", script.c_str(), kNoCause, &updater_info); // The update_file should be patched correctly. std::string updated_content; - ASSERT_TRUE(android::base::ReadFileToString(update_file.path, &updated_content)); - ASSERT_EQ(tgt_hash, get_sha1(updated_content)); - - ASSERT_EQ(0, fclose(updater_info.cmd_pipe)); - CloseArchive(handle); + ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated_content)); + ASSERT_EQ(tgt_content, updated_content); } TEST_F(UpdaterTest, block_image_update_fail) { std::string src_content(4096 * 2, 'e'); std::string src_hash = get_sha1(src_content); // Stash and free some blocks, then fail the update intentionally. - std::vector<std::string> transfer_list = { - "4", "2", "0", "2", "stash " + src_hash + " 2,0,2", "free " + src_hash, "fail", + std::vector<std::string> transfer_list{ + // clang-format off + "4", + "2", + "0", + "2", + "stash " + src_hash + " 2,0,2", + "free " + src_hash, + "abort", + // clang-format on }; // Add a new data of 10 bytes to test the deadlock. - std::unordered_map<std::string, std::string> entries = { + PackageEntries entries{ { "new_data", std::string(10, 0) }, { "patch_data", "" }, { "transfer_list", android::base::Join(transfer_list, '\n') }, }; - // Build the update package. - TemporaryFile zip_file; - BuildUpdatePackage(entries, zip_file.release()); - - MemMapping map; - ASSERT_TRUE(map.MapFile(zip_file.path)); - ZipArchiveHandle handle; - ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle)); + ASSERT_TRUE(android::base::WriteStringToFile(src_content, image_file_)); - // Set up the handler, command_pipe, patch offset & length. - UpdaterInfo updater_info; - updater_info.package_zip = handle; - TemporaryFile temp_pipe; - updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe"); - updater_info.package_zip_addr = map.addr; - updater_info.package_zip_len = map.length; + RunBlockImageUpdate(false, entries, image_file_, ""); - TemporaryFile update_file; - ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path)); - // Expect the stashed blocks to be freed. - std::string script = "block_image_update(\"" + std::string(update_file.path) + - R"(", package_extract_file("transfer_list"), "new_data", "patch_data"))"; - expect("", script.c_str(), kNoCause, &updater_info); // Updater generates the stash name based on the input file name. - std::string name_digest = get_sha1(update_file.path); + std::string name_digest = get_sha1(image_file_); std::string stash_base = std::string(temp_stash_base_.path) + "/" + name_digest; ASSERT_EQ(0, access(stash_base.c_str(), F_OK)); + // Expect the stashed blocks to be freed. ASSERT_EQ(-1, access((stash_base + src_hash).c_str(), F_OK)); ASSERT_EQ(0, rmdir(stash_base.c_str())); - - ASSERT_EQ(0, fclose(updater_info.cmd_pipe)); - CloseArchive(handle); } TEST_F(UpdaterTest, new_data_over_write) { - std::vector<std::string> transfer_list = { - "4", "1", "0", "0", "new 2,0,1", + std::vector<std::string> transfer_list{ + // clang-format off + "4", + "1", + "0", + "0", + "new 2,0,1", + // clang-format on }; // Write 4096 + 100 bytes of new data. - std::unordered_map<std::string, std::string> entries = { + PackageEntries entries{ { "new_data", std::string(4196, 0) }, { "patch_data", "" }, { "transfer_list", android::base::Join(transfer_list, '\n') }, }; - // Build the update package. - TemporaryFile zip_file; - BuildUpdatePackage(entries, zip_file.release()); - - MemMapping map; - ASSERT_TRUE(map.MapFile(zip_file.path)); - ZipArchiveHandle handle; - ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle)); - - // Set up the handler, command_pipe, patch offset & length. - UpdaterInfo updater_info; - updater_info.package_zip = handle; - TemporaryFile temp_pipe; - updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe"); - updater_info.package_zip_addr = map.addr; - updater_info.package_zip_len = map.length; - - TemporaryFile update_file; - std::string script = "block_image_update(\"" + std::string(update_file.path) + - R"(", package_extract_file("transfer_list"), "new_data", "patch_data"))"; - expect("t", script.c_str(), kNoCause, &updater_info); - - ASSERT_EQ(0, fclose(updater_info.cmd_pipe)); - CloseArchive(handle); + RunBlockImageUpdate(false, entries, image_file_, "t"); } TEST_F(UpdaterTest, new_data_short_write) { - std::vector<std::string> transfer_list = { + std::vector<std::string> transfer_list{ + // clang-format off "4", "1", "0", "0", "new 2,0,1", + // clang-format on }; - std::unordered_map<std::string, std::string> entries = { - { "empty_new_data", "" }, - { "short_new_data", std::string(10, 'a') }, - { "exact_new_data", std::string(4096, 'a') }, + PackageEntries entries{ { "patch_data", "" }, { "transfer_list", android::base::Join(transfer_list, '\n') }, }; - TemporaryFile zip_file; - BuildUpdatePackage(entries, zip_file.release()); - - MemMapping map; - ASSERT_TRUE(map.MapFile(zip_file.path)); - ZipArchiveHandle handle; - ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle)); - - // Set up the handler, command_pipe, patch offset & length. - UpdaterInfo updater_info; - updater_info.package_zip = handle; - TemporaryFile temp_pipe; - updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe"); - updater_info.package_zip_addr = map.addr; - updater_info.package_zip_len = map.length; - // Updater should report the failure gracefully rather than stuck in deadlock. - TemporaryFile update_file; - std::string script_empty_data = "block_image_update(\"" + std::string(update_file.path) + - R"(", package_extract_file("transfer_list"), "empty_new_data", "patch_data"))"; - expect("", script_empty_data.c_str(), kNoCause, &updater_info); + entries["new_data"] = ""; + RunBlockImageUpdate(false, entries, image_file_, ""); - std::string script_short_data = "block_image_update(\"" + std::string(update_file.path) + - R"(", package_extract_file("transfer_list"), "short_new_data", "patch_data"))"; - expect("", script_short_data.c_str(), kNoCause, &updater_info); + entries["new_data"] = std::string(10, 'a'); + RunBlockImageUpdate(false, entries, image_file_, ""); // Expect to write 1 block of new data successfully. - std::string script_exact_data = "block_image_update(\"" + std::string(update_file.path) + - R"(", package_extract_file("transfer_list"), "exact_new_data", "patch_data"))"; - expect("t", script_exact_data.c_str(), kNoCause, &updater_info); - - ASSERT_EQ(0, fclose(updater_info.cmd_pipe)); - CloseArchive(handle); + entries["new_data"] = std::string(4096, 'a'); + RunBlockImageUpdate(false, entries, image_file_, "t"); } TEST_F(UpdaterTest, brotli_new_data) { @@ -681,55 +779,30 @@ TEST_F(UpdaterTest, brotli_new_data) { "new 2,99,100", }; - std::unordered_map<std::string, std::string> entries = { - { "new.dat.br", std::move(encoded_data) }, + PackageEntries entries{ + { "new_data.br", std::move(encoded_data) }, { "patch_data", "" }, { "transfer_list", android::base::Join(transfer_list, '\n') }, }; - TemporaryFile zip_file; - BuildUpdatePackage(entries, zip_file.release()); - - MemMapping map; - ASSERT_TRUE(map.MapFile(zip_file.path)); - ZipArchiveHandle handle; - ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle)); - - // Set up the handler, command_pipe, patch offset & length. - UpdaterInfo updater_info; - updater_info.package_zip = handle; - TemporaryFile temp_pipe; - updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wb"); - updater_info.package_zip_addr = map.addr; - updater_info.package_zip_len = map.length; - - // Check if we can decompress the new data correctly. - TemporaryFile update_file; - std::string script_new_data = - "block_image_update(\"" + std::string(update_file.path) + - R"(", package_extract_file("transfer_list"), "new.dat.br", "patch_data"))"; - expect("t", script_new_data.c_str(), kNoCause, &updater_info); + RunBlockImageUpdate(false, entries, image_file_, "t"); std::string updated_content; - ASSERT_TRUE(android::base::ReadFileToString(update_file.path, &updated_content)); + ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated_content)); ASSERT_EQ(brotli_new_data, updated_content); - - ASSERT_EQ(0, fclose(updater_info.cmd_pipe)); - CloseArchive(handle); } TEST_F(UpdaterTest, last_command_update) { - std::string last_command_file = CacheLocation::location().last_command_file(); - - std::string block1 = std::string(4096, '1'); - std::string block2 = std::string(4096, '2'); - std::string block3 = std::string(4096, '3'); + std::string block1(4096, '1'); + std::string block2(4096, '2'); + std::string block3(4096, '3'); std::string block1_hash = get_sha1(block1); std::string block2_hash = get_sha1(block2); std::string block3_hash = get_sha1(block3); // Compose the transfer list to fail the first update. - std::vector<std::string> transfer_list_fail = { + std::vector<std::string> transfer_list_fail{ + // clang-format off "4", "2", "0", @@ -737,11 +810,13 @@ TEST_F(UpdaterTest, last_command_update) { "stash " + block1_hash + " 2,0,1", "move " + block1_hash + " 2,1,2 1 2,0,1", "stash " + block3_hash + " 2,2,3", - "fail", + "abort", + // clang-format on }; // Mimic a resumed update with the same transfer commands. - std::vector<std::string> transfer_list_continue = { + std::vector<std::string> transfer_list_continue{ + // clang-format off "4", "2", "0", @@ -750,127 +825,88 @@ TEST_F(UpdaterTest, last_command_update) { "move " + block1_hash + " 2,1,2 1 2,0,1", "stash " + block3_hash + " 2,2,3", "move " + block1_hash + " 2,2,3 1 2,0,1", + // clang-format on }; - std::unordered_map<std::string, std::string> entries = { + ASSERT_TRUE(android::base::WriteStringToFile(block1 + block2 + block3, image_file_)); + + PackageEntries entries{ { "new_data", "" }, { "patch_data", "" }, - { "transfer_list_fail", android::base::Join(transfer_list_fail, '\n') }, - { "transfer_list_continue", android::base::Join(transfer_list_continue, '\n') }, + { "transfer_list", android::base::Join(transfer_list_fail, '\n') }, }; - // Build the update package. - TemporaryFile zip_file; - BuildUpdatePackage(entries, zip_file.release()); - - MemMapping map; - ASSERT_TRUE(map.MapFile(zip_file.path)); - ZipArchiveHandle handle; - ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle)); - - // Set up the handler, command_pipe, patch offset & length. - UpdaterInfo updater_info; - updater_info.package_zip = handle; - TemporaryFile temp_pipe; - updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe"); - updater_info.package_zip_addr = map.addr; - updater_info.package_zip_len = map.length; + // "2\nstash " + block3_hash + " 2,2,3" + std::string last_command_content = + "2\n" + transfer_list_fail[TransferList::kTransferListHeaderLines + 2]; - std::string src_content = block1 + block2 + block3; - TemporaryFile update_file; - ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path)); - std::string script = - "block_image_update(\"" + std::string(update_file.path) + - R"(", package_extract_file("transfer_list_fail"), "new_data", "patch_data"))"; - expect("", script.c_str(), kNoCause, &updater_info); + RunBlockImageUpdate(false, entries, image_file_, ""); // Expect last_command to contain the last stash command. - std::string last_command_content; - ASSERT_TRUE(android::base::ReadFileToString(last_command_file.c_str(), &last_command_content)); - EXPECT_EQ("2\nstash " + block3_hash + " 2,2,3", last_command_content); + std::string last_command_actual; + ASSERT_TRUE(android::base::ReadFileToString(last_command_file_, &last_command_actual)); + EXPECT_EQ(last_command_content, last_command_actual); + std::string updated_contents; - ASSERT_TRUE(android::base::ReadFileToString(update_file.path, &updated_contents)); + ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated_contents)); ASSERT_EQ(block1 + block1 + block3, updated_contents); - // Resume the update, expect the first 'move' to be skipped but the second 'move' to be executed. - ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path)); - std::string script_second_update = - "block_image_update(\"" + std::string(update_file.path) + - R"(", package_extract_file("transfer_list_continue"), "new_data", "patch_data"))"; - expect("t", script_second_update.c_str(), kNoCause, &updater_info); - ASSERT_TRUE(android::base::ReadFileToString(update_file.path, &updated_contents)); - ASSERT_EQ(block1 + block2 + block1, updated_contents); + // "Resume" the update. Expect the first 'move' to be skipped but the second 'move' to be + // executed. Note that we intentionally reset the image file. + entries["transfer_list"] = android::base::Join(transfer_list_continue, '\n'); + ASSERT_TRUE(android::base::WriteStringToFile(block1 + block2 + block3, image_file_)); + RunBlockImageUpdate(false, entries, image_file_, "t"); - ASSERT_EQ(0, fclose(updater_info.cmd_pipe)); - CloseArchive(handle); + ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated_contents)); + ASSERT_EQ(block1 + block2 + block1, updated_contents); } TEST_F(UpdaterTest, last_command_update_unresumable) { - std::string last_command_file = CacheLocation::location().last_command_file(); - - std::string block1 = std::string(4096, '1'); - std::string block2 = std::string(4096, '2'); + std::string block1(4096, '1'); + std::string block2(4096, '2'); std::string block1_hash = get_sha1(block1); std::string block2_hash = get_sha1(block2); // Construct an unresumable update with source blocks mismatch. - std::vector<std::string> transfer_list_unresumable = { - "4", "2", "0", "2", "stash " + block1_hash + " 2,0,1", "move " + block2_hash + " 2,1,2 1 2,0,1", + std::vector<std::string> transfer_list_unresumable{ + // clang-format off + "4", + "2", + "0", + "2", + "stash " + block1_hash + " 2,0,1", + "move " + block2_hash + " 2,1,2 1 2,0,1", + // clang-format on }; - std::unordered_map<std::string, std::string> entries = { + PackageEntries entries{ { "new_data", "" }, { "patch_data", "" }, - { "transfer_list_unresumable", android::base::Join(transfer_list_unresumable, '\n') }, + { "transfer_list", android::base::Join(transfer_list_unresumable, '\n') }, }; - // Build the update package. - TemporaryFile zip_file; - BuildUpdatePackage(entries, zip_file.release()); - - MemMapping map; - ASSERT_TRUE(map.MapFile(zip_file.path)); - ZipArchiveHandle handle; - ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle)); + ASSERT_TRUE(android::base::WriteStringToFile(block1 + block1, image_file_)); - // Set up the handler, command_pipe, patch offset & length. - UpdaterInfo updater_info; - updater_info.package_zip = handle; - TemporaryFile temp_pipe; - updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe"); - updater_info.package_zip_addr = map.addr; - updater_info.package_zip_len = map.length; + std::string last_command_content = + "0\n" + transfer_list_unresumable[TransferList::kTransferListHeaderLines]; + ASSERT_TRUE(android::base::WriteStringToFile(last_command_content, last_command_file_)); - // Set up the last_command_file - ASSERT_TRUE( - android::base::WriteStringToFile("0\nstash " + block1_hash + " 2,0,1", last_command_file)); - - // The last_command_file will be deleted if the update encounters an unresumable failure - // later. - std::string src_content = block1 + block1; - TemporaryFile update_file; - ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path)); - std::string script = - "block_image_update(\"" + std::string(update_file.path) + - R"(", package_extract_file("transfer_list_unresumable"), "new_data", "patch_data"))"; - expect("", script.c_str(), kNoCause, &updater_info); - ASSERT_EQ(-1, access(last_command_file.c_str(), R_OK)); + RunBlockImageUpdate(false, entries, image_file_, ""); - ASSERT_EQ(0, fclose(updater_info.cmd_pipe)); - CloseArchive(handle); + // The last_command_file will be deleted if the update encounters an unresumable failure later. + ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK)); } TEST_F(UpdaterTest, last_command_verify) { - std::string last_command_file = CacheLocation::location().last_command_file(); - - std::string block1 = std::string(4096, '1'); - std::string block2 = std::string(4096, '2'); - std::string block3 = std::string(4096, '3'); + std::string block1(4096, '1'); + std::string block2(4096, '2'); + std::string block3(4096, '3'); std::string block1_hash = get_sha1(block1); std::string block2_hash = get_sha1(block2); std::string block3_hash = get_sha1(block3); - std::vector<std::string> transfer_list_verify = { + std::vector<std::string> transfer_list_verify{ + // clang-format off "4", "2", "0", @@ -879,55 +915,265 @@ TEST_F(UpdaterTest, last_command_verify) { "move " + block1_hash + " 2,0,1 1 2,0,1", "move " + block1_hash + " 2,1,2 1 2,0,1", "stash " + block3_hash + " 2,2,3", + // clang-format on }; - std::unordered_map<std::string, std::string> entries = { + PackageEntries entries{ { "new_data", "" }, { "patch_data", "" }, - { "transfer_list_verify", android::base::Join(transfer_list_verify, '\n') }, + { "transfer_list", android::base::Join(transfer_list_verify, '\n') }, }; - // Build the update package. - TemporaryFile zip_file; - BuildUpdatePackage(entries, zip_file.release()); + ASSERT_TRUE(android::base::WriteStringToFile(block1 + block1 + block3, image_file_)); - MemMapping map; - ASSERT_TRUE(map.MapFile(zip_file.path)); - ZipArchiveHandle handle; - ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle)); + // Last command: "move " + block1_hash + " 2,1,2 1 2,0,1" + std::string last_command_content = + "2\n" + transfer_list_verify[TransferList::kTransferListHeaderLines + 2]; - // Set up the handler, command_pipe, patch offset & length. - UpdaterInfo updater_info; - updater_info.package_zip = handle; - TemporaryFile temp_pipe; - updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe"); - updater_info.package_zip_addr = map.addr; - updater_info.package_zip_len = map.length; + // First run: expect the verification to succeed and the last_command_file is intact. + ASSERT_TRUE(android::base::WriteStringToFile(last_command_content, last_command_file_)); + + RunBlockImageUpdate(true, entries, image_file_, "t"); - std::string src_content = block1 + block1 + block3; - TemporaryFile update_file; - ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path)); + std::string last_command_actual; + ASSERT_TRUE(android::base::ReadFileToString(last_command_file_, &last_command_actual)); + EXPECT_EQ(last_command_content, last_command_actual); - ASSERT_TRUE( - android::base::WriteStringToFile("2\nstash " + block3_hash + " 2,2,3", last_command_file)); + // Second run with a mismatching block image: expect the verification to succeed but + // last_command_file to be deleted; because the target blocks in the last command don't have the + // expected contents for the second move command. + ASSERT_TRUE(android::base::WriteStringToFile(block1 + block2 + block3, image_file_)); + RunBlockImageUpdate(true, entries, image_file_, "t"); + ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK)); +} - // Expect the verification to succeed and the last_command_file is intact. - std::string script_verify = - "block_image_verify(\"" + std::string(update_file.path) + - R"(", package_extract_file("transfer_list_verify"), "new_data","patch_data"))"; - expect("t", script_verify.c_str(), kNoCause, &updater_info); +class ResumableUpdaterTest : public testing::TestWithParam<size_t> { + protected: + void SetUp() override { + RegisterBuiltins(); + RegisterInstallFunctions(); + RegisterBlockImageFunctions(); - std::string last_command_content; - ASSERT_TRUE(android::base::ReadFileToString(last_command_file.c_str(), &last_command_content)); - EXPECT_EQ("2\nstash " + block3_hash + " 2,2,3", last_command_content); + Paths::Get().set_cache_temp_source(temp_saved_source_.path); + Paths::Get().set_last_command_file(temp_last_command_.path); + Paths::Get().set_stash_directory_base(temp_stash_base_.path); - // Expect the verification to succeed but last_command_file to be deleted; because the target - // blocks don't have the expected contents for the second move command. - src_content = block1 + block2 + block3; - ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path)); - expect("t", script_verify.c_str(), kNoCause, &updater_info); - ASSERT_EQ(-1, access(last_command_file.c_str(), R_OK)); + // Enable a special command "abort" to simulate interruption. + Command::abort_allowed_ = true; - ASSERT_EQ(0, fclose(updater_info.cmd_pipe)); - CloseArchive(handle); + index_ = GetParam(); + image_file_ = image_temp_file_.path; + last_command_file_ = temp_last_command_.path; + } + + void TearDown() override { + // Clean up the last_command_file if any. + ASSERT_TRUE(android::base::RemoveFileIfExists(last_command_file_)); + + // Clear partition updated marker if any. + std::string updated_marker{ temp_stash_base_.path }; + updated_marker += "/" + get_sha1(image_temp_file_.path) + ".UPDATED"; + ASSERT_TRUE(android::base::RemoveFileIfExists(updated_marker)); + } + + TemporaryFile temp_saved_source_; + TemporaryDir temp_stash_base_; + std::string last_command_file_; + std::string image_file_; + size_t index_; + + private: + TemporaryFile temp_last_command_; + TemporaryFile image_temp_file_; +}; + +static std::string g_source_image; +static std::string g_target_image; +static PackageEntries g_entries; + +static std::vector<std::string> GenerateTransferList() { + std::string a(4096, 'a'); + std::string b(4096, 'b'); + std::string c(4096, 'c'); + std::string d(4096, 'd'); + std::string e(4096, 'e'); + std::string f(4096, 'f'); + std::string g(4096, 'g'); + std::string h(4096, 'h'); + std::string i(4096, 'i'); + std::string zero(4096, '\0'); + + std::string a_hash = get_sha1(a); + std::string b_hash = get_sha1(b); + std::string c_hash = get_sha1(c); + std::string e_hash = get_sha1(e); + + auto loc = [](const std::string& range_text) { + std::vector<std::string> pieces = android::base::Split(range_text, "-"); + size_t left; + size_t right; + if (pieces.size() == 1) { + CHECK(android::base::ParseUint(pieces[0], &left)); + right = left + 1; + } else { + CHECK_EQ(2u, pieces.size()); + CHECK(android::base::ParseUint(pieces[0], &left)); + CHECK(android::base::ParseUint(pieces[1], &right)); + right++; + } + return android::base::StringPrintf("2,%zu,%zu", left, right); + }; + + // patch 1: "b d c" -> "g" + TemporaryFile patch_file_bdc_g; + std::string bdc = b + d + c; + std::string bdc_hash = get_sha1(bdc); + std::string g_hash = get_sha1(g); + CHECK_EQ(0, bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(bdc.data()), bdc.size(), + reinterpret_cast<const uint8_t*>(g.data()), g.size(), + patch_file_bdc_g.path, nullptr)); + std::string patch_bdc_g; + CHECK(android::base::ReadFileToString(patch_file_bdc_g.path, &patch_bdc_g)); + + // patch 2: "a b c d" -> "d c b" + TemporaryFile patch_file_abcd_dcb; + std::string abcd = a + b + c + d; + std::string abcd_hash = get_sha1(abcd); + std::string dcb = d + c + b; + std::string dcb_hash = get_sha1(dcb); + CHECK_EQ(0, bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(abcd.data()), abcd.size(), + reinterpret_cast<const uint8_t*>(dcb.data()), dcb.size(), + patch_file_abcd_dcb.path, nullptr)); + std::string patch_abcd_dcb; + CHECK(android::base::ReadFileToString(patch_file_abcd_dcb.path, &patch_abcd_dcb)); + + std::vector<std::string> transfer_list{ + "4", + "10", // total blocks written + "2", // maximum stash entries + "2", // maximum number of stashed blocks + + // a b c d e a b c d e + "stash " + b_hash + " " + loc("1"), + // a b c d e a b c d e [b(1)] + "stash " + c_hash + " " + loc("2"), + // a b c d e a b c d e [b(1)][c(2)] + "new " + loc("1-2"), + // a i h d e a b c d e [b(1)][c(2)] + "zero " + loc("0"), + // 0 i h d e a b c d e [b(1)][c(2)] + + // bsdiff "b d c" (from stash, 3, stash) to get g(3) + android::base::StringPrintf( + "bsdiff 0 %zu %s %s %s 3 %s %s %s:%s %s:%s", + patch_bdc_g.size(), // patch start (0), patch length + bdc_hash.c_str(), // source hash + g_hash.c_str(), // target hash + loc("3").c_str(), // target range + loc("3").c_str(), loc("1").c_str(), // load "d" from block 3, into buffer at offset 1 + b_hash.c_str(), loc("0").c_str(), // load "b" from stash, into buffer at offset 0 + c_hash.c_str(), loc("2").c_str()), // load "c" from stash, into buffer at offset 2 + + // 0 i h g e a b c d e [b(1)][c(2)] + "free " + b_hash, + // 0 i h g e a b c d e [c(2)] + "free " + a_hash, + // 0 i h g e a b c d e + "stash " + a_hash + " " + loc("5"), + // 0 i h g e a b c d e [a(5)] + "move " + e_hash + " " + loc("5") + " 1 " + loc("4"), + // 0 i h g e e b c d e [a(5)] + + // bsdiff "a b c d" (from stash, 6-8) to "d c b" (6-8) + android::base::StringPrintf( // + "bsdiff %zu %zu %s %s %s 4 %s %s %s:%s", + patch_bdc_g.size(), // patch start + patch_bdc_g.size() + patch_abcd_dcb.size(), // patch length + abcd_hash.c_str(), // source hash + dcb_hash.c_str(), // target hash + loc("6-8").c_str(), // target range + loc("6-8").c_str(), // load "b c d" from blocks 6-8 + loc("1-3").c_str(), // into buffer at offset 1-3 + a_hash.c_str(), // load "a" from stash + loc("0").c_str()), // into buffer at offset 0 + + // 0 i h g e e d c b e [a(5)] + "new " + loc("4"), + // 0 i h g f e d c b e [a(5)] + "move " + a_hash + " " + loc("9") + " 1 - " + a_hash + ":" + loc("0"), + // 0 i h g f e d c b a [a(5)] + "free " + a_hash, + // 0 i h g f e d c b a + }; + + std::string new_data = i + h + f; + std::string patch_data = patch_bdc_g + patch_abcd_dcb; + + g_entries = { + { "new_data", new_data }, + { "patch_data", patch_data }, + }; + g_source_image = a + b + c + d + e + a + b + c + d + e; + g_target_image = zero + i + h + g + f + e + d + c + b + a; + + return transfer_list; +} + +static const std::vector<std::string> g_transfer_list = GenerateTransferList(); + +INSTANTIATE_TEST_CASE_P(InterruptAfterEachCommand, ResumableUpdaterTest, + ::testing::Range(static_cast<size_t>(0), + g_transfer_list.size() - + TransferList::kTransferListHeaderLines)); + +TEST_P(ResumableUpdaterTest, InterruptVerifyResume) { + ASSERT_TRUE(android::base::WriteStringToFile(g_source_image, image_file_)); + + LOG(INFO) << "Interrupting at line " << index_ << " (" + << g_transfer_list[TransferList::kTransferListHeaderLines + index_] << ")"; + + std::vector<std::string> transfer_list_copy{ g_transfer_list }; + transfer_list_copy[TransferList::kTransferListHeaderLines + index_] = "abort"; + + g_entries["transfer_list"] = android::base::Join(transfer_list_copy, '\n'); + + // Run update that's expected to fail. + RunBlockImageUpdate(false, g_entries, image_file_, ""); + + std::string last_command_expected; + + // Assert the last_command_file. + if (index_ == 0) { + ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK)); + } else { + last_command_expected = std::to_string(index_ - 1) + "\n" + + g_transfer_list[TransferList::kTransferListHeaderLines + index_ - 1]; + std::string last_command_actual; + ASSERT_TRUE(android::base::ReadFileToString(last_command_file_, &last_command_actual)); + ASSERT_EQ(last_command_expected, last_command_actual); + } + + g_entries["transfer_list"] = android::base::Join(g_transfer_list, '\n'); + + // Resume the interrupted update, by doing verification first. + RunBlockImageUpdate(true, g_entries, image_file_, "t"); + + // last_command_file should remain intact. + if (index_ == 0) { + ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK)); + } else { + std::string last_command_actual; + ASSERT_TRUE(android::base::ReadFileToString(last_command_file_, &last_command_actual)); + ASSERT_EQ(last_command_expected, last_command_actual); + } + + // Resume the update. + RunBlockImageUpdate(false, g_entries, image_file_, "t"); + + // last_command_file should be gone after successful update. + ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK)); + + std::string updated_image_actual; + ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated_image_actual)); + ASSERT_EQ(g_target_image, updated_image_actual); } diff --git a/tests/component/verifier_test.cpp b/tests/component/verifier_test.cpp index 2ef3828ad..3246ecdbc 100644 --- a/tests/component/verifier_test.cpp +++ b/tests/component/verifier_test.cpp @@ -16,7 +16,6 @@ #include <errno.h> #include <fcntl.h> -#include <gtest/gtest.h> #include <stdio.h> #include <stdlib.h> #include <sys/stat.h> @@ -28,9 +27,10 @@ #include <android-base/file.h> #include <android-base/stringprintf.h> #include <android-base/test_utils.h> +#include <gtest/gtest.h> #include "common/test_constants.h" -#include "otautil/SysUtil.h" +#include "otautil/sysutil.h" #include "verifier.h" using namespace std::string_literals; diff --git a/tests/manual/recovery_test.cpp b/tests/manual/recovery_test.cpp index 64e3b59e6..e1d0771e7 100644 --- a/tests/manual/recovery_test.cpp +++ b/tests/manual/recovery_test.cpp @@ -14,27 +14,22 @@ * limitations under the License. */ -#include <dirent.h> +#include <errno.h> +#include <stdio.h> #include <string.h> #include <sys/types.h> #include <unistd.h> +#include <memory> #include <string> -#include <vector> #include <android-base/file.h> -#include <android-base/strings.h> #include <android/log.h> #include <gtest/gtest.h> -#include <png.h> #include <private/android_logger.h> -#include "minui/minui.h" - -static const std::string myFilename = "/data/misc/recovery/inject.txt"; -static const std::string myContent = "Hello World\nWelcome to my recovery\n"; -static const std::string kLocale = "zu"; -static const std::string kResourceTestDir = "/data/nativetest/recovery/"; +static const std::string kInjectTxtFilename = "/data/misc/recovery/inject.txt"; +static const std::string kInjectTxtContent = "Hello World\nWelcome to my recovery\n"; // Failure is expected on systems that do not deliver either the // recovery-persist or recovery-refresh executables. Tests also require @@ -44,9 +39,9 @@ static ssize_t __pmsg_fn(log_id_t logId, char prio, const char *filename, const char *buf, size_t len, void *arg) { EXPECT_EQ(LOG_ID_SYSTEM, logId); EXPECT_EQ(ANDROID_LOG_INFO, prio); - EXPECT_NE(std::string::npos, myFilename.find(filename)); - EXPECT_EQ(myContent, buf); - EXPECT_EQ(myContent.size(), len); + EXPECT_NE(std::string::npos, kInjectTxtFilename.find(filename)); + EXPECT_EQ(kInjectTxtContent, buf); + EXPECT_EQ(kInjectTxtContent.size(), len); EXPECT_EQ(nullptr, arg); return len; } @@ -59,13 +54,14 @@ TEST(recovery, refresh) { ssize_t ret = __android_log_pmsg_file_read( LOG_ID_SYSTEM, ANDROID_LOG_INFO, "recovery/", __pmsg_fn, nullptr); if (ret == -ENOENT) { - EXPECT_LT(0, __android_log_pmsg_file_write(LOG_ID_SYSTEM, ANDROID_LOG_INFO, - myFilename.c_str(), myContent.c_str(), myContent.size())); + EXPECT_LT(0, __android_log_pmsg_file_write( + LOG_ID_SYSTEM, ANDROID_LOG_INFO, kInjectTxtFilename.c_str(), + kInjectTxtContent.c_str(), kInjectTxtContent.size())); - fprintf(stderr, "injected test data, requires two intervening reboots " - "to check for replication\n"); + fprintf(stderr, + "injected test data, requires two intervening reboots to check for replication\n"); } - EXPECT_EQ(static_cast<ssize_t>(myContent.size()), ret); + EXPECT_EQ(static_cast<ssize_t>(kInjectTxtContent.size()), ret); } // recovery.persist - Requires recovery.inject, then a reboot, then @@ -76,149 +72,18 @@ TEST(recovery, persist) { ssize_t ret = __android_log_pmsg_file_read( LOG_ID_SYSTEM, ANDROID_LOG_INFO, "recovery/", __pmsg_fn, nullptr); if (ret == -ENOENT) { - EXPECT_LT(0, __android_log_pmsg_file_write(LOG_ID_SYSTEM, ANDROID_LOG_INFO, - myFilename.c_str(), myContent.c_str(), myContent.size())); + EXPECT_LT(0, __android_log_pmsg_file_write( + LOG_ID_SYSTEM, ANDROID_LOG_INFO, kInjectTxtFilename.c_str(), + kInjectTxtContent.c_str(), kInjectTxtContent.size())); - fprintf(stderr, "injected test data, requires intervening reboot " - "to check for storage\n"); + fprintf(stderr, "injected test data, requires intervening reboot to check for storage\n"); } std::string buf; - EXPECT_TRUE(android::base::ReadFileToString(myFilename, &buf)); - EXPECT_EQ(myContent, buf); - if (access(myFilename.c_str(), F_OK) == 0) { - fprintf(stderr, "Removing persistent test data, " - "check if reconstructed on reboot\n"); - } - EXPECT_EQ(0, unlink(myFilename.c_str())); -} - -std::vector<std::string> image_dir { - "res-mdpi/images/", - "res-hdpi/images/", - "res-xhdpi/images/", - "res-xxhdpi/images/", - "res-xxxhdpi/images/" -}; - -static int png_filter(const dirent* de) { - if (de->d_type != DT_REG || !android::base::EndsWith(de->d_name, "_text.png")) { - return 0; + EXPECT_TRUE(android::base::ReadFileToString(kInjectTxtFilename, &buf)); + EXPECT_EQ(kInjectTxtContent, buf); + if (access(kInjectTxtFilename.c_str(), F_OK) == 0) { + fprintf(stderr, "Removing persistent test data, check if reconstructed on reboot\n"); } - return 1; + EXPECT_EQ(0, unlink(kInjectTxtFilename.c_str())); } - -// Find out all png files to test under /data/nativetest/recovery/. -static std::vector<std::string> add_files() { - std::vector<std::string> files; - for (const std::string& str : image_dir) { - std::string dir_path = kResourceTestDir + str; - dirent** namelist; - int n = scandir(dir_path.c_str(), &namelist, png_filter, alphasort); - if (n == -1) { - printf("Failed to scan dir %s: %s\n", kResourceTestDir.c_str(), strerror(errno)); - return files; - } - if (n == 0) { - printf("No file is added for test in %s\n", kResourceTestDir.c_str()); - } - - while (n--) { - std::string file_path = dir_path + namelist[n]->d_name; - files.push_back(file_path); - free(namelist[n]); - } - free(namelist); - } - return files; -} - -class ResourceTest : public testing::TestWithParam<std::string> { - public: - static std::vector<std::string> png_list; - - // Parse a png file and test if it's qualified for the background text image - // under recovery. - void SetUp() override { - std::string file_path = GetParam(); - fp = fopen(file_path.c_str(), "rbe"); - ASSERT_NE(nullptr, fp); - - unsigned char header[8]; - size_t bytesRead = fread(header, 1, sizeof(header), fp); - ASSERT_EQ(sizeof(header), bytesRead); - ASSERT_EQ(0, png_sig_cmp(header, 0, sizeof(header))); - - png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); - ASSERT_NE(nullptr, png_ptr); - - info_ptr = png_create_info_struct(png_ptr); - ASSERT_NE(nullptr, info_ptr); - - png_init_io(png_ptr, fp); - png_set_sig_bytes(png_ptr, sizeof(header)); - png_read_info(png_ptr, info_ptr); - - int color_type, bit_depth; - png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type, nullptr, nullptr, - nullptr); - ASSERT_EQ(PNG_COLOR_TYPE_GRAY, color_type) << "Recovery expects grayscale PNG file."; - ASSERT_LT(static_cast<png_uint_32>(5), width); - ASSERT_LT(static_cast<png_uint_32>(0), height); - if (bit_depth <= 8) { - // 1-, 2-, 4-, or 8-bit gray images: expand to 8-bit gray. - png_set_expand_gray_1_2_4_to_8(png_ptr); - } - - png_byte channels = png_get_channels(png_ptr, info_ptr); - ASSERT_EQ(1, channels) << "Recovery background text images expects 1-channel PNG file."; - } - - void TearDown() override { - if (png_ptr != nullptr && info_ptr != nullptr) { - png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); - } - - if (fp != nullptr) { - fclose(fp); - } - } - - protected: - png_structp png_ptr; - png_infop info_ptr; - png_uint_32 width, height; - - FILE* fp; -}; - -std::vector<std::string> ResourceTest::png_list = add_files(); - -TEST_P(ResourceTest, ValidateLocale) { - std::vector<unsigned char> row(width); - for (png_uint_32 y = 0; y < height; ++y) { - png_read_row(png_ptr, row.data(), nullptr); - int w = (row[1] << 8) | row[0]; - int h = (row[3] << 8) | row[2]; - int len = row[4]; - EXPECT_LT(0, w); - EXPECT_LT(0, h); - EXPECT_LT(0, len) << "Locale string should be non-empty."; - EXPECT_NE(0, row[5]) << "Locale string is missing."; - - ASSERT_GT(height, y + 1 + h) << "Locale: " << kLocale << " is not found in the file."; - char* loc = reinterpret_cast<char*>(&row[5]); - if (matches_locale(loc, kLocale.c_str())) { - EXPECT_TRUE(android::base::StartsWith(loc, kLocale)); - break; - } else { - for (int i = 0; i < h; ++i, ++y) { - png_read_row(png_ptr, row.data(), nullptr); - } - } - } -} - -INSTANTIATE_TEST_CASE_P(BackgroundTextValidation, ResourceTest, - ::testing::ValuesIn(ResourceTest::png_list.cbegin(), - ResourceTest::png_list.cend())); diff --git a/tests/testdata/font.png b/tests/testdata/font.png Binary files differnew file mode 100644 index 000000000..d95408a93 --- /dev/null +++ b/tests/testdata/font.png diff --git a/tests/testdata/gzipped_source b/tests/testdata/gzipped_source Binary files differnew file mode 100644 index 000000000..6d425d059 --- /dev/null +++ b/tests/testdata/gzipped_source diff --git a/tests/testdata/gzipped_target b/tests/testdata/gzipped_target Binary files differnew file mode 100644 index 000000000..562126286 --- /dev/null +++ b/tests/testdata/gzipped_target diff --git a/tests/testdata/loop00000.png b/tests/testdata/loop00000.png Binary files differnew file mode 100644 index 000000000..0e11c0100 --- /dev/null +++ b/tests/testdata/loop00000.png diff --git a/tests/testdata/new.file b/tests/testdata/new.file Binary files differdeleted file mode 100644 index cdeb8fd50..000000000 --- a/tests/testdata/new.file +++ /dev/null diff --git a/tests/testdata/old.file b/tests/testdata/old.file Binary files differdeleted file mode 100644 index 166c8732e..000000000 --- a/tests/testdata/old.file +++ /dev/null diff --git a/tests/unit/applypatch_test.cpp b/tests/unit/applypatch_test.cpp new file mode 100644 index 000000000..066f981b4 --- /dev/null +++ b/tests/unit/applypatch_test.cpp @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agree to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <dirent.h> +#include <fcntl.h> +#include <inttypes.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <time.h> +#include <unistd.h> + +#include <algorithm> +#include <memory> +#include <string> +#include <vector> + +#include <android-base/file.h> +#include <android-base/logging.h> +#include <android-base/stringprintf.h> +#include <android-base/test_utils.h> +#include <android-base/unique_fd.h> +#include <gtest/gtest.h> + +#include "applypatch/applypatch.h" +#include "common/test_constants.h" +#include "edify/expr.h" +#include "otautil/paths.h" +#include "otautil/print_sha1.h" + +using namespace std::string_literals; + +class ApplyPatchTest : public ::testing::Test { + protected: + void SetUp() override { + source_file = from_testdata_base("boot.img"); + FileContents boot_fc; + ASSERT_TRUE(LoadFileContents(source_file, &boot_fc)); + source_size = boot_fc.data.size(); + source_sha1 = print_sha1(boot_fc.sha1); + + target_file = from_testdata_base("recovery.img"); + FileContents recovery_fc; + ASSERT_TRUE(LoadFileContents(target_file, &recovery_fc)); + target_size = recovery_fc.data.size(); + target_sha1 = print_sha1(recovery_fc.sha1); + + source_partition = Partition(source_file, source_size, source_sha1); + target_partition = Partition(partition_file.path, target_size, target_sha1); + + srand(time(nullptr)); + bad_sha1_a = android::base::StringPrintf("%040x", rand()); + bad_sha1_b = android::base::StringPrintf("%040x", rand()); + + // Reset the cache backup file. + Paths::Get().set_cache_temp_source(cache_temp_source.path); + } + + void TearDown() override { + ASSERT_TRUE(android::base::RemoveFileIfExists(cache_temp_source.path)); + } + + std::string source_file; + std::string source_sha1; + size_t source_size; + + std::string target_file; + std::string target_sha1; + size_t target_size; + + std::string bad_sha1_a; + std::string bad_sha1_b; + + Partition source_partition; + Partition target_partition; + + private: + TemporaryFile partition_file; + TemporaryFile cache_temp_source; +}; + +TEST_F(ApplyPatchTest, CheckPartition) { + ASSERT_TRUE(CheckPartition(source_partition)); +} + +TEST_F(ApplyPatchTest, CheckPartition_Mismatching) { + ASSERT_FALSE(CheckPartition(Partition(source_file, target_size, target_sha1))); + ASSERT_FALSE(CheckPartition(Partition(source_file, source_size, bad_sha1_a))); + + ASSERT_FALSE(CheckPartition(Partition(source_file, source_size - 1, source_sha1))); + ASSERT_FALSE(CheckPartition(Partition(source_file, source_size + 1, source_sha1))); +} + +TEST_F(ApplyPatchTest, PatchPartitionCheck) { + ASSERT_TRUE(PatchPartitionCheck(target_partition, source_partition)); + + ASSERT_TRUE( + PatchPartitionCheck(Partition(source_file, source_size - 1, source_sha1), source_partition)); + + ASSERT_TRUE( + PatchPartitionCheck(Partition(source_file, source_size + 1, source_sha1), source_partition)); +} + +TEST_F(ApplyPatchTest, PatchPartitionCheck_UseBackup) { + ASSERT_FALSE( + PatchPartitionCheck(target_partition, Partition(target_file, source_size, source_sha1))); + + Paths::Get().set_cache_temp_source(source_file); + ASSERT_TRUE( + PatchPartitionCheck(target_partition, Partition(target_file, source_size, source_sha1))); +} + +TEST_F(ApplyPatchTest, PatchPartitionCheck_UseBackup_BothCorrupted) { + ASSERT_FALSE( + PatchPartitionCheck(target_partition, Partition(target_file, source_size, source_sha1))); + + Paths::Get().set_cache_temp_source(target_file); + ASSERT_FALSE( + PatchPartitionCheck(target_partition, Partition(target_file, source_size, source_sha1))); +} + +TEST_F(ApplyPatchTest, PatchPartition) { + FileContents patch_fc; + ASSERT_TRUE(LoadFileContents(from_testdata_base("recovery-from-boot.p"), &patch_fc)); + Value patch(Value::Type::BLOB, std::string(patch_fc.data.cbegin(), patch_fc.data.cend())); + + FileContents bonus_fc; + ASSERT_TRUE(LoadFileContents(from_testdata_base("bonus.file"), &bonus_fc)); + Value bonus(Value::Type::BLOB, std::string(bonus_fc.data.cbegin(), bonus_fc.data.cend())); + + ASSERT_TRUE(PatchPartition(target_partition, source_partition, patch, &bonus)); +} + +// Tests patching an eMMC target without a separate bonus file (i.e. recovery-from-boot patch has +// everything). +TEST_F(ApplyPatchTest, PatchPartitionWithoutBonusFile) { + FileContents patch_fc; + ASSERT_TRUE(LoadFileContents(from_testdata_base("recovery-from-boot-with-bonus.p"), &patch_fc)); + Value patch(Value::Type::BLOB, std::string(patch_fc.data.cbegin(), patch_fc.data.cend())); + + ASSERT_TRUE(PatchPartition(target_partition, source_partition, patch, nullptr)); +} + +class FreeCacheTest : public ::testing::Test { + protected: + static constexpr size_t PARTITION_SIZE = 4096 * 10; + + // Returns a sorted list of files in |dirname|. + static std::vector<std::string> FindFilesInDir(const std::string& dirname) { + std::vector<std::string> file_list; + + std::unique_ptr<DIR, decltype(&closedir)> d(opendir(dirname.c_str()), closedir); + struct dirent* de; + while ((de = readdir(d.get())) != 0) { + std::string path = dirname + "/" + de->d_name; + + struct stat st; + if (stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode)) { + file_list.emplace_back(de->d_name); + } + } + + std::sort(file_list.begin(), file_list.end()); + return file_list; + } + + void AddFilesToDir(const std::string& dir, const std::vector<std::string>& files) { + std::string zeros(4096, 0); + for (const auto& file : files) { + temporary_files_.push_back(dir + "/" + file); + ASSERT_TRUE(android::base::WriteStringToFile(zeros, temporary_files_.back())); + } + } + + void SetUp() override { + Paths::Get().set_cache_log_directory(mock_log_dir.path); + temporary_files_.clear(); + } + + void TearDown() override { + for (const auto& file : temporary_files_) { + ASSERT_TRUE(android::base::RemoveFileIfExists(file)); + } + } + + // A mock method to calculate the free space. It assumes the partition has a total size of 40960 + // bytes and all files are 4096 bytes in size. + static size_t MockFreeSpaceChecker(const std::string& dirname) { + std::vector<std::string> files = FindFilesInDir(dirname); + return PARTITION_SIZE - 4096 * files.size(); + } + + TemporaryDir mock_cache; + TemporaryDir mock_log_dir; + + private: + std::vector<std::string> temporary_files_; +}; + +TEST_F(FreeCacheTest, FreeCacheSmoke) { + std::vector<std::string> files = { "file1", "file2", "file3" }; + AddFilesToDir(mock_cache.path, files); + ASSERT_EQ(files, FindFilesInDir(mock_cache.path)); + ASSERT_EQ(4096 * 7, MockFreeSpaceChecker(mock_cache.path)); + + ASSERT_TRUE(RemoveFilesInDirectory(4096 * 9, mock_cache.path, MockFreeSpaceChecker)); + + ASSERT_EQ(std::vector<std::string>{ "file3" }, FindFilesInDir(mock_cache.path)); + ASSERT_EQ(4096 * 9, MockFreeSpaceChecker(mock_cache.path)); +} + +TEST_F(FreeCacheTest, FreeCacheFreeSpaceCheckerError) { + std::vector<std::string> files{ "file1", "file2", "file3" }; + AddFilesToDir(mock_cache.path, files); + ASSERT_EQ(files, FindFilesInDir(mock_cache.path)); + ASSERT_EQ(4096 * 7, MockFreeSpaceChecker(mock_cache.path)); + + ASSERT_FALSE( + RemoveFilesInDirectory(4096 * 9, mock_cache.path, [](const std::string&) { return -1; })); +} + +TEST_F(FreeCacheTest, FreeCacheOpenFile) { + std::vector<std::string> files = { "file1", "file2" }; + AddFilesToDir(mock_cache.path, files); + ASSERT_EQ(files, FindFilesInDir(mock_cache.path)); + ASSERT_EQ(4096 * 8, MockFreeSpaceChecker(mock_cache.path)); + + std::string file1_path = mock_cache.path + "/file1"s; + android::base::unique_fd fd(open(file1_path.c_str(), O_RDONLY)); + + // file1 can't be deleted as it's opened by us. + ASSERT_FALSE(RemoveFilesInDirectory(4096 * 10, mock_cache.path, MockFreeSpaceChecker)); + + ASSERT_EQ(std::vector<std::string>{ "file1" }, FindFilesInDir(mock_cache.path)); +} + +TEST_F(FreeCacheTest, FreeCacheLogsSmoke) { + std::vector<std::string> log_files = { "last_log", "last_log.1", "last_kmsg.2", "last_log.5", + "last_log.10" }; + AddFilesToDir(mock_log_dir.path, log_files); + ASSERT_EQ(4096 * 5, MockFreeSpaceChecker(mock_log_dir.path)); + + ASSERT_TRUE(RemoveFilesInDirectory(4096 * 8, mock_log_dir.path, MockFreeSpaceChecker)); + + // Logs with a higher index will be deleted first + std::vector<std::string> expected = { "last_log", "last_log.1" }; + ASSERT_EQ(expected, FindFilesInDir(mock_log_dir.path)); + ASSERT_EQ(4096 * 8, MockFreeSpaceChecker(mock_log_dir.path)); +} + +TEST_F(FreeCacheTest, FreeCacheLogsStringComparison) { + std::vector<std::string> log_files = { "last_log.1", "last_kmsg.1", "last_log.not_number", + "last_kmsgrandom" }; + AddFilesToDir(mock_log_dir.path, log_files); + ASSERT_EQ(4096 * 6, MockFreeSpaceChecker(mock_log_dir.path)); + + ASSERT_TRUE(RemoveFilesInDirectory(4096 * 9, mock_log_dir.path, MockFreeSpaceChecker)); + + // Logs with incorrect format will be deleted first; and the last_kmsg with the same index is + // deleted before last_log. + std::vector<std::string> expected = { "last_log.1" }; + ASSERT_EQ(expected, FindFilesInDir(mock_log_dir.path)); + ASSERT_EQ(4096 * 9, MockFreeSpaceChecker(mock_log_dir.path)); +} + +TEST_F(FreeCacheTest, FreeCacheLogsOtherFiles) { + std::vector<std::string> log_files = { "last_install", "command", "block.map", "last_log", + "last_kmsg.1" }; + AddFilesToDir(mock_log_dir.path, log_files); + ASSERT_EQ(4096 * 5, MockFreeSpaceChecker(mock_log_dir.path)); + + ASSERT_FALSE(RemoveFilesInDirectory(4096 * 8, mock_log_dir.path, MockFreeSpaceChecker)); + + // Non log files in /cache/recovery won't be deleted. + std::vector<std::string> expected = { "block.map", "command", "last_install" }; + ASSERT_EQ(expected, FindFilesInDir(mock_log_dir.path)); +} diff --git a/tests/unit/commands_test.cpp b/tests/unit/commands_test.cpp new file mode 100644 index 000000000..8a54df703 --- /dev/null +++ b/tests/unit/commands_test.cpp @@ -0,0 +1,554 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <algorithm> +#include <string> + +#include <android-base/strings.h> +#include <gtest/gtest.h> +#include <openssl/sha.h> + +#include "otautil/print_sha1.h" +#include "otautil/rangeset.h" +#include "private/commands.h" + +TEST(CommandsTest, ParseType) { + ASSERT_EQ(Command::Type::ZERO, Command::ParseType("zero")); + ASSERT_EQ(Command::Type::NEW, Command::ParseType("new")); + ASSERT_EQ(Command::Type::ERASE, Command::ParseType("erase")); + ASSERT_EQ(Command::Type::MOVE, Command::ParseType("move")); + ASSERT_EQ(Command::Type::BSDIFF, Command::ParseType("bsdiff")); + ASSERT_EQ(Command::Type::IMGDIFF, Command::ParseType("imgdiff")); + ASSERT_EQ(Command::Type::STASH, Command::ParseType("stash")); + ASSERT_EQ(Command::Type::FREE, Command::ParseType("free")); + ASSERT_EQ(Command::Type::COMPUTE_HASH_TREE, Command::ParseType("compute_hash_tree")); +} + +TEST(CommandsTest, ParseType_InvalidCommand) { + ASSERT_EQ(Command::Type::LAST, Command::ParseType("foo")); + ASSERT_EQ(Command::Type::LAST, Command::ParseType("bar")); +} + +TEST(CommandsTest, ParseTargetInfoAndSourceInfo_SourceBlocksOnly) { + const std::vector<std::string> tokens{ + "4,569884,569904,591946,592043", + "117", + "4,566779,566799,591946,592043", + }; + TargetInfo target; + SourceInfo source; + std::string err; + ASSERT_TRUE(Command::ParseTargetInfoAndSourceInfo( + tokens, "1d74d1a60332fd38cf9405f1bae67917888da6cb", &target, + "1d74d1a60332fd38cf9405f1bae67917888da6cb", &source, &err)); + ASSERT_EQ(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb", + RangeSet({ { 569884, 569904 }, { 591946, 592043 } })), + target); + ASSERT_EQ(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb", + RangeSet({ { 566779, 566799 }, { 591946, 592043 } }), {}, {}), + source); + ASSERT_EQ(117, source.blocks()); +} + +TEST(CommandsTest, ParseTargetInfoAndSourceInfo_StashesOnly) { + const std::vector<std::string> tokens{ + "2,350729,350731", + "2", + "-", + "6ebcf8cf1f6be0bc49e7d4a864214251925d1d15:2,0,2", + }; + TargetInfo target; + SourceInfo source; + std::string err; + ASSERT_TRUE(Command::ParseTargetInfoAndSourceInfo( + tokens, "6ebcf8cf1f6be0bc49e7d4a864214251925d1d15", &target, + "1c25ba04d3278d6b65a1b9f17abac78425ec8b8d", &source, &err)); + ASSERT_EQ( + TargetInfo("6ebcf8cf1f6be0bc49e7d4a864214251925d1d15", RangeSet({ { 350729, 350731 } })), + target); + ASSERT_EQ( + SourceInfo("1c25ba04d3278d6b65a1b9f17abac78425ec8b8d", {}, {}, + { + StashInfo("6ebcf8cf1f6be0bc49e7d4a864214251925d1d15", RangeSet({ { 0, 2 } })), + }), + source); + ASSERT_EQ(2, source.blocks()); +} + +TEST(CommandsTest, ParseTargetInfoAndSourceInfo_SourceBlocksAndStashes) { + const std::vector<std::string> tokens{ + "4,611641,611643,636981,637075", + "96", + "4,636981,637075,770665,770666", + "4,0,94,95,96", + "9eedf00d11061549e32503cadf054ec6fbfa7a23:2,94,95", + }; + TargetInfo target; + SourceInfo source; + std::string err; + ASSERT_TRUE(Command::ParseTargetInfoAndSourceInfo( + tokens, "4734d1b241eb3d0f993714aaf7d665fae43772b6", &target, + "a6cbdf3f416960f02189d3a814ec7e9e95c44a0d", &source, &err)); + ASSERT_EQ(TargetInfo("4734d1b241eb3d0f993714aaf7d665fae43772b6", + RangeSet({ { 611641, 611643 }, { 636981, 637075 } })), + target); + ASSERT_EQ(SourceInfo( + "a6cbdf3f416960f02189d3a814ec7e9e95c44a0d", + RangeSet({ { 636981, 637075 }, { 770665, 770666 } }), // source ranges + RangeSet({ { 0, 94 }, { 95, 96 } }), // source location + { + StashInfo("9eedf00d11061549e32503cadf054ec6fbfa7a23", RangeSet({ { 94, 95 } })), + }), + source); + ASSERT_EQ(96, source.blocks()); +} + +TEST(CommandsTest, ParseTargetInfoAndSourceInfo_InvalidInput) { + const std::vector<std::string> tokens{ + "4,611641,611643,636981,637075", + "96", + "4,636981,637075,770665,770666", + "4,0,94,95,96", + "9eedf00d11061549e32503cadf054ec6fbfa7a23:2,94,95", + }; + TargetInfo target; + SourceInfo source; + std::string err; + + // Mismatching block count. + { + std::vector<std::string> tokens_copy(tokens); + tokens_copy[1] = "97"; + ASSERT_FALSE(Command::ParseTargetInfoAndSourceInfo( + tokens_copy, "1d74d1a60332fd38cf9405f1bae67917888da6cb", &target, + "1d74d1a60332fd38cf9405f1bae67917888da6cb", &source, &err)); + } + + // Excess stashes (causing block count mismatch). + { + std::vector<std::string> tokens_copy(tokens); + tokens_copy.push_back("e145a2f83a33334714ac65e34969c1f115e54a6f:2,0,22"); + ASSERT_FALSE(Command::ParseTargetInfoAndSourceInfo( + tokens_copy, "1d74d1a60332fd38cf9405f1bae67917888da6cb", &target, + "1d74d1a60332fd38cf9405f1bae67917888da6cb", &source, &err)); + } + + // Invalid args. + for (size_t i = 0; i < tokens.size(); i++) { + TargetInfo target; + SourceInfo source; + std::string err; + ASSERT_FALSE(Command::ParseTargetInfoAndSourceInfo( + std::vector<std::string>(tokens.cbegin() + i + 1, tokens.cend()), + "1d74d1a60332fd38cf9405f1bae67917888da6cb", &target, + "1d74d1a60332fd38cf9405f1bae67917888da6cb", &source, &err)); + } +} + +TEST(CommandsTest, Parse_EmptyInput) { + std::string err; + ASSERT_FALSE(Command::Parse("", 0, &err)); + ASSERT_EQ("invalid type", err); +} + +TEST(CommandsTest, Parse_ABORT_Allowed) { + Command::abort_allowed_ = true; + + const std::string input{ "abort" }; + std::string err; + Command command = Command::Parse(input, 0, &err); + ASSERT_TRUE(command); + + ASSERT_EQ(TargetInfo(), command.target()); + ASSERT_EQ(SourceInfo(), command.source()); + ASSERT_EQ(StashInfo(), command.stash()); + ASSERT_EQ(PatchInfo(), command.patch()); +} + +TEST(CommandsTest, Parse_ABORT_NotAllowed) { + const std::string input{ "abort" }; + std::string err; + Command command = Command::Parse(input, 0, &err); + ASSERT_FALSE(command); +} + +TEST(CommandsTest, Parse_BSDIFF) { + const std::string input{ + "bsdiff 0 148 " + "f201a4e04bd3860da6ad47b957ef424d58a58f8c 9d5d223b4bc5c45dbd25a799c4f1a98466731599 " + "4,565704,565752,566779,566799 " + "68 4,64525,64545,565704,565752" + }; + std::string err; + Command command = Command::Parse(input, 1, &err); + ASSERT_TRUE(command); + + ASSERT_EQ(Command::Type::BSDIFF, command.type()); + ASSERT_EQ(1, command.index()); + ASSERT_EQ(input, command.cmdline()); + + ASSERT_EQ(TargetInfo("9d5d223b4bc5c45dbd25a799c4f1a98466731599", + RangeSet({ { 565704, 565752 }, { 566779, 566799 } })), + command.target()); + ASSERT_EQ(SourceInfo("f201a4e04bd3860da6ad47b957ef424d58a58f8c", + RangeSet({ { 64525, 64545 }, { 565704, 565752 } }), RangeSet(), {}), + command.source()); + ASSERT_EQ(StashInfo(), command.stash()); + ASSERT_EQ(PatchInfo(0, 148), command.patch()); +} + +TEST(CommandsTest, Parse_ERASE) { + const std::string input{ "erase 2,5,10" }; + std::string err; + Command command = Command::Parse(input, 2, &err); + ASSERT_TRUE(command); + + ASSERT_EQ(Command::Type::ERASE, command.type()); + ASSERT_EQ(2, command.index()); + ASSERT_EQ(input, command.cmdline()); + + ASSERT_EQ(TargetInfo("unknown-hash", RangeSet({ { 5, 10 } })), command.target()); + ASSERT_EQ(SourceInfo(), command.source()); + ASSERT_EQ(StashInfo(), command.stash()); + ASSERT_EQ(PatchInfo(), command.patch()); +} + +TEST(CommandsTest, Parse_FREE) { + const std::string input{ "free hash1" }; + std::string err; + Command command = Command::Parse(input, 3, &err); + ASSERT_TRUE(command); + + ASSERT_EQ(Command::Type::FREE, command.type()); + ASSERT_EQ(3, command.index()); + ASSERT_EQ(input, command.cmdline()); + + ASSERT_EQ(TargetInfo(), command.target()); + ASSERT_EQ(SourceInfo(), command.source()); + ASSERT_EQ(StashInfo("hash1", RangeSet()), command.stash()); + ASSERT_EQ(PatchInfo(), command.patch()); +} + +TEST(CommandsTest, Parse_IMGDIFF) { + const std::string input{ + "imgdiff 29629269 185 " + "a6b1c49aed1b57a2aab1ec3e1505b945540cd8db 51978f65035f584a8ef7afa941dacb6d5e862164 " + "2,90851,90852 " + "1 2,90851,90852" + }; + std::string err; + Command command = Command::Parse(input, 4, &err); + ASSERT_TRUE(command); + + ASSERT_EQ(Command::Type::IMGDIFF, command.type()); + ASSERT_EQ(4, command.index()); + ASSERT_EQ(input, command.cmdline()); + + ASSERT_EQ(TargetInfo("51978f65035f584a8ef7afa941dacb6d5e862164", RangeSet({ { 90851, 90852 } })), + command.target()); + ASSERT_EQ(SourceInfo("a6b1c49aed1b57a2aab1ec3e1505b945540cd8db", RangeSet({ { 90851, 90852 } }), + RangeSet(), {}), + command.source()); + ASSERT_EQ(StashInfo(), command.stash()); + ASSERT_EQ(PatchInfo(29629269, 185), command.patch()); +} + +TEST(CommandsTest, Parse_MOVE) { + const std::string input{ + "move 1d74d1a60332fd38cf9405f1bae67917888da6cb " + "4,569884,569904,591946,592043 117 4,566779,566799,591946,592043" + }; + std::string err; + Command command = Command::Parse(input, 5, &err); + ASSERT_TRUE(command); + + ASSERT_EQ(Command::Type::MOVE, command.type()); + ASSERT_EQ(5, command.index()); + ASSERT_EQ(input, command.cmdline()); + + ASSERT_EQ(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb", + RangeSet({ { 569884, 569904 }, { 591946, 592043 } })), + command.target()); + ASSERT_EQ(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb", + RangeSet({ { 566779, 566799 }, { 591946, 592043 } }), RangeSet(), {}), + command.source()); + ASSERT_EQ(StashInfo(), command.stash()); + ASSERT_EQ(PatchInfo(), command.patch()); +} + +TEST(CommandsTest, Parse_NEW) { + const std::string input{ "new 4,3,5,10,12" }; + std::string err; + Command command = Command::Parse(input, 6, &err); + ASSERT_TRUE(command); + + ASSERT_EQ(Command::Type::NEW, command.type()); + ASSERT_EQ(6, command.index()); + ASSERT_EQ(input, command.cmdline()); + + ASSERT_EQ(TargetInfo("unknown-hash", RangeSet({ { 3, 5 }, { 10, 12 } })), command.target()); + ASSERT_EQ(SourceInfo(), command.source()); + ASSERT_EQ(StashInfo(), command.stash()); + ASSERT_EQ(PatchInfo(), command.patch()); +} + +TEST(CommandsTest, Parse_STASH) { + const std::string input{ "stash hash1 2,5,10" }; + std::string err; + Command command = Command::Parse(input, 7, &err); + ASSERT_TRUE(command); + + ASSERT_EQ(Command::Type::STASH, command.type()); + ASSERT_EQ(7, command.index()); + ASSERT_EQ(input, command.cmdline()); + + ASSERT_EQ(TargetInfo(), command.target()); + ASSERT_EQ(SourceInfo(), command.source()); + ASSERT_EQ(StashInfo("hash1", RangeSet({ { 5, 10 } })), command.stash()); + ASSERT_EQ(PatchInfo(), command.patch()); +} + +TEST(CommandsTest, Parse_ZERO) { + const std::string input{ "zero 2,1,5" }; + std::string err; + Command command = Command::Parse(input, 8, &err); + ASSERT_TRUE(command); + + ASSERT_EQ(Command::Type::ZERO, command.type()); + ASSERT_EQ(8, command.index()); + ASSERT_EQ(input, command.cmdline()); + + ASSERT_EQ(TargetInfo("unknown-hash", RangeSet({ { 1, 5 } })), command.target()); + ASSERT_EQ(SourceInfo(), command.source()); + ASSERT_EQ(StashInfo(), command.stash()); + ASSERT_EQ(PatchInfo(), command.patch()); +} + +TEST(CommandsTest, Parse_COMPUTE_HASH_TREE) { + const std::string input{ "compute_hash_tree 2,0,1 2,3,4 sha1 unknown-salt unknown-root-hash" }; + std::string err; + Command command = Command::Parse(input, 9, &err); + ASSERT_TRUE(command); + + ASSERT_EQ(Command::Type::COMPUTE_HASH_TREE, command.type()); + ASSERT_EQ(9, command.index()); + ASSERT_EQ(input, command.cmdline()); + + HashTreeInfo expected_info(RangeSet({ { 0, 1 } }), RangeSet({ { 3, 4 } }), "sha1", "unknown-salt", + "unknown-root-hash"); + ASSERT_EQ(expected_info, command.hash_tree_info()); + ASSERT_EQ(TargetInfo(), command.target()); + ASSERT_EQ(SourceInfo(), command.source()); + ASSERT_EQ(StashInfo(), command.stash()); + ASSERT_EQ(PatchInfo(), command.patch()); +} + +TEST(CommandsTest, Parse_InvalidNumberOfArgs) { + Command::abort_allowed_ = true; + + // Note that the case of having excess args in BSDIFF, IMGDIFF and MOVE is covered by + // ParseTargetInfoAndSourceInfo_InvalidInput. + std::vector<std::string> inputs{ + "abort foo", + "bsdiff", + "compute_hash_tree, 2,0,1 2,0,1 unknown-algorithm unknown-salt", + "erase", + "erase 4,3,5,10,12 hash1", + "free", + "free id1 id2", + "imgdiff", + "move", + "new", + "new 4,3,5,10,12 hash1", + "stash", + "stash id1", + "stash id1 4,3,5,10,12 id2", + "zero", + "zero 4,3,5,10,12 hash2", + }; + for (const auto& input : inputs) { + std::string err; + ASSERT_FALSE(Command::Parse(input, 0, &err)); + } +} + +TEST(SourceInfoTest, Overlaps) { + ASSERT_TRUE(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb", + RangeSet({ { 7, 9 }, { 16, 20 } }), {}, {}) + .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb", + RangeSet({ { 7, 9 }, { 16, 20 } })))); + + ASSERT_TRUE(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb", + RangeSet({ { 7, 9 }, { 16, 20 } }), {}, {}) + .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb", + RangeSet({ { 4, 7 }, { 16, 23 } })))); + + ASSERT_FALSE(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb", + RangeSet({ { 7, 9 }, { 16, 20 } }), {}, {}) + .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb", + RangeSet({ { 9, 16 } })))); +} + +TEST(SourceInfoTest, Overlaps_EmptySourceOrTarget) { + ASSERT_FALSE(SourceInfo().Overlaps(TargetInfo())); + + ASSERT_FALSE(SourceInfo().Overlaps( + TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb", RangeSet({ { 7, 9 }, { 16, 20 } })))); + + ASSERT_FALSE(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb", + RangeSet({ { 7, 9 }, { 16, 20 } }), {}, {}) + .Overlaps(TargetInfo())); +} + +TEST(SourceInfoTest, Overlaps_WithStashes) { + ASSERT_FALSE(SourceInfo("a6cbdf3f416960f02189d3a814ec7e9e95c44a0d", + RangeSet({ { 81, 175 }, { 265, 266 } }), // source ranges + RangeSet({ { 0, 94 }, { 95, 96 } }), // source location + { StashInfo("9eedf00d11061549e32503cadf054ec6fbfa7a23", + RangeSet({ { 94, 95 } })) }) + .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb", + RangeSet({ { 175, 265 } })))); + + ASSERT_TRUE(SourceInfo("a6cbdf3f416960f02189d3a814ec7e9e95c44a0d", + RangeSet({ { 81, 175 }, { 265, 266 } }), // source ranges + RangeSet({ { 0, 94 }, { 95, 96 } }), // source location + { StashInfo("9eedf00d11061549e32503cadf054ec6fbfa7a23", + RangeSet({ { 94, 95 } })) }) + .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb", + RangeSet({ { 265, 266 } })))); +} + +// The block size should be specified by the caller of ReadAll (i.e. from Command instance during +// normal run). +constexpr size_t kBlockSize = 4096; + +TEST(SourceInfoTest, ReadAll) { + // "2727756cfee3fbfe24bf5650123fd7743d7b3465" is the SHA-1 hex digest of 8192 * 'a'. + const SourceInfo source("2727756cfee3fbfe24bf5650123fd7743d7b3465", RangeSet({ { 0, 2 } }), {}, + {}); + auto block_reader = [](const RangeSet& src, std::vector<uint8_t>* block_buffer) -> int { + std::fill_n(block_buffer->begin(), src.blocks() * kBlockSize, 'a'); + return 0; + }; + auto stash_reader = [](const std::string&, std::vector<uint8_t>*) -> int { return 0; }; + std::vector<uint8_t> buffer(source.blocks() * kBlockSize); + ASSERT_TRUE(source.ReadAll(&buffer, kBlockSize, block_reader, stash_reader)); + ASSERT_EQ(source.blocks() * kBlockSize, buffer.size()); + + uint8_t digest[SHA_DIGEST_LENGTH]; + SHA1(buffer.data(), buffer.size(), digest); + ASSERT_EQ(source.hash(), print_sha1(digest)); +} + +TEST(SourceInfoTest, ReadAll_WithStashes) { + const SourceInfo source( + // SHA-1 hex digest of 8192 * 'a' + 4096 * 'b'. + "ee3ebea26130769c10ad13604712100346d48660", RangeSet({ { 0, 2 } }), RangeSet({ { 0, 2 } }), + { StashInfo("1e41f7a59e80c6eb4dc043caae80d273f130bed8", RangeSet({ { 2, 3 } })) }); + auto block_reader = [](const RangeSet& src, std::vector<uint8_t>* block_buffer) -> int { + std::fill_n(block_buffer->begin(), src.blocks() * kBlockSize, 'a'); + return 0; + }; + auto stash_reader = [](const std::string&, std::vector<uint8_t>* stash_buffer) -> int { + std::fill_n(stash_buffer->begin(), kBlockSize, 'b'); + return 0; + }; + std::vector<uint8_t> buffer(source.blocks() * kBlockSize); + ASSERT_TRUE(source.ReadAll(&buffer, kBlockSize, block_reader, stash_reader)); + ASSERT_EQ(source.blocks() * kBlockSize, buffer.size()); + + uint8_t digest[SHA_DIGEST_LENGTH]; + SHA1(buffer.data(), buffer.size(), digest); + ASSERT_EQ(source.hash(), print_sha1(digest)); +} + +TEST(SourceInfoTest, ReadAll_BufferTooSmall) { + const SourceInfo source("2727756cfee3fbfe24bf5650123fd7743d7b3465", RangeSet({ { 0, 2 } }), {}, + {}); + auto block_reader = [](const RangeSet&, std::vector<uint8_t>*) -> int { return 0; }; + auto stash_reader = [](const std::string&, std::vector<uint8_t>*) -> int { return 0; }; + std::vector<uint8_t> buffer(source.blocks() * kBlockSize - 1); + ASSERT_FALSE(source.ReadAll(&buffer, kBlockSize, block_reader, stash_reader)); +} + +TEST(SourceInfoTest, ReadAll_FailingReader) { + const SourceInfo source( + "ee3ebea26130769c10ad13604712100346d48660", RangeSet({ { 0, 2 } }), RangeSet({ { 0, 2 } }), + { StashInfo("1e41f7a59e80c6eb4dc043caae80d273f130bed8", RangeSet({ { 2, 3 } })) }); + std::vector<uint8_t> buffer(source.blocks() * kBlockSize); + auto failing_block_reader = [](const RangeSet&, std::vector<uint8_t>*) -> int { return -1; }; + auto stash_reader = [](const std::string&, std::vector<uint8_t>*) -> int { return 0; }; + ASSERT_FALSE(source.ReadAll(&buffer, kBlockSize, failing_block_reader, stash_reader)); + + auto block_reader = [](const RangeSet&, std::vector<uint8_t>*) -> int { return 0; }; + auto failing_stash_reader = [](const std::string&, std::vector<uint8_t>*) -> int { return -1; }; + ASSERT_FALSE(source.ReadAll(&buffer, kBlockSize, block_reader, failing_stash_reader)); +} + +TEST(TransferListTest, Parse) { + std::vector<std::string> input_lines{ + "4", // version + "2", // total blocks + "1", // max stashed entries + "1", // max stashed blocks + "stash 1d74d1a60332fd38cf9405f1bae67917888da6cb 2,0,1", + "move 1d74d1a60332fd38cf9405f1bae67917888da6cb 2,0,1 1 2,0,1", + }; + + std::string err; + TransferList transfer_list = TransferList::Parse(android::base::Join(input_lines, '\n'), &err); + ASSERT_TRUE(static_cast<bool>(transfer_list)); + ASSERT_EQ(4, transfer_list.version()); + ASSERT_EQ(2, transfer_list.total_blocks()); + ASSERT_EQ(1, transfer_list.stash_max_entries()); + ASSERT_EQ(1, transfer_list.stash_max_blocks()); + ASSERT_EQ(2U, transfer_list.commands().size()); + ASSERT_EQ(Command::Type::STASH, transfer_list.commands()[0].type()); + ASSERT_EQ(Command::Type::MOVE, transfer_list.commands()[1].type()); +} + +TEST(TransferListTest, Parse_InvalidCommand) { + std::vector<std::string> input_lines{ + "4", // version + "2", // total blocks + "1", // max stashed entries + "1", // max stashed blocks + "stash 1d74d1a60332fd38cf9405f1bae67917888da6cb 2,0,1", + "move 1d74d1a60332fd38cf9405f1bae67917888da6cb 2,0,1 1", + }; + + std::string err; + TransferList transfer_list = TransferList::Parse(android::base::Join(input_lines, '\n'), &err); + ASSERT_FALSE(static_cast<bool>(transfer_list)); +} + +TEST(TransferListTest, Parse_ZeroTotalBlocks) { + std::vector<std::string> input_lines{ + "4", // version + "0", // total blocks + "0", // max stashed entries + "0", // max stashed blocks + }; + + std::string err; + TransferList transfer_list = TransferList::Parse(android::base::Join(input_lines, '\n'), &err); + ASSERT_TRUE(static_cast<bool>(transfer_list)); + ASSERT_EQ(4, transfer_list.version()); + ASSERT_EQ(0, transfer_list.total_blocks()); + ASSERT_EQ(0, transfer_list.stash_max_entries()); + ASSERT_EQ(0, transfer_list.stash_max_blocks()); + ASSERT_TRUE(transfer_list.commands().empty()); +} diff --git a/tests/unit/dirutil_test.cpp b/tests/unit/dirutil_test.cpp index 7f85d13ea..1ca786c28 100644 --- a/tests/unit/dirutil_test.cpp +++ b/tests/unit/dirutil_test.cpp @@ -22,7 +22,8 @@ #include <android-base/test_utils.h> #include <gtest/gtest.h> -#include <otautil/DirUtil.h> + +#include "otautil/dirutil.h" TEST(DirUtilTest, create_invalid) { // Requesting to create an empty dir is invalid. diff --git a/tests/unit/parse_install_logs_test.cpp b/tests/unit/parse_install_logs_test.cpp new file mode 100644 index 000000000..8061f3be1 --- /dev/null +++ b/tests/unit/parse_install_logs_test.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <map> +#include <string> +#include <vector> + +#include <android-base/file.h> +#include <android-base/strings.h> +#include <android-base/test_utils.h> +#include <gtest/gtest.h> + +#include "otautil/parse_install_logs.h" + +TEST(ParseInstallLogsTest, EmptyFile) { + TemporaryFile last_install; + + auto metrics = ParseLastInstall(last_install.path); + ASSERT_TRUE(metrics.empty()); +} + +TEST(ParseInstallLogsTest, SideloadSmoke) { + TemporaryFile last_install; + ASSERT_TRUE(android::base::WriteStringToFile("/cache/recovery/ota.zip\n0\n", last_install.path)); + auto metrics = ParseLastInstall(last_install.path); + ASSERT_EQ(metrics.end(), metrics.find("ota_sideload")); + + ASSERT_TRUE(android::base::WriteStringToFile("/sideload/package.zip\n0\n", last_install.path)); + metrics = ParseLastInstall(last_install.path); + ASSERT_NE(metrics.end(), metrics.find("ota_sideload")); +} + +TEST(ParseInstallLogsTest, ParseRecoveryUpdateMetrics) { + std::vector<std::string> lines = { + "/sideload/package.zip", + "0", + "time_total: 300", + "uncrypt_time: 40", + "source_build: 4973410", + "bytes_written_system: " + std::to_string(1200 * 1024 * 1024), + "bytes_stashed_system: " + std::to_string(300 * 1024 * 1024), + "bytes_written_vendor: " + std::to_string(40 * 1024 * 1024), + "bytes_stashed_vendor: " + std::to_string(50 * 1024 * 1024), + "temperature_start: 37000", + "temperature_end: 38000", + "temperature_max: 39000", + "error: 22", + "cause: 55", + }; + + auto metrics = ParseRecoveryUpdateMetrics(lines); + + std::map<std::string, int64_t> expected_result = { + { "ota_time_total", 300 }, { "ota_uncrypt_time", 40 }, + { "ota_source_version", 4973410 }, { "ota_written_in_MiBs", 1240 }, + { "ota_stashed_in_MiBs", 350 }, { "ota_temperature_start", 37000 }, + { "ota_temperature_end", 38000 }, { "ota_temperature_max", 39000 }, + { "ota_non_ab_error_code", 22 }, { "ota_non_ab_cause_code", 55 }, + }; + + ASSERT_EQ(expected_result, metrics); +} diff --git a/tests/unit/rangeset_test.cpp b/tests/unit/rangeset_test.cpp index 7ae193e18..fc72f2f6d 100644 --- a/tests/unit/rangeset_test.cpp +++ b/tests/unit/rangeset_test.cpp @@ -209,6 +209,7 @@ TEST(RangeSetTest, GetBlockNumber) { ASSERT_EQ(static_cast<size_t>(6), rs.GetBlockNumber(5)); ASSERT_EQ(static_cast<size_t>(9), rs.GetBlockNumber(8)); + ::testing::FLAGS_gtest_death_test_style = "threadsafe"; // Out of bound. ASSERT_EXIT(rs.GetBlockNumber(9), ::testing::KilledBySignal(SIGABRT), ""); } @@ -284,6 +285,8 @@ TEST(SortedRangeSetTest, file_range) { ASSERT_EQ(static_cast<size_t>(10), rs.GetOffsetInRangeSet(4106)); ASSERT_EQ(static_cast<size_t>(40970), rs.GetOffsetInRangeSet(4096 * 16 + 10)); + + ::testing::FLAGS_gtest_death_test_style = "threadsafe"; // block#10 not in range. ASSERT_EXIT(rs.GetOffsetInRangeSet(40970), ::testing::KilledBySignal(SIGABRT), ""); } diff --git a/tests/unit/screen_ui_test.cpp b/tests/unit/screen_ui_test.cpp new file mode 100644 index 000000000..7d97a006b --- /dev/null +++ b/tests/unit/screen_ui_test.cpp @@ -0,0 +1,494 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <stddef.h> +#include <stdio.h> + +#include <functional> +#include <map> +#include <memory> +#include <string> +#include <vector> + +#include <android-base/logging.h> +#include <android-base/stringprintf.h> +#include <android-base/test_utils.h> +#include <gtest/gtest.h> + +#include "common/test_constants.h" +#include "device.h" +#include "minui/minui.h" +#include "otautil/paths.h" +#include "private/resources.h" +#include "screen_ui.h" + +static const std::vector<std::string> HEADERS{ "header" }; +static const std::vector<std::string> ITEMS{ "item1", "item2", "item3", "item4", "1234567890" }; + +TEST(ScreenUITest, StartPhoneMenuSmoke) { + Menu menu(false, 10, 20, HEADERS, ITEMS, 0); + ASSERT_FALSE(menu.scrollable()); + ASSERT_EQ(HEADERS[0], menu.text_headers()[0]); + ASSERT_EQ(5u, menu.ItemsCount()); + + std::string message; + ASSERT_FALSE(menu.ItemsOverflow(&message)); + for (size_t i = 0; i < menu.ItemsCount(); i++) { + ASSERT_EQ(ITEMS[i], menu.TextItem(i)); + } + + ASSERT_EQ(0, menu.selection()); +} + +TEST(ScreenUITest, StartWearMenuSmoke) { + Menu menu(true, 10, 8, HEADERS, ITEMS, 1); + ASSERT_TRUE(menu.scrollable()); + ASSERT_EQ(HEADERS[0], menu.text_headers()[0]); + ASSERT_EQ(5u, menu.ItemsCount()); + + std::string message; + ASSERT_FALSE(menu.ItemsOverflow(&message)); + for (size_t i = 0; i < menu.ItemsCount() - 1; i++) { + ASSERT_EQ(ITEMS[i], menu.TextItem(i)); + } + // Test of the last item is truncated + ASSERT_EQ("12345678", menu.TextItem(4)); + ASSERT_EQ(1, menu.selection()); +} + +TEST(ScreenUITest, StartPhoneMenuItemsOverflow) { + Menu menu(false, 1, 20, HEADERS, ITEMS, 0); + ASSERT_FALSE(menu.scrollable()); + ASSERT_EQ(1u, menu.ItemsCount()); + + std::string message; + ASSERT_FALSE(menu.ItemsOverflow(&message)); + for (size_t i = 0; i < menu.ItemsCount(); i++) { + ASSERT_EQ(ITEMS[i], menu.TextItem(i)); + } + + ASSERT_EQ(0u, menu.MenuStart()); + ASSERT_EQ(1u, menu.MenuEnd()); +} + +TEST(ScreenUITest, StartWearMenuItemsOverflow) { + Menu menu(true, 1, 20, HEADERS, ITEMS, 0); + ASSERT_TRUE(menu.scrollable()); + ASSERT_EQ(5u, menu.ItemsCount()); + + std::string message; + ASSERT_TRUE(menu.ItemsOverflow(&message)); + ASSERT_EQ("Current item: 1/5", message); + + for (size_t i = 0; i < menu.ItemsCount(); i++) { + ASSERT_EQ(ITEMS[i], menu.TextItem(i)); + } + + ASSERT_EQ(0u, menu.MenuStart()); + ASSERT_EQ(1u, menu.MenuEnd()); +} + +TEST(ScreenUITest, PhoneMenuSelectSmoke) { + int sel = 0; + Menu menu(false, 10, 20, HEADERS, ITEMS, sel); + // Mimic down button 10 times (2 * items size) + for (int i = 0; i < 10; i++) { + sel = menu.Select(++sel); + ASSERT_EQ(sel, menu.selection()); + + // Wraps the selection for unscrollable menu when it reaches the boundary. + int expected = (i + 1) % 5; + ASSERT_EQ(expected, menu.selection()); + + ASSERT_EQ(0u, menu.MenuStart()); + ASSERT_EQ(5u, menu.MenuEnd()); + } + + // Mimic up button 10 times + for (int i = 0; i < 10; i++) { + sel = menu.Select(--sel); + ASSERT_EQ(sel, menu.selection()); + + int expected = (9 - i) % 5; + ASSERT_EQ(expected, menu.selection()); + + ASSERT_EQ(0u, menu.MenuStart()); + ASSERT_EQ(5u, menu.MenuEnd()); + } +} + +TEST(ScreenUITest, WearMenuSelectSmoke) { + int sel = 0; + Menu menu(true, 10, 20, HEADERS, ITEMS, sel); + // Mimic pressing down button 10 times (2 * items size) + for (int i = 0; i < 10; i++) { + sel = menu.Select(++sel); + ASSERT_EQ(sel, menu.selection()); + + // Stops the selection at the boundary if the menu is scrollable. + int expected = std::min(i + 1, 4); + ASSERT_EQ(expected, menu.selection()); + + ASSERT_EQ(0u, menu.MenuStart()); + ASSERT_EQ(5u, menu.MenuEnd()); + } + + // Mimic pressing up button 10 times + for (int i = 0; i < 10; i++) { + sel = menu.Select(--sel); + ASSERT_EQ(sel, menu.selection()); + + int expected = std::max(3 - i, 0); + ASSERT_EQ(expected, menu.selection()); + + ASSERT_EQ(0u, menu.MenuStart()); + ASSERT_EQ(5u, menu.MenuEnd()); + } +} + +TEST(ScreenUITest, WearMenuSelectItemsOverflow) { + int sel = 1; + Menu menu(true, 3, 20, HEADERS, ITEMS, sel); + ASSERT_EQ(5u, menu.ItemsCount()); + + // Scroll the menu to the end, and check the start & end of menu. + for (int i = 0; i < 3; i++) { + sel = menu.Select(++sel); + ASSERT_EQ(i + 2, sel); + ASSERT_EQ(static_cast<size_t>(i), menu.MenuStart()); + ASSERT_EQ(static_cast<size_t>(i + 3), menu.MenuEnd()); + } + + // Press down button one more time won't change the MenuStart() and MenuEnd(). + sel = menu.Select(++sel); + ASSERT_EQ(4, sel); + ASSERT_EQ(2u, menu.MenuStart()); + ASSERT_EQ(5u, menu.MenuEnd()); + + // Scroll the menu to the top. + // The expected menu sel, start & ends are: + // sel 3, start 2, end 5 + // sel 2, start 2, end 5 + // sel 1, start 1, end 4 + // sel 0, start 0, end 3 + for (int i = 0; i < 4; i++) { + sel = menu.Select(--sel); + ASSERT_EQ(3 - i, sel); + ASSERT_EQ(static_cast<size_t>(std::min(3 - i, 2)), menu.MenuStart()); + ASSERT_EQ(static_cast<size_t>(std::min(6 - i, 5)), menu.MenuEnd()); + } + + // Press up button one more time won't change the MenuStart() and MenuEnd(). + sel = menu.Select(--sel); + ASSERT_EQ(0, sel); + ASSERT_EQ(0u, menu.MenuStart()); + ASSERT_EQ(3u, menu.MenuEnd()); +} + +static constexpr int kMagicAction = 101; + +enum class KeyCode : int { + TIMEOUT = -1, + NO_OP = 0, + UP = 1, + DOWN = 2, + ENTER = 3, + MAGIC = 1001, + LAST, +}; + +static const std::map<KeyCode, int> kKeyMapping{ + // clang-format off + { KeyCode::NO_OP, Device::kNoAction }, + { KeyCode::UP, Device::kHighlightUp }, + { KeyCode::DOWN, Device::kHighlightDown }, + { KeyCode::ENTER, Device::kInvokeItem }, + { KeyCode::MAGIC, kMagicAction }, + // clang-format on +}; + +class TestableScreenRecoveryUI : public ScreenRecoveryUI { + public: + int WaitKey() override; + + void SetKeyBuffer(const std::vector<KeyCode>& buffer); + + int KeyHandler(int key, bool visible) const; + + // The following functions expose the protected members for test purpose. + void RunLoadAnimation() { + LoadAnimation(); + } + + size_t GetLoopFrames() const { + return loop_frames; + } + + size_t GetIntroFrames() const { + return intro_frames; + } + + bool GetRtlLocale() const { + return rtl_locale_; + } + + private: + std::vector<KeyCode> key_buffer_; + size_t key_buffer_index_; +}; + +void TestableScreenRecoveryUI::SetKeyBuffer(const std::vector<KeyCode>& buffer) { + key_buffer_ = buffer; + key_buffer_index_ = 0; +} + +int TestableScreenRecoveryUI::KeyHandler(int key, bool) const { + KeyCode key_code = static_cast<KeyCode>(key); + if (kKeyMapping.find(key_code) != kKeyMapping.end()) { + return kKeyMapping.at(key_code); + } + return Device::kNoAction; +} + +int TestableScreenRecoveryUI::WaitKey() { + if (IsKeyInterrupted()) { + return static_cast<int>(RecoveryUI::KeyError::INTERRUPTED); + } + + CHECK_LT(key_buffer_index_, key_buffer_.size()); + return static_cast<int>(key_buffer_[key_buffer_index_++]); +} + +class ScreenRecoveryUITest : public ::testing::Test { + protected: + const std::string kTestLocale = "en-US"; + const std::string kTestRtlLocale = "ar"; + const std::string kTestRtlLocaleWithSuffix = "ar-EG"; + + void SetUp() override { + has_graphics_ = gr_init() == 0; + gr_exit(); + + if (has_graphics_) { + ui_ = std::make_unique<TestableScreenRecoveryUI>(); + } + + testdata_dir_ = from_testdata_base(""); + Paths::Get().set_resource_dir(testdata_dir_); + res_set_resource_dir(testdata_dir_); + } + + bool has_graphics_; + std::unique_ptr<TestableScreenRecoveryUI> ui_; + std::string testdata_dir_; +}; + +#define RETURN_IF_NO_GRAPHICS \ + do { \ + if (!has_graphics_) { \ + GTEST_LOG_(INFO) << "Test skipped due to no available graphics device"; \ + return; \ + } \ + } while (false) + +TEST_F(ScreenRecoveryUITest, Init) { + RETURN_IF_NO_GRAPHICS; + + ASSERT_TRUE(ui_->Init(kTestLocale)); + ASSERT_EQ(kTestLocale, ui_->GetLocale()); + ASSERT_FALSE(ui_->GetRtlLocale()); + ASSERT_FALSE(ui_->IsTextVisible()); + ASSERT_FALSE(ui_->WasTextEverVisible()); +} + +TEST_F(ScreenRecoveryUITest, dtor_NotCallingInit) { + ui_.reset(); + ASSERT_FALSE(ui_); +} + +TEST_F(ScreenRecoveryUITest, ShowText) { + RETURN_IF_NO_GRAPHICS; + + ASSERT_TRUE(ui_->Init(kTestLocale)); + ASSERT_FALSE(ui_->IsTextVisible()); + ui_->ShowText(true); + ASSERT_TRUE(ui_->IsTextVisible()); + ASSERT_TRUE(ui_->WasTextEverVisible()); + + ui_->ShowText(false); + ASSERT_FALSE(ui_->IsTextVisible()); + ASSERT_TRUE(ui_->WasTextEverVisible()); +} + +TEST_F(ScreenRecoveryUITest, RtlLocale) { + RETURN_IF_NO_GRAPHICS; + + ASSERT_TRUE(ui_->Init(kTestRtlLocale)); + ASSERT_TRUE(ui_->GetRtlLocale()); +} + +TEST_F(ScreenRecoveryUITest, RtlLocaleWithSuffix) { + RETURN_IF_NO_GRAPHICS; + + ASSERT_TRUE(ui_->Init(kTestRtlLocaleWithSuffix)); + ASSERT_TRUE(ui_->GetRtlLocale()); +} + +TEST_F(ScreenRecoveryUITest, ShowMenu) { + RETURN_IF_NO_GRAPHICS; + + ASSERT_TRUE(ui_->Init(kTestLocale)); + ui_->SetKeyBuffer({ + KeyCode::UP, + KeyCode::DOWN, + KeyCode::UP, + KeyCode::DOWN, + KeyCode::ENTER, + }); + ASSERT_EQ(3u, ui_->ShowMenu(HEADERS, ITEMS, 3, true, + std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(), + std::placeholders::_1, std::placeholders::_2))); + + ui_->SetKeyBuffer({ + KeyCode::UP, + KeyCode::UP, + KeyCode::NO_OP, + KeyCode::NO_OP, + KeyCode::UP, + KeyCode::ENTER, + }); + ASSERT_EQ(2u, ui_->ShowMenu(HEADERS, ITEMS, 0, true, + std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(), + std::placeholders::_1, std::placeholders::_2))); +} + +TEST_F(ScreenRecoveryUITest, ShowMenu_NotMenuOnly) { + RETURN_IF_NO_GRAPHICS; + + ASSERT_TRUE(ui_->Init(kTestLocale)); + ui_->SetKeyBuffer({ + KeyCode::MAGIC, + }); + ASSERT_EQ(static_cast<size_t>(kMagicAction), + ui_->ShowMenu(HEADERS, ITEMS, 3, false, + std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(), + std::placeholders::_1, std::placeholders::_2))); +} + +TEST_F(ScreenRecoveryUITest, ShowMenu_TimedOut) { + RETURN_IF_NO_GRAPHICS; + + ASSERT_TRUE(ui_->Init(kTestLocale)); + ui_->SetKeyBuffer({ + KeyCode::TIMEOUT, + }); + ASSERT_EQ(static_cast<size_t>(RecoveryUI::KeyError::TIMED_OUT), + ui_->ShowMenu(HEADERS, ITEMS, 3, true, nullptr)); +} + +TEST_F(ScreenRecoveryUITest, ShowMenu_TimedOut_TextWasEverVisible) { + RETURN_IF_NO_GRAPHICS; + + ASSERT_TRUE(ui_->Init(kTestLocale)); + ui_->ShowText(true); + ui_->ShowText(false); + ASSERT_TRUE(ui_->WasTextEverVisible()); + + ui_->SetKeyBuffer({ + KeyCode::TIMEOUT, + KeyCode::DOWN, + KeyCode::ENTER, + }); + ASSERT_EQ(4u, ui_->ShowMenu(HEADERS, ITEMS, 3, true, + std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(), + std::placeholders::_1, std::placeholders::_2))); +} + +TEST_F(ScreenRecoveryUITest, ShowMenuWithInterrupt) { + RETURN_IF_NO_GRAPHICS; + + ASSERT_TRUE(ui_->Init(kTestLocale)); + ui_->SetKeyBuffer({ + KeyCode::UP, + KeyCode::DOWN, + KeyCode::UP, + KeyCode::DOWN, + KeyCode::ENTER, + }); + + ui_->InterruptKey(); + ASSERT_EQ(static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED), + ui_->ShowMenu(HEADERS, ITEMS, 3, true, + std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(), + std::placeholders::_1, std::placeholders::_2))); + + ui_->SetKeyBuffer({ + KeyCode::UP, + KeyCode::UP, + KeyCode::NO_OP, + KeyCode::NO_OP, + KeyCode::UP, + KeyCode::ENTER, + }); + ASSERT_EQ(static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED), + ui_->ShowMenu(HEADERS, ITEMS, 0, true, + std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(), + std::placeholders::_1, std::placeholders::_2))); +} + +TEST_F(ScreenRecoveryUITest, LoadAnimation) { + RETURN_IF_NO_GRAPHICS; + + ASSERT_TRUE(ui_->Init(kTestLocale)); + // Make a few copies of loop00000.png from testdata. + std::string image_data; + ASSERT_TRUE(android::base::ReadFileToString(testdata_dir_ + "/loop00000.png", &image_data)); + + std::vector<std::string> tempfiles; + TemporaryDir resource_dir; + for (const auto& name : { "00002", "00100", "00050" }) { + tempfiles.push_back(android::base::StringPrintf("%s/loop%s.png", resource_dir.path, name)); + ASSERT_TRUE(android::base::WriteStringToFile(image_data, tempfiles.back())); + } + for (const auto& name : { "00", "01" }) { + tempfiles.push_back(android::base::StringPrintf("%s/intro%s.png", resource_dir.path, name)); + ASSERT_TRUE(android::base::WriteStringToFile(image_data, tempfiles.back())); + } + Paths::Get().set_resource_dir(resource_dir.path); + + ui_->RunLoadAnimation(); + + ASSERT_EQ(2u, ui_->GetIntroFrames()); + ASSERT_EQ(3u, ui_->GetLoopFrames()); + + for (const auto& name : tempfiles) { + ASSERT_EQ(0, unlink(name.c_str())); + } +} + +TEST_F(ScreenRecoveryUITest, LoadAnimation_MissingAnimation) { + RETURN_IF_NO_GRAPHICS; + + ASSERT_TRUE(ui_->Init(kTestLocale)); + // We need a dir that doesn't contain any animation. However, using TemporaryDir will give + // leftovers since this is a death test where TemporaryDir::~TemporaryDir() won't be called. + Paths::Get().set_resource_dir("/proc/self"); + + ::testing::FLAGS_gtest_death_test_style = "threadsafe"; + ASSERT_EXIT(ui_->RunLoadAnimation(), ::testing::KilledBySignal(SIGABRT), ""); +} + +#undef RETURN_IF_NO_GRAPHICS diff --git a/tests/unit/sysutil_test.cpp b/tests/unit/sysutil_test.cpp index 434ee25bf..de8ff7065 100644 --- a/tests/unit/sysutil_test.cpp +++ b/tests/unit/sysutil_test.cpp @@ -14,14 +14,13 @@ * limitations under the License. */ -#include <gtest/gtest.h> - #include <string> #include <android-base/file.h> #include <android-base/test_utils.h> +#include <gtest/gtest.h> -#include "otautil/SysUtil.h" +#include "otautil/sysutil.h" TEST(SysUtilTest, InvalidArgs) { MemMapping mapping; @@ -128,3 +127,13 @@ TEST(SysUtilTest, MapFileBlockMapInvalidBlockMap) { ASSERT_TRUE(android::base::WriteStringToFile("/doesntexist\n4096 4096\n1\n0 1\n", temp_file.path)); ASSERT_FALSE(mapping.MapFile(filename)); } + +TEST(SysUtilTest, StringVectorToNullTerminatedArray) { + std::vector<std::string> args{ "foo", "bar", "baz" }; + auto args_with_nullptr = StringVectorToNullTerminatedArray(args); + ASSERT_EQ(4, args_with_nullptr.size()); + ASSERT_STREQ("foo", args_with_nullptr[0]); + ASSERT_STREQ("bar", args_with_nullptr[1]); + ASSERT_STREQ("baz", args_with_nullptr[2]); + ASSERT_EQ(nullptr, args_with_nullptr[3]); +} diff --git a/tests/unit/zip_test.cpp b/tests/unit/zip_test.cpp index 827668521..47f33d9ea 100644 --- a/tests/unit/zip_test.cpp +++ b/tests/unit/zip_test.cpp @@ -23,10 +23,10 @@ #include <android-base/file.h> #include <android-base/test_utils.h> #include <gtest/gtest.h> -#include <otautil/SysUtil.h> #include <ziparchive/zip_archive.h> #include "common/test_constants.h" +#include "otautil/sysutil.h" TEST(ZipTest, OpenFromMemory) { std::string zip_path = from_testdata_base("ziptest_dummy-update.zip"); |