// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you 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 "cpp/util.h" #include #include #include #include #include #include #include #include "common/config.h" #include "common/logging.h" #include "common/simple_thread_pool.h" #include "common/string_util.h" #include "cpp/aws_common.h" #include "cpp/sync_point.h" #include "gtest/gtest.h" #include "recycler/recycler.h" #include "recycler/s3_accessor.h" #include "recycler/sync_executor.h" using namespace doris::cloud; int main(int argc, char** argv) { const auto* conf_file = "doris_cloud.conf"; if (!config::init(conf_file, true)) { std::cerr << "failed to init config file, conf=" << conf_file << std::endl; return -1; } if (!::init_glog("util")) { std::cerr << "failed to init glog" << std::endl; return -1; } ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } TEST(StringUtilTest, test_string_strip) { // clang-format off // str expect to_drop std::vector> leading_inputs { {"" , "" , "" }, {"" , "" , "/" }, {"/" , "" , "/" }, {"\t////" , "" , "/ \t"}, {"/a///" , "a///" , "/" }, {"/a/b/c/", "a/b/c/", "/" }, {"a/b/c/" , "a/b/c/", "/" }, {"a/b/c/" , "/b/c/" , "a" }, }; int idx = 0; for (auto&& i : leading_inputs) { doris::cloud::strip_leading(std::get<0>(i), std::get<2>(i)); EXPECT_EQ(std::get<0>(i), std::get<1>(i)) << " index=" << idx; ++idx; } idx = 0; std::vector> trailing_inputs { {"" , "" , "" }, {"/" , "" , "/" }, {"////\t" , "" , "/ \t"}, {"/a///" , "/a" , "/" }, {"/a/b/c/", "/a/b/c", "/" }, {"a/b/c/" , "a/b/c" , "/" }, {"a/b/c" , "a/b/c" , "/" }, {"a/b/c" , "a/b/" , "c" }, }; for (auto&& i : trailing_inputs) { doris::cloud::strip_trailing(std::get<0>(i), std::get<2>(i)); EXPECT_EQ(std::get<0>(i), std::get<1>(i)) << " index=" << idx; ++idx; } idx = 0; std::vector> trim_inputs { {"" , "" }, {"" , "" }, {"/" , "" }, {"\t////" , "" }, {"/a ///" , "a" }, {"/a/b/c/" , "a/b/c"}, {"a/b/c/" , "a/b/c"}, {"a/b/c" , "a/b/c"}, {"\t/bbc///" , "bbc" }, {"ab c" , "ab c" }, {"\t /a/b/c \t/", "a/b/c"}, }; for (auto&& i : trim_inputs) { doris::cloud::trim(std::get<0>(i)); EXPECT_EQ(std::get<0>(i), std::get<1>(i)) << " index=" << idx; ++idx; } // clang-format on } template auto task_wrapper(Func... funcs) -> std::function { return [funcs...]() { return [](std::initializer_list numbers) { int i = 0; for (int num : numbers) { if (num != 0) { i = num; } } return i; }({funcs()...}); }; } TEST(UtilTest, stage_wrapper) { std::function func1 = []() { return 0; }; std::function func2 = []() { return -1; }; std::function func3 = []() { return 0; }; auto f = task_wrapper(func1, func2, func3); ASSERT_EQ(-1, f()); f = task_wrapper(func1, func3); ASSERT_EQ(0, f()); } template void test_cancel_after_tasks_started(T normal_value, T cancel_value, std::function is_cancel) { auto pool = std::make_shared(3); pool->start(); bthread::CountdownEvent normal_tasks_started(2); bthread::CountdownEvent cancellation_observed(1); SyncExecutor sync_executor(pool, "cancel after tasks started", [&](const T& value) { bool cancelled = is_cancel(value); if (cancelled) { cancellation_observed.signal(); } return cancelled; }); auto normal_task = [&, normal_value]() { normal_tasks_started.signal(); cancellation_observed.wait(); return normal_value; }; auto cancel_task = [&, cancel_value]() { normal_tasks_started.wait(); return cancel_value; }; sync_executor.add(normal_task); sync_executor.add(normal_task); sync_executor.add(cancel_task); bool finished = true; auto results = sync_executor.when_all(&finished); ASSERT_FALSE(finished); ASSERT_EQ(3, results.size()); EXPECT_EQ(normal_value, results[0]); EXPECT_EQ(normal_value, results[1]); EXPECT_EQ(cancel_value, results[2]); } TEST(UtilTest, delay) { test_cancel_after_tasks_started(1, -1, [](const int& value) { return value == -1; }); test_cancel_after_tasks_started( "fake", "", [](const std::string_view value) { return value.empty(); }); } TEST(UtilTest, normal) { auto s3_producer_pool = std::make_shared(config::recycle_pool_parallelism); s3_producer_pool->start(); // test normal execute { SyncExecutor sync_executor(s3_producer_pool, "normal test", [](int k) { return k == -1; }); auto f1 = []() { return 1; }; sync_executor.add(f1); sync_executor.add(f1); sync_executor.add(f1); bool finished = true; std::vector res = sync_executor.when_all(&finished); ASSERT_EQ(3, res.size()); ASSERT_EQ(finished, true); std::for_each(res.begin(), res.end(), [](auto&& n) { ASSERT_EQ(1, n); }); } // test when error happen { SyncExecutor sync_executor(s3_producer_pool, "normal test", [](int k) { return k == -1; }); auto f1 = []() { return 1; }; sync_executor._stop_token = true; sync_executor.add(f1); sync_executor.add(f1); sync_executor.add(f1); bool finished = true; std::vector res = sync_executor.when_all(&finished); ASSERT_EQ(finished, false); ASSERT_EQ(0, res.size()); } { SyncExecutor sync_executor(s3_producer_pool, "normal test", [](int k) { return k == -1; }); auto f1 = []() { return 1; }; auto cancel = []() { return -1; }; sync_executor.add(f1); sync_executor.add(f1); sync_executor.add(f1); sync_executor.add(cancel); bool finished = true; std::vector res = sync_executor.when_all(&finished); ASSERT_EQ(finished, false); } // test string_view { SyncExecutor sync_executor( s3_producer_pool, "normal test", [](const std::string_view k) { return k.empty(); }); std::string s = "Hello World"; auto f1 = [&s]() { return std::string_view(s); }; sync_executor.add(f1); sync_executor.add(f1); sync_executor.add(f1); bool finished = true; std::vector res = sync_executor.when_all(&finished); ASSERT_EQ(3, res.size()); std::for_each(res.begin(), res.end(), [&s](auto&& n) { ASSERT_EQ(s, n); }); } } TEST(UtilTest, test_add_after_when_all) { auto f = []() { auto pool = std::make_shared(config::recycle_pool_parallelism); pool->start(); SyncExecutor sync_executor(pool, "test add after when all: inside", [](int k) { return k != 0; }); auto f1 = []() { return 0; }; sync_executor.add(f1); bool finished = true; std::vector res = sync_executor.when_all(&finished); sync_executor.add(f1); res = sync_executor.when_all(&finished); EXPECT_EQ(1, res.size()); EXPECT_EQ(finished, true); std::for_each(res.begin(), res.end(), [](auto&& n) { EXPECT_EQ(0, n); }); return 0; }; auto s3_producer_pool = std::make_shared(config::recycle_pool_parallelism); s3_producer_pool->start(); SyncExecutor s3_sync_executor(s3_producer_pool, "test add after when all: outside", [](int k) { return k != 0; }); s3_sync_executor.add(f); bool finished = true; std::vector res = s3_sync_executor.when_all(&finished); EXPECT_EQ(1, res.size()); EXPECT_EQ(finished, true); std::for_each(res.begin(), res.end(), [](auto&& n) { EXPECT_EQ(0, n); }); } TEST(UtilTest, exception) { auto s3_producer_pool = std::make_shared(config::recycle_pool_parallelism); s3_producer_pool->start(); { SyncExecutor sync_executor(s3_producer_pool, "exception test", [](int k) { return k != 0; }); auto f = []() { throw(std::runtime_error("test exception")); return 1; }; sync_executor.add(f); bool finished = true; std::vector res = sync_executor.when_all(&finished); EXPECT_EQ(0, res.size()); EXPECT_EQ(finished, false); std::for_each(res.begin(), res.end(), [](auto&& n) { EXPECT_EQ(1, n); }); } } TEST(UtilTest, test_sync_executor) { auto f = []() { sleep(1); auto pool = std::make_shared(config::recycle_pool_parallelism); pool->start(); SyncExecutor sync_executor(pool, "test sync executor: inside", [](int k) { return k != 0; }); auto f1 = []() { return 0; }; sync_executor.add(f1); bool finished = true; std::vector res = sync_executor.when_all(&finished); sync_executor.add(f1); res = sync_executor.when_all(&finished); EXPECT_EQ(1, res.size()); EXPECT_EQ(finished, true); std::for_each(res.begin(), res.end(), [](auto&& n) { EXPECT_EQ(0, n); }); return 0; }; std::mutex go_mutex; auto* sp = doris::SyncPoint::get_instance(); sp->set_call_back("SyncExecutor::when_all.set_wait_time", [&](auto&& args) { std::unique_lock _lock(go_mutex); [[maybe_unused]] auto max_wait_time = *doris::try_any_cast(args[0]); max_wait_time = 100; }); auto s3_producer_pool = std::make_shared(config::recycle_pool_parallelism); s3_producer_pool->start(); SyncExecutor s3_sync_executor(s3_producer_pool, "test sync executor: outside", [](int k) { return k != 0; }); s3_sync_executor.add(f); bool finished = true; std::vector res = s3_sync_executor.when_all(&finished); EXPECT_EQ(1, res.size()); EXPECT_EQ(finished, true); std::for_each(res.begin(), res.end(), [](auto&& n) { EXPECT_EQ(0, n); }); } TEST(UtilTest, test_split) { auto path = doris::get_valid_ca_cert_path(doris::cloud::split(config::ca_cert_file_paths, ';')); LOG(INFO) << "config:" << config::ca_cert_file_paths << " path:" << path; ASSERT_FALSE(path.empty()); } TEST(UtilTest, test_normalize_http_uri) { // ===== Basic functionality with HTTPS protocol ===== EXPECT_EQ(doris::normalize_http_uri("https://example.com/path"), "https://example.com/path"); EXPECT_EQ(doris::normalize_http_uri("https://example.com//path"), "https://example.com/path"); EXPECT_EQ(doris::normalize_http_uri("https://example.com///path"), "https://example.com/path"); // ===== Basic functionality with HTTP protocol ===== EXPECT_EQ(doris::normalize_http_uri("http://example.com/path"), "http://example.com/path"); EXPECT_EQ(doris::normalize_http_uri("http://example.com//path"), "http://example.com/path"); EXPECT_EQ(doris::normalize_http_uri("http://example.com///path"), "http://example.com/path"); // ===== Multiple consecutive slashes in different positions ===== EXPECT_EQ(doris::normalize_http_uri("https://host.com//bucket//prefix"), "https://host.com/bucket/prefix"); EXPECT_EQ(doris::normalize_http_uri("https://host.com///bucket///prefix///"), "https://host.com/bucket/prefix/"); EXPECT_EQ(doris::normalize_http_uri("https://host.com////bucket////prefix////file"), "https://host.com/bucket/prefix/file"); // ===== Azure blob storage specific URLs ===== EXPECT_EQ(doris::normalize_http_uri("https://account.blob.core.windows.net//container"), "https://account.blob.core.windows.net/container"); EXPECT_EQ( doris::normalize_http_uri("https://account.blob.core.windows.net///container//prefix"), "https://account.blob.core.windows.net/container/prefix"); EXPECT_EQ(doris::normalize_http_uri( "https://account.blob.core.windows.net////container///prefix///file.txt"), "https://account.blob.core.windows.net/container/prefix/file.txt"); // ===== URLs without protocol ===== EXPECT_EQ(doris::normalize_http_uri("example.com//path"), "example.com/path"); EXPECT_EQ(doris::normalize_http_uri("host.com///bucket//prefix"), "host.com/bucket/prefix"); EXPECT_EQ(doris::normalize_http_uri("//path//to//file"), "/path/to/file"); // ===== Edge cases ===== // Empty string EXPECT_EQ(doris::normalize_http_uri(""), ""); // Only protocol EXPECT_EQ(doris::normalize_http_uri("https://"), "https://"); EXPECT_EQ(doris::normalize_http_uri("http://"), "http://"); // Only slashes EXPECT_EQ(doris::normalize_http_uri("//"), "/"); EXPECT_EQ(doris::normalize_http_uri("///"), "/"); EXPECT_EQ(doris::normalize_http_uri("////"), "/"); // Single character paths EXPECT_EQ(doris::normalize_http_uri("https://a"), "https://a"); EXPECT_EQ(doris::normalize_http_uri("https://a/"), "https://a/"); EXPECT_EQ(doris::normalize_http_uri("https://a//"), "https://a/"); // ===== Protocol preservation ===== // Ensure protocol :// is never modified EXPECT_EQ(doris::normalize_http_uri("https://example.com"), "https://example.com"); EXPECT_EQ(doris::normalize_http_uri("http://example.com"), "http://example.com"); // Even with extra slashes after protocol EXPECT_EQ(doris::normalize_http_uri("https:///example.com"), "https://example.com"); EXPECT_EQ(doris::normalize_http_uri("http:///example.com"), "http://example.com"); // Mixed case protocol (though unusual) EXPECT_EQ(doris::normalize_http_uri("HTTP://example.com//path"), "HTTP://example.com/path"); EXPECT_EQ(doris::normalize_http_uri("HTTPS://example.com//path"), "HTTPS://example.com/path"); // ===== Trailing slashes ===== EXPECT_EQ(doris::normalize_http_uri("https://example.com/path/"), "https://example.com/path/"); EXPECT_EQ(doris::normalize_http_uri("https://example.com/path//"), "https://example.com/path/"); EXPECT_EQ(doris::normalize_http_uri("https://example.com/path///"), "https://example.com/path/"); EXPECT_EQ(doris::normalize_http_uri("https://example.com/path////"), "https://example.com/path/"); // ===== Complex real-world scenarios ===== // Simulating common configuration mistakes EXPECT_EQ(doris::normalize_http_uri("https://endpoint.com///bucket//prefix//file.txt"), "https://endpoint.com/bucket/prefix/file.txt"); // User configured endpoint with trailing slash + bucket with leading slash EXPECT_EQ(doris::normalize_http_uri("https://endpoint.com///bucket"), "https://endpoint.com/bucket"); // Multiple slashes everywhere EXPECT_EQ( doris::normalize_http_uri("https://host.com////bucket////prefix////subfolder////file"), "https://host.com/bucket/prefix/subfolder/file"); // ===== Special characters in path ===== EXPECT_EQ( doris::normalize_http_uri("https://example.com//path-with-dash//file_with_underscore"), "https://example.com/path-with-dash/file_with_underscore"); EXPECT_EQ(doris::normalize_http_uri("https://example.com//path.with.dots//file@special"), "https://example.com/path.with.dots/file@special"); EXPECT_EQ(doris::normalize_http_uri("https://example.com//bucket123//prefix456//file789"), "https://example.com/bucket123/prefix456/file789"); // ===== URLs with query parameters and fragments ===== EXPECT_EQ(doris::normalize_http_uri("https://example.com//path?query=value"), "https://example.com/path?query=value"); EXPECT_EQ(doris::normalize_http_uri("https://example.com//path#fragment"), "https://example.com/path#fragment"); EXPECT_EQ(doris::normalize_http_uri("https://example.com//path?query=value#fragment"), "https://example.com/path?query=value#fragment"); } TEST(UtilTest, test_set_s3_client_default_http_scheme) { doris::cloud::S3Environment::getInstance(); Aws::Client::ClientConfiguration client_config = doris::cloud::S3Environment::getClientConfiguration(); client_config.endpointOverride = "example.com:9000"; doris::set_s3_client_default_http_scheme(client_config, "http"); EXPECT_EQ(client_config.endpointOverride, "example.com:9000"); EXPECT_EQ(client_config.scheme, Aws::Http::Scheme::HTTP); doris::set_s3_client_default_http_scheme(client_config, "https"); EXPECT_EQ(client_config.endpointOverride, "example.com:9000"); EXPECT_EQ(client_config.scheme, Aws::Http::Scheme::HTTPS); client_config.endpointOverride = "http://example.com:9000"; client_config.scheme = Aws::Http::Scheme::HTTP; doris::set_s3_client_default_http_scheme(client_config, "https"); EXPECT_EQ(client_config.endpointOverride, "http://example.com:9000"); EXPECT_EQ(client_config.scheme, Aws::Http::Scheme::HTTP); client_config.endpointOverride = "https://example.com:9000"; client_config.scheme = Aws::Http::Scheme::HTTPS; doris::set_s3_client_default_http_scheme(client_config, "http"); EXPECT_EQ(client_config.endpointOverride, "https://example.com:9000"); EXPECT_EQ(client_config.scheme, Aws::Http::Scheme::HTTPS); } TEST(UtilTest, test_long_normalize_http_uri) { std::string longPath = "https://example.com"; for (int i = 0; i < 100; i++) { longPath += "//segment" + std::to_string(i); } std::string expected = "https://example.com"; for (int i = 0; i < 100; i++) { expected += "/segment" + std::to_string(i); } EXPECT_EQ(doris::normalize_http_uri(longPath), expected); }