/ cpp / test / cas_client_test.cpp
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  }