// Copyright 2022 The Centipede Authors. // // 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 // // https://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 "./centipede/util.h" #include #include #include // NOLINT #include #include #include #include "gmock/gmock.h" #include "gtest/gtest.h" #include "absl/container/flat_hash_map.h" #include "absl/log/log.h" #include "./centipede/feature.h" #include "./centipede/thread_pool.h" #include "./common/defs.h" #include "./common/hash.h" namespace fuzztest::internal { TEST(UtilTest, AsString) { EXPECT_EQ(AsPrintableString({'a', 'b', 'c'}, 3), "abc"); EXPECT_EQ(AsPrintableString({'a', 'b', 'C'}, 4), "abC"); EXPECT_EQ(AsPrintableString({'a', 'b', 'c'}, 2), "ab"); // NOTE: Test both int (0xAB) and char ('\xAB') literals as ByteArray // initializers: the latter used to cause compilation failures with // Bazel/Clang default setup (without --cxxopt=--fno-signed-char in .bazelrc). EXPECT_EQ(AsPrintableString({'a', 0xAB, 0xCD}, 3), "a\\xAB\\xCD"); EXPECT_EQ(AsPrintableString({'a', 0xAB, 0xCD}, 4), "a\\xAB\\xCD"); EXPECT_EQ(AsPrintableString({'a', '\xAB', '\xCD'}, 2), "a\\xAB"); EXPECT_EQ(AsPrintableString({'a', '\xAB', '\xCD', 'z'}, 5), "a\\xAB\\xCDz"); } TEST(UtilTest, ExtractHashFromArray) { const ByteArray a{1, 2, 3, 4}; const ByteArray b{100, 111, 122, 133, 145}; auto hash1 = Hash({4, 5, 6}); auto hash2 = Hash({7, 8}); ByteArray a1 = a; AppendHashToArray(a1, hash1); EXPECT_EQ(a1.size(), a.size() + hash1.size()); ByteArray b2 = b; AppendHashToArray(b2, hash2); EXPECT_EQ(b2.size(), b.size() + hash2.size()); EXPECT_EQ(ExtractHashFromArray(b2), hash2); EXPECT_EQ(b2, b); EXPECT_EQ(ExtractHashFromArray(a1), hash1); EXPECT_EQ(a1, a); } TEST(UtilTest, PackAndUnpackFeatures) { const ByteArray kData{1, 2, 3, 4}; const FeatureVec kFeatures = {102, 30, 7, 15}; std::string hash = Hash(kData); ByteArray packed = PackFeaturesAndHash(kData, kFeatures); FeatureVec unpacked_features; std::string unpacked_hash = UnpackFeaturesAndHash(packed, &unpacked_features); EXPECT_EQ(kFeatures, unpacked_features); EXPECT_EQ(hash, unpacked_hash); } TEST(UtilTest, PackAndUnpackEmptyFeatures) { const ByteArray kData{1, 2, 3, 4}; std::string hash = Hash(kData); ByteArray packed = PackFeaturesAndHash(kData, {}); FeatureVec unpacked_features; std::string unpacked_hash = UnpackFeaturesAndHash(packed, &unpacked_features); EXPECT_TRUE(unpacked_features.empty()); EXPECT_EQ(hash, unpacked_hash); } TEST(UtilTest, PackAndUnpackFeaturesAsRawBytes) { const ByteArray kData{1, 2, 3, 4}; std::string hash = Hash(kData); const FeatureVec kFeatures = {102, 30, 7, 15}; ByteArray packed = PackFeaturesAndHashAsRawBytes(kData, AsByteSpan(kFeatures)); FeatureVec unpacked_features; std::string unpacked_hash = UnpackFeaturesAndHash(packed, &unpacked_features); EXPECT_EQ(kFeatures, unpacked_features); EXPECT_EQ(hash, unpacked_hash); } // Tests TemporaryLocalDirPath from several threads. TEST(UtilTest, TemporaryLocalDirPath) { { // Check that repeated calls return the same path. auto temp_dir = TemporaryLocalDirPath(); LOG(INFO) << temp_dir; EXPECT_EQ(temp_dir, TemporaryLocalDirPath()); } { auto temp_dir = TemporaryLocalDirPath(); // Create dir, create a file there, write to file, read from it, remove dir. std::filesystem::create_directories(temp_dir); std::string temp_file_path = std::filesystem::path(temp_dir).append("blah"); ByteArray written_data{1, 2, 3}; WriteToLocalFile(temp_file_path, written_data); ByteArray read_data; ReadFromLocalFile(temp_file_path, read_data); EXPECT_EQ(read_data, written_data); std::filesystem::remove_all(temp_dir); // temp_file_path should be gone by now. read_data.clear(); ReadFromLocalFile(temp_file_path, read_data); EXPECT_TRUE(read_data.empty()); } { // Create dirs in two threads. std::string temp_dir_1, temp_dir_2; { // NOTE: Not using a 2-threaded pool here because the scheduled tasks are // too fast and can end up both running in the same thread. Also, not // using `std::thread`. ThreadPool thread1{1}, thread2{1}; thread1.Schedule( [&temp_dir_1]() { temp_dir_1 = TemporaryLocalDirPath(); }); thread2.Schedule( [&temp_dir_2]() { temp_dir_2 = TemporaryLocalDirPath(); }); } // The threads join here. EXPECT_NE(temp_dir_1, temp_dir_2); } } TEST(UtilTest, CreateLocalDirRemovedAtExit) { // We need to test that dirs created via CreateLocalDirRemovedAtExit // are removed at exit. // To do that, we run death tests and check if the dirs exist afterwards. // The path to directory is computed in the parent test, then it is // passed via an env. var. to the child test so that the child test doesn't // recompute it to be something different. const char *centipede_util_test_temp_dir = getenv("CENTIPEDE_UTIL_TEST_TEMP_DIR"); auto tmpdir = centipede_util_test_temp_dir ? centipede_util_test_temp_dir : TemporaryLocalDirPath(); EXPECT_FALSE(std::filesystem::exists(tmpdir)); CreateLocalDirRemovedAtExit(tmpdir); EXPECT_TRUE(std::filesystem::exists(tmpdir)); setenv("CENTIPEDE_UTIL_TEST_TEMP_DIR", tmpdir.c_str(), 1); // Create two subdirs via CreateLocalDirRemovedAtExit. std::string subdir1 = std::filesystem::path(tmpdir).append("1"); std::string subdir2 = std::filesystem::path(tmpdir).append("2"); CreateLocalDirRemovedAtExit(subdir1); CreateLocalDirRemovedAtExit(subdir2); EXPECT_TRUE(std::filesystem::exists(subdir1)); EXPECT_TRUE(std::filesystem::exists(subdir2)); // Run a subprocess that creates the same two subdirs and ends with abort. // Both subdirs should still be there. auto create_dir_and_abort = [&]() { CreateLocalDirRemovedAtExit(subdir1); CreateLocalDirRemovedAtExit(subdir2); abort(); // atexit handlers are not called. }; EXPECT_DEATH(create_dir_and_abort(), ""); EXPECT_TRUE(std::filesystem::exists(subdir1)); EXPECT_TRUE(std::filesystem::exists(subdir2)); // Run a subprocess that creates the same two subdirs and ends with exit. // Both subdirs should be gone. auto create_dir_and_exit1 = [&]() { CreateLocalDirRemovedAtExit(subdir1); CreateLocalDirRemovedAtExit(subdir2); exit(1); // atexit handlers are called. }; EXPECT_DEATH(create_dir_and_exit1(), ""); EXPECT_FALSE(std::filesystem::exists(subdir1)); EXPECT_FALSE(std::filesystem::exists(subdir2)); } TEST(UtilTest, ParseAFLDictionary) { std::vector dict; EXPECT_TRUE(ParseAFLDictionary("", dict)); // Empty text. EXPECT_FALSE(ParseAFLDictionary("\xAB", dict)); // Non-ascii. EXPECT_FALSE(ParseAFLDictionary(" l1 \n\t\t\tl2 \n", dict)); // Missing " EXPECT_FALSE(ParseAFLDictionary(" \"zzz", dict)); // Missing second " // Two entries and a comment. EXPECT_TRUE( ParseAFLDictionary(" name=\"v1\" \n" " # comment\n" " \"v2\"", dict)); EXPECT_EQ(dict, std::vector({{'v', '1'}, {'v', '2'}})); // Hex entries and a properly escaped backslash. EXPECT_TRUE(ParseAFLDictionary(" \"\\xBC\\\\a\\xAB\\x00\"", dict)); EXPECT_EQ(dict, std::vector({{'\xBC', '\\', 'a', '\xAB', 0}})); // Special characters. EXPECT_TRUE(ParseAFLDictionary("\"\\r\\t\\n\\\"\"", dict)); EXPECT_EQ(dict, std::vector({{'\r', '\t', '\n', '"'}})); // Improper use of backslash, still parses. EXPECT_TRUE(ParseAFLDictionary("\"\\g\\h\"", dict)); EXPECT_EQ(dict, std::vector({{'\\', 'g', '\\', 'h'}})); } TEST(UtilTest, RandomWeightedSubset) { using v = std::vector; // to make test code more compact. std::vector set{20, 10, 0, 40, 50}; Rng rng(0); // target_size >= 4, expect only the index of 0s. EXPECT_THAT(RandomWeightedSubset(set, 10, rng), testing::ElementsAre(2)); EXPECT_THAT(RandomWeightedSubset(set, 4, rng), testing::ElementsAre(2)); // For more interesting values of target_size, run many iterations, sort // results by frequency, verify that more likely results are more frequent. constexpr size_t kNumIter = 100000; // Maps a result to its frequency. absl::flat_hash_map, size_t> results; // Returns a vector of results ordered from least frequent to most frequent. auto order_results = [&]() { std::vector> ordered_results; std::map> freq_to_res; for (const auto &it : results) freq_to_res[it.second] = it.first; ordered_results.reserve(freq_to_res.size()); for (const auto &it : freq_to_res) ordered_results.push_back(it.second); return ordered_results; }; // target size: 3 for (size_t i = 0; i < kNumIter; ++i) { ++results[RandomWeightedSubset(set, /*target_size=*/3, rng)]; } EXPECT_THAT(order_results(), testing::ElementsAre(v{2, 4}, v{2, 3}, v{0, 2}, v{1, 2})); // target_size: 2 results.clear(); for (size_t i = 0; i < kNumIter; ++i) { ++results[RandomWeightedSubset(set, /*target_size=*/2, rng)]; } EXPECT_THAT(order_results(), testing::ElementsAre(v{2, 3, 4}, v{0, 2, 4}, v{0, 2, 3}, v{1, 2, 4}, v{1, 2, 3}, v{0, 1, 2})); // target_size: 1 results.clear(); for (size_t i = 0; i < kNumIter; ++i) { ++results[RandomWeightedSubset(set, /*target_size=*/1, rng)]; } EXPECT_THAT(order_results(), testing::ElementsAre(v{0, 2, 3, 4}, v{1, 2, 3, 4}, v{0, 1, 2, 4}, v{0, 1, 2, 3})); } TEST(UtilTest, RemoveSubset) { std::vector set; auto Remove = [](const std::vector &subset_indices, std::vector set) { RemoveSubset(subset_indices, set); return set; }; EXPECT_THAT(Remove({0, 1}, {10, 20, 30, 40}), testing::ElementsAre(30, 40)); EXPECT_THAT(Remove({1, 2}, {10, 20, 30, 40}), testing::ElementsAre(10, 40)); EXPECT_THAT(Remove({2, 3}, {10, 20, 30, 40}), testing::ElementsAre(10, 20)); EXPECT_THAT(Remove({1}, {10, 20, 30, 40}), testing::ElementsAre(10, 30, 40)); EXPECT_THAT(Remove({}, {10, 20, 30, 40}), testing::ElementsAre(10, 20, 30, 40)); EXPECT_THAT(Remove({0, 1, 2, 3}, {10, 20, 30, 40}), testing::IsEmpty()); // Check that RemoveSubset can be applied to a vector. std::vector> vector_set = {{1}, {2}, {3}}; RemoveSubset({1}, vector_set); EXPECT_THAT(vector_set, testing::ElementsAre(std::vector{1}, std::vector{3})); } } // namespace fuzztest::internal