Skip to content

Commit 962c65b

Browse files
committed
2
1 parent 7bf668f commit 962c65b

File tree

4 files changed

+194
-95
lines changed

4 files changed

+194
-95
lines changed

src/iceberg/test/CMakeLists.txt

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,14 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717

18-
fetchcontent_declare(cpp-httplib
19-
GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git
20-
GIT_TAG 89c932f313c6437c38f2982869beacc89c2f2246 #release-0.26.0
21-
)
22-
2318
fetchcontent_declare(googletest
2419
GIT_REPOSITORY https://github.com/google/googletest.git
2520
GIT_TAG b514bdc898e2951020cbdca1304b75f5950d1f59 # release-1.15.2
2621
FIND_PACKAGE_ARGS
2722
NAMES
2823
GTest)
2924

30-
if(ICEBERG_BUILD_REST)
31-
fetchcontent_makeavailable(cpp-httplib googletest)
32-
else()
33-
fetchcontent_makeavailable(googletest)
34-
endif()
25+
fetchcontent_makeavailable(googletest)
3526

3627
set(ICEBERG_TEST_RESOURCES "${CMAKE_SOURCE_DIR}/src/iceberg/test/resources")
3728

@@ -53,11 +44,9 @@ function(add_iceberg_test test_name)
5344
target_sources(${test_name} PRIVATE ${ARG_SOURCES})
5445

5546
if(ARG_USE_BUNDLE)
56-
target_link_libraries(${test_name} PRIVATE iceberg_bundle_static GTest::gtest_main
57-
GTest::gmock)
47+
target_link_libraries(${test_name} PRIVATE iceberg_bundle_static GTest::gmock_main)
5848
else()
59-
target_link_libraries(${test_name} PRIVATE iceberg_static GTest::gtest_main
60-
GTest::gmock)
49+
target_link_libraries(${test_name} PRIVATE iceberg_static GTest::gmock_main)
6150
endif()
6251

6352
add_test(NAME ${test_name} COMMAND ${test_name})
@@ -171,16 +160,18 @@ if(ICEBERG_BUILD_REST)
171160
add_executable(${test_name})
172161
target_include_directories(${test_name} PRIVATE "${CMAKE_BINARY_DIR}/iceberg/test/")
173162
target_sources(${test_name} PRIVATE ${ARG_SOURCES})
174-
target_link_libraries(${test_name} PRIVATE GTest::gtest_main GTest::gmock
175-
iceberg_rest_static)
163+
target_link_libraries(${test_name} PRIVATE GTest::gmock_main iceberg_rest_static)
176164
add_test(NAME ${test_name} COMMAND ${test_name})
177165
endfunction()
178166

