cas_client_test.cpp
1 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2 // // cornell // cas_client_test 3 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4 // 5 // Integration tests against real NativeLink CAS 6 // 7 // Prerequisites: 8 // - NativeLink running on localhost:50051 9 // - Start with: nix run github:TraceMachina/nativelink ./local/nativelink.json5 10 // 11 // These tests are SKIPPED if NativeLink is not reachable. 12 13 #include "cornell/cas_client.hpp" 14 15 #include <cstdlib> 16 #include <iostream> 17 #include <string> 18 19 namespace cornell { 20 21 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 22 // Test Framework (minimal) 23 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 24 25 static int g_tests_run = 0; 26 static int g_tests_passed = 0; 27 static int g_tests_skipped = 0; 28 29 #define TEST(name) \ 30 void test_##name(); \ 31 struct Test_##name { \ 32 Test_##name() { \ 33 std::cout << "[ RUN ] " << #name << std::endl; \ 34 try { \ 35 test_##name(); \ 36 std::cout << "[ OK ] " << #name << std::endl; \ 37 ++g_tests_passed; \ 38 } catch (const SkipTest& e) { \ 39 std::cout << "[ SKIPPED ] " << #name << ": " << e.what() << std::endl; \ 40 ++g_tests_skipped; \ 41 } catch (const std::exception& e) { \ 42 std::cout << "[ FAILED ] " << #name << ": " << e.what() << std::endl; \ 43 } \ 44 ++g_tests_run; \ 45 } \ 46 } test_instance_##name; \ 47 void test_##name() 48 49 struct SkipTest : std::exception { 50 std::string msg; 51 explicit SkipTest(std::string m) : msg(std::move(m)) {} 52 const char* what() const noexcept override { return msg.c_str(); } 53 }; 54 55 #define ASSERT_TRUE(cond) \ 56 do { if (!(cond)) throw std::runtime_error("Assertion failed: " #cond); } while(0) 57 58 #define ASSERT_FALSE(cond) \ 59 do { if (cond) throw std::runtime_error("Assertion failed (expected false): " #cond); } while(0) 60 61 #define ASSERT_EQ(a, b) \ 62 do { if ((a) != (b)) throw std::runtime_error("Assertion failed: " #a " != " #b); } while(0) 63 64 #define SKIP(msg) throw SkipTest(msg) 65 66 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 67 // Configuration 68 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 69 70 CASConfig testConfig() { 71 return CASConfig{ 72 .host = "127.0.0.1", 73 .port = 50051, 74 .useTLS = false, 75 .instanceName = "main" 76 }; 77 } 78 79 // Check if NativeLink is available 80 bool isNativeLinkAvailable() { 81 try { 82 CASClient client(testConfig()); 83 // Try a simple operation 84 auto digest = digestFromBytes("connection-test"); 85 client.blobExists(digest); 86 return true; 87 } catch (...) { 88 return false; 89 } 90 } 91 92 #define WITH_NATIVELINK_OR_SKIP() \ 93 do { if (!isNativeLinkAvailable()) SKIP("NativeLink not running at 127.0.0.1:50051"); } while(0) 94 95 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 96 // Tests 97 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 98 99 // Test FindMissingBlobs - check if we can connect and query 100 TEST(find_missing_blobs) { 101 WITH_NATIVELINK_OR_SKIP(); 102 103 CASClient client(testConfig()); 104 105 // Create a digest for a blob that definitely doesn't exist 106 auto nonExistentDigest = digestFromBytes("this-blob-should-not-exist-in-cas-12345"); 107 108 // Check if it exists (it shouldn't) 109 bool exists = client.blobExists(nonExistentDigest); 110 std::cout << " Non-existent blob exists: " << std::boolalpha << exists << std::endl; 111 ASSERT_FALSE(exists); 112 } 113 114 // Test uploading a small blob via BatchUpdateBlobs 115 TEST(upload_small_blob) { 116 WITH_NATIVELINK_OR_SKIP(); 117 118 CASClient client(testConfig()); 119 120 std::string content = "Hello from cornell C++ integration test!"; 121 auto expectedDigest = digestFromBytes(content); 122 123 std::cout << " Uploading blob: " << expectedDigest.hash << std::endl; 124 125 // Upload returns the digest 126 auto resultDigest = client.uploadBlob(content); 127 128 // Verify digest matches 129 ASSERT_EQ(expectedDigest.hash, resultDigest.hash); 130 ASSERT_EQ(expectedDigest.size, resultDigest.size); 131 std::cout << " Small blob upload completed" << std::endl; 132 } 133 134 // Test full roundtrip: upload then download 135 TEST(upload_download_roundtrip) { 136 WITH_NATIVELINK_OR_SKIP(); 137 138 CASClient client(testConfig()); 139 140 // Create content with binary data 141 std::string content = "Roundtrip test content: "; 142 for (int i = 0; i < 256; ++i) { 143 content.push_back(static_cast<char>(i)); 144 } 145 auto digest = digestFromBytes(content); 146 147 std::cout << " Upload: " << digest.hash << " (" << content.size() << " bytes)" << std::endl; 148 149 // Upload 150 client.uploadBlob(content); 151 152 // Download 153 auto result = client.downloadBlob(digest); 154 155 ASSERT_TRUE(result.has_value()); 156 std::cout << " Downloaded: " << result->size() << " bytes" << std::endl; 157 ASSERT_EQ(content, *result); 158 } 159 160 // Test blobExists returns True after upload 161 TEST(blob_exists_after_upload) { 162 WITH_NATIVELINK_OR_SKIP(); 163 164 CASClient client(testConfig()); 165 166 std::string content = "Existence test blob content 98765 (C++)"; 167 auto digest = digestFromBytes(content); 168 169 // Check before upload (might exist from previous run, that's OK) 170 bool existsBefore = client.blobExists(digest); 171 std::cout << " Exists before upload: " << std::boolalpha << existsBefore << std::endl; 172 173 // Upload 174 client.uploadBlob(content); 175 176 // Check after upload 177 bool existsAfter = client.blobExists(digest); 178 std::cout << " Exists after upload: " << std::boolalpha << existsAfter << std::endl; 179 ASSERT_TRUE(existsAfter); 180 } 181 182 // Test batch operations 183 TEST(batch_upload_download) { 184 WITH_NATIVELINK_OR_SKIP(); 185 186 CASClient client(testConfig()); 187 188 std::vector<std::string> contents = { 189 "Batch blob 1 - cornell C++", 190 "Batch blob 2 - more content here", 191 "Batch blob 3 - even more stuff" 192 }; 193 194 std::vector<std::string_view> views; 195 for (const auto& c : contents) { 196 views.push_back(c); 197 } 198 199 std::cout << " Uploading " << contents.size() << " blobs in batch" << std::endl; 200 201 auto digests = client.uploadBlobs(views); 202 ASSERT_EQ(digests.size(), contents.size()); 203 204 // Download all 205 auto results = client.downloadBlobs(digests); 206 ASSERT_EQ(results.size(), contents.size()); 207 208 for (size_t i = 0; i < contents.size(); ++i) { 209 ASSERT_TRUE(results[i].has_value()); 210 ASSERT_EQ(*results[i], contents[i]); 211 } 212 213 std::cout << " Batch roundtrip successful" << std::endl; 214 } 215 216 // Test find missing blobs with multiple digests 217 TEST(find_missing_blobs_batch) { 218 WITH_NATIVELINK_OR_SKIP(); 219 220 CASClient client(testConfig()); 221 222 // One existing, one non-existing 223 std::string existingContent = "This blob will be uploaded"; 224 auto existingDigest = digestFromBytes(existingContent); 225 client.uploadBlob(existingContent); 226 227 auto missingDigest = digestFromBytes("this-blob-definitely-does-not-exist-xyz"); 228 229 std::vector<Digest> toCheck = {existingDigest, missingDigest}; 230 auto missing = client.findMissingBlobs(toCheck); 231 232 std::cout << " Missing blobs: " << missing.size() << " of " << toCheck.size() << std::endl; 233 234 // Should have exactly 1 missing (the non-uploaded one) 235 ASSERT_EQ(missing.size(), 1u); 236 ASSERT_EQ(missing[0].hash, missingDigest.hash); 237 } 238 239 // Test SHA256 hash computation 240 TEST(digest_from_bytes) { 241 // Known hash for "hello" 242 std::string content = "hello"; 243 auto digest = digestFromBytes(content); 244 245 // SHA256("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 246 ASSERT_EQ(digest.hash, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"); 247 ASSERT_EQ(digest.size, 5); 248 249 std::cout << " hash(\"hello\") = " << digest.hash << std::endl; 250 } 251 252 // Test resource name generation 253 TEST(resource_name_generation) { 254 Digest d{"abc123", 42}; 255 256 auto readName = d.toResourceName("myinstance"); 257 ASSERT_EQ(readName, "myinstance/blobs/abc123/42"); 258 259 auto uploadName = d.toUploadResourceName("myinstance", "uuid-here"); 260 ASSERT_EQ(uploadName, "myinstance/uploads/uuid-here/blobs/abc123/42"); 261 262 std::cout << " read: " << readName << std::endl; 263 std::cout << " upload: " << uploadName << std::endl; 264 } 265 266 } // namespace cornell 267 268 int main() { 269 std::cout << "\n=== Cornell CAS Client Tests ===" << std::endl; 270 std::cout << "================================\n" << std::endl; 271 272 // Tests are auto-registered via static initialization 273 274 std::cout << "\n================================" << std::endl; 275 std::cout << "Tests run: " << cornell::g_tests_run << std::endl; 276 std::cout << "Tests passed: " << cornell::g_tests_passed << std::endl; 277 std::cout << "Tests skipped: " << cornell::g_tests_skipped << std::endl; 278 279 int failed = cornell::g_tests_run - cornell::g_tests_passed - cornell::g_tests_skipped; 280 std::cout << "Tests failed: " << failed << std::endl; 281 282 return failed > 0 ? 1 : 0; 283 }