179-
add_rest_iceberg_test(rest_catalog_test
180-
SOURCES
181-
rest_catalog_test.cc
182-
rest_json_internal_test.cc
167+
add_rest_iceberg_test(rest_catalog_test SOURCES rest_json_internal_test.cc
183168
rest_util_test.cc)
184169

185-
target_include_directories(rest_catalog_test PRIVATE ${cpp-httplib_SOURCE_DIR})
170+
if(ICEBERG_BUILD_REST_INTEGRATION_TESTS)
171+
add_rest_iceberg_test(rest_catalog_integration_test
172+
SOURCES
173+
rest_catalog_test.cc
174+
util/cmd_util.cc
175+
util/docker_compose_util.cc)
176+
endif()
186177
endif()

src/iceberg/test/meson.build

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,17 +89,28 @@ iceberg_tests = {
8989
}
9090

9191
if get_option('rest').enabled()
92-
cpp_httplib_dep = dependency('cpp-httplib')
9392
iceberg_tests += {
9493
'rest_catalog_test': {
95-
'sources': files(
96-
'rest_catalog_test.cc',
97-
'rest_json_internal_test.cc',
98-
'rest_util_test.cc',
99-
),
100-
'dependencies': [iceberg_rest_dep, cpp_httplib_dep],
94+
'sources': files('rest_json_internal_test.cc', 'rest_util_test.cc'),
95+
'dependencies': [iceberg_rest_dep],
10196
},
10297
}
98+
if get_option('rest_integration_test').enabled()
99+
if host_machine.system() == 'windows'
100+
warning('Cannot build rest integration test on Windows, skipping.')
101+
else
102+
iceberg_tests += {
103+
'rest_integration_test': {
104+
'sources': files(
105+
'rest_catalog_test.cc',
106+
'util/cmd_util.cc',
107+
'util/docker_compose_util.cc',
108+
),
109+
'dependencies': [iceberg_rest_dep],
110+
},
111+
}
112+
endif
113+
endif
103114
endif
104115

105116
foreach test_name, values : iceberg_tests

src/iceberg/test/rest_catalog_test.cc

Lines changed: 164 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -19,103 +19,206 @@
1919

2020
#include "iceberg/catalog/rest/rest_catalog.h"
2121

22+
#include <unistd.h>
23+
24+
#include <chrono>
25+
#include <memory>
26+
#include <print>
2227
#include <string>
28+
#include <thread>
2329
#include <unordered_map>
2430

31+
#include <arpa/inet.h>
2532
#include <gmock/gmock.h>
2633
#include <gtest/gtest.h>
34+
#include <netinet/in.h>
35+
#include <sys/socket.h>
2736

2837
#include "iceberg/catalog/rest/catalog_properties.h"
38+
#include "iceberg/result.h"
2939
#include "iceberg/table_identifier.h"
3040
#include "iceberg/test/matchers.h"
41+
#include "iceberg/test/util/docker_compose_util.h"
3142

3243
namespace iceberg::rest {
3344

34-
// Test fixture for REST catalog tests, This assumes you have a local REST catalog service
35-
// running Default configuration: http://localhost:8181.
36-
class RestCatalogTest : public ::testing::Test {
45+
namespace {
46+
47+
constexpr uint16_t kRestCatalogPort = 8181;
48+
constexpr int kMaxRetries = 60; // Wait up to 60 seconds
49+
constexpr int kRetryDelayMs = 1000;
50+
51+
constexpr std::string_view kDockerProjectName = "iceberg-rest-catalog-service";
52+
constexpr std::string_view kCatalogName = "test_catalog";
53+
constexpr std::string_view kWarehouseName = "default";
54+
constexpr std::string_view kLocalhostUri = "http://localhost";
55+
56+
/// \brief Check if a localhost port is ready to accept connections
57+
/// \param port Port number to check
58+
/// \return true if the port is accessible on localhost, false otherwise
59+
bool CheckServiceReady(uint16_t port) {
60+
int sock = socket(AF_INET, SOCK_STREAM, 0);
61+
if (sock < 0) {
62+
return false;
63+
}
64+
65+
struct timeval timeout{
66+
.tv_sec = 1,
67+
.tv_usec = 0,
68+
};
69+
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
70+
71+
sockaddr_in addr{
72+
.sin_family = AF_INET,
73+
.sin_port = htons(port),
74+
.sin_addr = {.s_addr = htonl(INADDR_LOOPBACK)} // 127.0.0.1
75+
};
76+
bool result =
77+
(connect(sock, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)) == 0);
78+
close(sock);
79+
return result;
80+
}
81+
82+
} // namespace
83+
84+
/// \brief Integration test fixture for REST catalog with automatic Docker Compose setup。
85+
class RestCatalogIntegrationTest : public ::testing::Test {
3786
protected:
38-
void SetUp() override {
39-
// Default configuration for local testing
40-
// You can override this with environment variables if needed
41-
const char* uri_env = std::getenv("ICEBERG_REST_URI");
42-
const char* warehouse_env = std::getenv("ICEBERG_REST_WAREHOUSE");
87+
static void SetUpTestSuite() {
88+
std::string project_name{kDockerProjectName};
89+
std::filesystem::path resources_dir =
90+
std::filesystem::path(__FILE__).parent_path() / "resources";
91+
92+
// Create and start DockerCompose
93+
docker_compose_ = std::make_unique<DockerCompose>(project_name, resources_dir);
94+
docker_compose_->Up();
95+
96+
// Wait for REST catalog to be ready on localhost
97+
std::println("[INFO] Waiting for REST catalog to be ready at localhost:{}...",
98+
kRestCatalogPort);
99+
for (int i = 0; i < kMaxRetries; ++i) {
100+
if (CheckServiceReady(kRestCatalogPort)) {
101+
std::println("[INFO] REST catalog is ready!");
102+
return;
103+
}
104+
std::println(
105+
"[INFO] Waiting for 1s for REST catalog to be ready... (attempt {}/{})", i + 1,
106+
kMaxRetries);
107+
std::this_thread::sleep_for(std::chrono::milliseconds(kRetryDelayMs));
108+
}
109+
throw RestError("REST catalog failed to start within {} seconds", kMaxRetries);
110+
}
43111

44-
std::string uri = uri_env ? uri_env : "http://localhost:8181";
45-
std::string warehouse = warehouse_env ? warehouse_env : "default";
112+
static void TearDownTestSuite() { docker_compose_.reset(); }
46113

114+
void SetUp() override {
47115
config_ = RestCatalogProperties::default_properties();
48-
config_->Set(RestCatalogProperties::kUri, uri)
49-
.Set(RestCatalogProperties::kName, std::string("test_catalog"))
50-
.Set(RestCatalogProperties::kWarehouse, warehouse);
116+
config_
117+
->Set(RestCatalogProperties::kUri,
118+
std::format("{}:{}", kLocalhostUri, kRestCatalogPort))
119+
.Set(RestCatalogProperties::kName, std::string(kCatalogName))
120+
.Set(RestCatalogProperties::kWarehouse, std::string(kWarehouseName));
51121
}
52122

53123
void TearDown() override {}
54124

125+
/// \brief Helper function to create a REST catalog instance
126+
Result<std::unique_ptr<RestCatalog>> CreateCatalog() {
127+
return RestCatalog::Make(*config_);
128+
}
129+
130+
static inline std::unique_ptr<DockerCompose> docker_compose_;
55131
std::unique_ptr<RestCatalogProperties> config_;
56132
};
57133

58-
TEST_F(RestCatalogTest, DISABLED_MakeCatalogSuccess) {
59-
auto catalog_result = RestCatalog::Make(*config_);
60-
EXPECT_THAT(catalog_result, IsOk());
134+
TEST_F(RestCatalogIntegrationTest, MakeCatalogSuccess) {
135+
auto catalog_result = CreateCatalog();
136+
ASSERT_THAT(catalog_result, IsOk());
61137

62-
if (catalog_result.has_value()) {
63-
auto& catalog = catalog_result.value();
64-
EXPECT_EQ(catalog->name(), "test_catalog");
65-
}
138+
auto& catalog = catalog_result.value();
139+
EXPECT_EQ(catalog->name(), kCatalogName);
66140
}
67141

68-
TEST_F(RestCatalogTest, DISABLED_MakeCatalogEmptyUri) {
69-
auto invalid_config = RestCatalogProperties::default_properties();
70-
invalid_config->Set(RestCatalogProperties::kUri, std::string(""));
142+
TEST_F(RestCatalogIntegrationTest, ListNamespaces) {
143+
auto catalog_result = CreateCatalog();
144+
ASSERT_THAT(catalog_result, IsOk());
145+
auto& catalog = catalog_result.value();
71146

72-
auto catalog_result = RestCatalog::Make(*invalid_config);
73-
EXPECT_THAT(catalog_result, IsError(ErrorKind::kInvalidArgument));
74-
EXPECT_THAT(catalog_result, HasErrorMessage("uri"));
147+
Namespace root{.levels = {}};
148+
auto result = catalog->ListNamespaces(root);
149+
EXPECT_THAT(result, IsOk());
75150
}
76151

77-
TEST_F(RestCatalogTest, DISABLED_MakeCatalogWithCustomProperties) {
78-
auto custom_config = RestCatalogProperties::default_properties();
79-
custom_config
80-
->Set(RestCatalogProperties::kUri, config_->Get(RestCatalogProperties::kUri))
81-
.Set(RestCatalogProperties::kName, config_->Get(RestCatalogProperties::kName))
82-
.Set(RestCatalogProperties::kWarehouse,
83-
config_->Get(RestCatalogProperties::kWarehouse))
84-
.Set(RestCatalogProperties::Entry<std::string>{"custom_prop", ""},
85-
std::string("custom_value"))
86-
.Set(RestCatalogProperties::Entry<std::string>{"timeout", ""},
87-
std::string("30000"));
88-
89-
auto catalog_result = RestCatalog::Make(*custom_config);
90-
EXPECT_THAT(catalog_result, IsOk());
152+
TEST_F(RestCatalogIntegrationTest, DISABLED_GetNonExistentNamespace) {
153+
auto catalog_result = CreateCatalog();
154+
ASSERT_THAT(catalog_result, IsOk());
155+
auto& catalog = catalog_result.value();
156+
157+
Namespace ns{.levels = {"test_get_non_existent_namespace"}};
158+
auto result = catalog->GetNamespaceProperties(ns);
159+
160+
EXPECT_THAT(result, HasErrorMessage("does not exist"));
91161
}
92162

93-
TEST_F(RestCatalogTest, DISABLED_ListNamespaces) {
94-
auto catalog_result = RestCatalog::Make(*config_);
163+
TEST_F(RestCatalogIntegrationTest, DISABLED_CreateAndDropNamespace) {
164+
auto catalog_result = CreateCatalog();
95165
ASSERT_THAT(catalog_result, IsOk());
96-
auto& catalog = catalog_result.value();
166+
auto catalog = std::move(catalog_result.value());
97167

98-
Namespace ns{.levels = {}};
99-
auto result = catalog->ListNamespaces(ns);
100-
EXPECT_THAT(result, IsOk());
101-
EXPECT_FALSE(result->empty());
102-
EXPECT_EQ(result->front().levels, (std::vector<std::string>{"my_namespace_test2"}));
168+
// Create a namespace
169+
Namespace test_ns{.levels = {"test_create_drop_ns"}};
170+
std::unordered_map<std::string, std::string> props = {{"owner", "test_user"}};
171+
172+
auto create_result = catalog->CreateNamespace(test_ns, props);
173+
ASSERT_THAT(create_result, IsOk());
174+
175+
// Verify it exists
176+
auto exists_result = catalog->NamespaceExists(test_ns);
177+
EXPECT_THAT(exists_result, HasValue(::testing::Eq(true)));
178+
179+
// Drop it
180+
auto drop_result = catalog->DropNamespace(test_ns);
181+
EXPECT_THAT(drop_result, IsOk());
182+
183+
// Verify it no longer exists
184+
auto exists_result2 = catalog->NamespaceExists(test_ns);
185+
EXPECT_THAT(exists_result2, HasValue(::testing::Eq(false)));
103186
}
104187

105-
TEST_F(RestCatalogTest, DISABLED_CreateNamespaceNotImplemented) {
106-
auto catalog_result = RestCatalog::Make(*config_);
188+
TEST_F(RestCatalogIntegrationTest, DISABLED_UpdateNamespaceProperties) {
189+
auto catalog_result = CreateCatalog();
107190
ASSERT_THAT(catalog_result, IsOk());
108191
auto catalog = std::move(catalog_result.value());
109192

110-
Namespace ns{.levels = {"test_namespace"}};
111-
std::unordered_map<std::string, std::string> props = {{"owner", "test"}};
193+
// Create a namespace
194+
Namespace test_ns{.levels = {"test_update_props_ns"}};
195+
std::unordered_map<std::string, std::string> initial_props = {{"owner", "alice"},
196+
{"team", "data_eng"}};
197+
198+
auto create_result = catalog->CreateNamespace(test_ns, initial_props);
199+
ASSERT_THAT(create_result, IsOk());
200+
201+
// Update properties
202+
std::unordered_map<std::string, std::string> updates = {
203+
{"owner", "bob"}, {"description", "test namespace"}};
204+
std::unordered_set<std::string> removals = {"team"};
205+
206+
auto update_result = catalog->UpdateNamespaceProperties(test_ns, updates, removals);
207+
EXPECT_THAT(update_result, IsOk());
208+
209+
// Verify updated properties
210+
auto props_result = catalog->GetNamespaceProperties(test_ns);
211+
ASSERT_THAT(props_result, IsOk());
212+
EXPECT_EQ((*props_result)["owner"], "bob");
213+
EXPECT_EQ((*props_result)["description"], "test namespace");
214+
EXPECT_EQ(props_result->count("team"), 0); // Should be removed
112215

113-
auto result = catalog->CreateNamespace(ns, props);
114-
EXPECT_THAT(result, IsError(ErrorKind::kNotImplemented));
216+
// Cleanup
217+
catalog->DropNamespace(test_ns);
115218
}
116219

117-
TEST_F(RestCatalogTest, DISABLED_IntegrationTestFullNamespaceWorkflow) {
118-
auto catalog_result = RestCatalog::Make(*config_);
220+
TEST_F(RestCatalogIntegrationTest, DISABLED_FullNamespaceWorkflow) {
221+
auto catalog_result = CreateCatalog();
119222
ASSERT_THAT(catalog_result, IsOk());
120223
auto catalog = std::move(catalog_result.value());
121224

@@ -126,11 +229,11 @@ TEST_F(RestCatalogTest, DISABLED_IntegrationTestFullNamespaceWorkflow) {
126229
size_t initial_count = list_result1->size();
127230

128231
// 2. Create a new namespace
129-
Namespace test_ns{.levels = {"integration_test_ns"}};
232+
Namespace test_ns{.levels = {"integration_test_workflow"}};
130233
std::unordered_map<std::string, std::string> props = {
131-
{"owner", "test"}, {"created_by", "rest_catalog_test"}};
234+
{"owner", "test"}, {"created_by", "rest_catalog_integration_test"}};
132235
auto create_result = catalog->CreateNamespace(test_ns, props);
133-
EXPECT_THAT(create_result, IsOk());
236+
ASSERT_THAT(create_result, IsOk());
134237

135238
// 3. Verify namespace exists
136239
auto exists_result = catalog->NamespaceExists(test_ns);
@@ -148,15 +251,15 @@ TEST_F(RestCatalogTest, DISABLED_IntegrationTestFullNamespaceWorkflow) {
148251

149252
// 6. Update properties
150253
std::unordered_map<std::string, std::string> updates = {
151-
{"description", "test namespace"}};
254+
{"description", "integration test namespace"}};
152255
std::unordered_set<std::string> removals = {};
153256
auto update_result = catalog->UpdateNamespaceProperties(test_ns, updates, removals);
154257
EXPECT_THAT(update_result, IsOk());
155258

156259
// 7. Verify updated properties
157260
auto props_result2 = catalog->GetNamespaceProperties(test_ns);
158261
ASSERT_THAT(props_result2, IsOk());
159-
EXPECT_EQ((*props_result2)["description"], "test namespace");
262+
EXPECT_EQ((*props_result2)["description"], "integration test namespace");
160263

161264
// 8. Drop the namespace (cleanup)
162265
auto drop_result = catalog->DropNamespace(test_ns);

0 commit comments

Comments
 (0)