Mercurial > hg > orthanc
comparison UnitTests/ServerIndex.cpp @ 183:baada606da3c
databasewrapper
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Mon, 12 Nov 2012 14:52:30 +0100 |
parents | 93ff5babcaf8 |
children | d4e967d401d3 |
comparison
equal
deleted
inserted
replaced
182:93ff5babcaf8 | 183:baada606da3c |
---|---|
1 #include "gtest/gtest.h" | 1 #include "gtest/gtest.h" |
2 | 2 |
3 #include "../OrthancServer/DatabaseWrapper.h" | |
4 | |
3 #include <ctype.h> | 5 #include <ctype.h> |
4 | |
5 #include "../Core/SQLite/Connection.h" | |
6 #include "../Core/Compression/ZlibCompressor.h" | |
7 #include "../Core/DicomFormat/DicomTag.h" | |
8 #include "../Core/DicomFormat/DicomArray.h" | |
9 #include "../Core/FileStorage.h" | |
10 #include "../OrthancCppClient/HttpClient.h" | |
11 #include "../Core/HttpServer/HttpHandler.h" | |
12 #include "../Core/OrthancException.h" | |
13 #include "../Core/Toolbox.h" | |
14 #include "../Core/Uuid.h" | |
15 #include "../OrthancServer/FromDcmtkBridge.h" | |
16 #include "../OrthancServer/OrthancInitialization.h" | |
17 #include "../OrthancServer/ServerIndex.h" | |
18 #include "EmbeddedResources.h" | |
19 | |
20 #include <glog/logging.h> | 6 #include <glog/logging.h> |
21 #include <boost/thread.hpp> | 7 |
22 | 8 |
23 | 9 using namespace Orthanc; |
24 namespace Orthanc | 10 |
11 namespace | |
25 { | 12 { |
26 enum CompressionType | |
27 { | |
28 CompressionType_None = 1, | |
29 CompressionType_Zlib = 2 | |
30 }; | |
31 | |
32 enum MetadataType | |
33 { | |
34 MetadataType_Instance_RemoteAet = 1, | |
35 MetadataType_Instance_IndexInSeries = 2, | |
36 MetadataType_Series_ExpectedNumberOfInstances = 3 | |
37 }; | |
38 | |
39 class IServerIndexListener | |
40 { | |
41 public: | |
42 virtual ~IServerIndexListener() | |
43 { | |
44 } | |
45 | |
46 virtual void SignalResourceDeleted(ResourceType type, | |
47 const std::string& parentPublicId) = 0; | |
48 | |
49 virtual void SignalFileDeleted(const std::string& fileUuid) = 0; | |
50 | |
51 }; | |
52 | |
53 namespace Internals | |
54 { | |
55 class SignalFileDeleted : public SQLite::IScalarFunction | |
56 { | |
57 private: | |
58 IServerIndexListener& listener_; | |
59 | |
60 public: | |
61 SignalFileDeleted(IServerIndexListener& listener) : | |
62 listener_(listener) | |
63 { | |
64 } | |
65 | |
66 virtual const char* GetName() const | |
67 { | |
68 return "SignalFileDeleted"; | |
69 } | |
70 | |
71 virtual unsigned int GetCardinality() const | |
72 { | |
73 return 1; | |
74 } | |
75 | |
76 virtual void Compute(SQLite::FunctionContext& context) | |
77 { | |
78 listener_.SignalFileDeleted(context.GetStringValue(0)); | |
79 } | |
80 }; | |
81 | |
82 class SignalResourceDeleted : public SQLite::IScalarFunction | |
83 { | |
84 public: | |
85 virtual const char* GetName() const | |
86 { | |
87 return "SignalResourceDeleted"; | |
88 } | |
89 | |
90 virtual unsigned int GetCardinality() const | |
91 { | |
92 return 2; | |
93 } | |
94 | |
95 virtual void Compute(SQLite::FunctionContext& context) | |
96 { | |
97 LOG(INFO) << "A resource has been removed, of type " | |
98 << context.GetIntValue(0) | |
99 << ", with parent " | |
100 << context.GetIntValue(1); | |
101 } | |
102 }; | |
103 } | |
104 | |
105 | |
106 class ServerIndexHelper | |
107 { | |
108 private: | |
109 IServerIndexListener& listener_; | |
110 SQLite::Connection db_; | |
111 boost::mutex mutex_; | |
112 | |
113 void Open(const std::string& path); | |
114 | |
115 public: | |
116 void SetGlobalProperty(const std::string& name, | |
117 const std::string& value) | |
118 { | |
119 SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO GlobalProperties VALUES(?, ?)"); | |
120 s.BindString(0, name); | |
121 s.BindString(1, value); | |
122 s.Run(); | |
123 } | |
124 | |
125 bool FindGlobalProperty(std::string& target, | |
126 const std::string& name) | |
127 { | |
128 SQLite::Statement s(db_, SQLITE_FROM_HERE, | |
129 "SELECT value FROM GlobalProperties WHERE name=?"); | |
130 s.BindString(0, name); | |
131 | |
132 if (!s.Step()) | |
133 { | |
134 return false; | |
135 } | |
136 else | |
137 { | |
138 target = s.ColumnString(0); | |
139 return true; | |
140 } | |
141 } | |
142 | |
143 std::string GetGlobalProperty(const std::string& name, | |
144 const std::string& defaultValue = "") | |
145 { | |
146 std::string s; | |
147 if (FindGlobalProperty(s, name)) | |
148 { | |
149 return s; | |
150 } | |
151 else | |
152 { | |
153 return defaultValue; | |
154 } | |
155 } | |
156 | |
157 int64_t CreateResource(const std::string& publicId, | |
158 ResourceType type) | |
159 { | |
160 SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO Resources VALUES(NULL, ?, ?, NULL)"); | |
161 s.BindInt(0, type); | |
162 s.BindString(1, publicId); | |
163 s.Run(); | |
164 return db_.GetLastInsertRowId(); | |
165 } | |
166 | |
167 bool FindResource(const std::string& publicId, | |
168 int64_t& id, | |
169 ResourceType& type) | |
170 { | |
171 SQLite::Statement s(db_, SQLITE_FROM_HERE, | |
172 "SELECT internalId, resourceType FROM Resources WHERE publicId=?"); | |
173 s.BindString(0, publicId); | |
174 | |
175 if (!s.Step()) | |
176 { | |
177 return false; | |
178 } | |
179 else | |
180 { | |
181 id = s.ColumnInt(0); | |
182 type = static_cast<ResourceType>(s.ColumnInt(1)); | |
183 | |
184 // Check whether there is a single resource with this public id | |
185 assert(!s.Step()); | |
186 | |
187 return true; | |
188 } | |
189 } | |
190 | |
191 void AttachChild(int64_t parent, | |
192 int64_t child) | |
193 { | |
194 SQLite::Statement s(db_, SQLITE_FROM_HERE, "UPDATE Resources SET parentId = ? WHERE internalId = ?"); | |
195 s.BindInt(0, parent); | |
196 s.BindInt(1, child); | |
197 s.Run(); | |
198 } | |
199 | |
200 void DeleteResource(int64_t id) | |
201 { | |
202 SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Resources WHERE internalId=?"); | |
203 s.BindInt(0, id); | |
204 s.Run(); | |
205 } | |
206 | |
207 void SetMetadata(int64_t id, | |
208 MetadataType type, | |
209 const std::string& value) | |
210 { | |
211 SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT OR REPLACE INTO Metadata VALUES(?, ?, ?)"); | |
212 s.BindInt(0, id); | |
213 s.BindInt(1, type); | |
214 s.BindString(2, value); | |
215 s.Run(); | |
216 } | |
217 | |
218 bool FindMetadata(std::string& target, | |
219 int64_t id, | |
220 MetadataType type) | |
221 { | |
222 SQLite::Statement s(db_, SQLITE_FROM_HERE, | |
223 "SELECT value FROM Metadata WHERE id=? AND type=?"); | |
224 s.BindInt(0, id); | |
225 s.BindInt(1, type); | |
226 | |
227 if (!s.Step()) | |
228 { | |
229 return false; | |
230 } | |
231 else | |
232 { | |
233 target = s.ColumnString(0); | |
234 return true; | |
235 } | |
236 } | |
237 | |
238 std::string GetMetadata(int64_t id, | |
239 MetadataType type, | |
240 const std::string& defaultValue = "") | |
241 { | |
242 std::string s; | |
243 if (FindMetadata(s, id, type)) | |
244 { | |
245 return s; | |
246 } | |
247 else | |
248 { | |
249 return defaultValue; | |
250 } | |
251 } | |
252 | |
253 void AttachFile(int64_t id, | |
254 const std::string& name, | |
255 const std::string& fileUuid, | |
256 size_t uncompressedSize, | |
257 CompressionType compressionType) | |
258 { | |
259 SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO AttachedFiles VALUES(?, ?, ?, ?, ?)"); | |
260 s.BindInt(0, id); | |
261 s.BindString(1, name); | |
262 s.BindString(2, fileUuid); | |
263 s.BindInt(3, uncompressedSize); | |
264 s.BindInt(4, compressionType); | |
265 s.Run(); | |
266 } | |
267 | |
268 bool FindFile(int64_t id, | |
269 const std::string& name, | |
270 std::string& fileUuid, | |
271 size_t& uncompressedSize, | |
272 CompressionType& compressionType) | |
273 { | |
274 SQLite::Statement s(db_, SQLITE_FROM_HERE, | |
275 "SELECT uuid, uncompressedSize, compressionType FROM AttachedFiles WHERE id=? AND name=?"); | |
276 s.BindInt(0, id); | |
277 s.BindString(1, name); | |
278 | |
279 if (!s.Step()) | |
280 { | |
281 return false; | |
282 } | |
283 else | |
284 { | |
285 fileUuid = s.ColumnString(0); | |
286 uncompressedSize = s.ColumnInt(1); | |
287 compressionType = static_cast<CompressionType>(s.ColumnInt(2)); | |
288 return true; | |
289 } | |
290 } | |
291 | |
292 void SetMainDicomTags(int64_t id, | |
293 const DicomMap& tags) | |
294 { | |
295 DicomArray flattened(tags); | |
296 for (size_t i = 0; i < flattened.GetSize(); i++) | |
297 { | |
298 SQLite::Statement s(db_, SQLITE_FROM_HERE, "INSERT INTO MainDicomTags VALUES(?, ?, ?, ?)"); | |
299 s.BindInt(0, id); | |
300 s.BindInt(1, flattened.GetElement(i).GetTag().GetGroup()); | |
301 s.BindInt(2, flattened.GetElement(i).GetTag().GetElement()); | |
302 s.BindString(3, flattened.GetElement(i).GetValue().AsString()); | |
303 s.Run(); | |
304 } | |
305 } | |
306 | |
307 void GetMainDicomTags(DicomMap& map, | |
308 int64_t id) | |
309 { | |
310 map.Clear(); | |
311 | |
312 SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM MainDicomTags WHERE id=?"); | |
313 s.BindInt(0, id); | |
314 while (s.Step()) | |
315 { | |
316 map.SetValue(s.ColumnInt(1), | |
317 s.ColumnInt(2), | |
318 s.ColumnString(3)); | |
319 } | |
320 } | |
321 | |
322 | |
323 bool GetParentPublicId(std::string& result, | |
324 int64_t id) | |
325 { | |
326 SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b " | |
327 "WHERE a.internalId = b.parentId AND b.internalId = ?"); | |
328 s.BindInt(0, id); | |
329 | |
330 if (s.Step()) | |
331 { | |
332 result = s.ColumnString(0); | |
333 return true; | |
334 } | |
335 else | |
336 { | |
337 return false; | |
338 } | |
339 } | |
340 | |
341 | |
342 void GetChildrenPublicId(std::list<std::string>& result, | |
343 int64_t id) | |
344 { | |
345 SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT a.publicId FROM Resources AS a, Resources AS b " | |
346 "WHERE a.parentId = b.internalId AND b.internalId = ?"); | |
347 s.BindInt(0, id); | |
348 | |
349 result.clear(); | |
350 | |
351 while (s.Step()) | |
352 { | |
353 result.push_back(s.ColumnString(0)); | |
354 } | |
355 } | |
356 | |
357 | |
358 int64_t GetTableRecordCount(const std::string& table) | |
359 { | |
360 char buf[128]; | |
361 sprintf(buf, "SELECT COUNT(*) FROM %s", table.c_str()); | |
362 SQLite::Statement s(db_, buf); | |
363 | |
364 assert(s.Step()); | |
365 int64_t c = s.ColumnInt(0); | |
366 assert(!s.Step()); | |
367 | |
368 return c; | |
369 } | |
370 | |
371 ServerIndexHelper(const std::string& path, | |
372 IServerIndexListener& listener) : | |
373 listener_(listener) | |
374 { | |
375 Open(path); | |
376 } | |
377 | |
378 ServerIndexHelper(IServerIndexListener& listener) : | |
379 listener_(listener) | |
380 { | |
381 Open(""); | |
382 } | |
383 }; | |
384 | |
385 | |
386 | |
387 void ServerIndexHelper::Open(const std::string& path) | |
388 { | |
389 if (path == "") | |
390 { | |
391 db_.OpenInMemory(); | |
392 } | |
393 else | |
394 { | |
395 db_.Open(path); | |
396 } | |
397 | |
398 if (!db_.DoesTableExist("GlobalProperties")) | |
399 { | |
400 LOG(INFO) << "Creating the database"; | |
401 std::string query; | |
402 EmbeddedResources::GetFileResource(query, EmbeddedResources::PREPARE_DATABASE_2); | |
403 db_.Execute(query); | |
404 } | |
405 | |
406 db_.Register(new Internals::SignalFileDeleted(listener_)); | |
407 db_.Register(new Internals::SignalResourceDeleted); | |
408 } | |
409 | |
410 | |
411 class ServerIndexListener : public IServerIndexListener | 13 class ServerIndexListener : public IServerIndexListener |
412 { | 14 { |
413 public: | 15 public: |
414 virtual void SignalResourceDeleted(ResourceType type, | 16 std::set<std::string> deletedFiles_; |
415 const std::string& parentPublicId) | 17 std::string ancestorId_; |
18 ResourceType ancestorType_; | |
19 | |
20 void Reset() | |
416 { | 21 { |
22 ancestorId_ = ""; | |
23 deletedFiles_.clear(); | |
24 } | |
25 | |
26 virtual void SignalRemainingAncestor(ResourceType type, | |
27 const std::string& publicId) | |
28 { | |
29 ancestorId_ = publicId; | |
30 ancestorType_ = type; | |
417 } | 31 } |
418 | 32 |
419 virtual void SignalFileDeleted(const std::string& fileUuid) | 33 virtual void SignalFileDeleted(const std::string& fileUuid) |
420 { | 34 { |
35 deletedFiles_.insert(fileUuid); | |
421 LOG(INFO) << "A file must be removed: " << fileUuid; | 36 LOG(INFO) << "A file must be removed: " << fileUuid; |
422 } | 37 } |
423 }; | 38 }; |
424 | |
425 /* | |
426 class ServerIndex2 | |
427 { | |
428 private: | |
429 ServerIndexListener listener_; | |
430 ServerIndexHelper helper_; | |
431 | |
432 void Open(const std::string& storagePath) | |
433 { | |
434 boost::filesystem::path p = storagePath; | |
435 | |
436 try | |
437 { | |
438 boost::filesystem::create_directories(storagePath); | |
439 } | |
440 catch (boost::filesystem::filesystem_error) | |
441 { | |
442 } | |
443 | |
444 p /= "index"; | |
445 } | |
446 | |
447 public: | |
448 ServerIndexHelper(const std::string& storagePath) : | |
449 helper_(storagePath) | |
450 { | |
451 Open(storagePath); | |
452 } | |
453 }; | |
454 */ | |
455 } | 39 } |
456 | 40 |
457 | 41 |
458 | 42 TEST(DatabaseWrapper, Simple) |
459 using namespace Orthanc; | |
460 | |
461 TEST(ServerIndexHelper, Simple) | |
462 { | 43 { |
463 ServerIndexListener listener; | 44 ServerIndexListener listener; |
464 /*Toolbox::RemoveFile("toto"); | 45 DatabaseWrapper index(listener); |
465 ServerIndexHelper index("toto", listener);*/ | |
466 ServerIndexHelper index(listener); | |
467 | |
468 LOG(WARNING) << "ok"; | |
469 | 46 |
470 int64_t a[] = { | 47 int64_t a[] = { |
471 index.CreateResource("a", ResourceType_Patient), // 0 | 48 index.CreateResource("a", ResourceType_Patient), // 0 |
472 index.CreateResource("b", ResourceType_Study), // 1 | 49 index.CreateResource("b", ResourceType_Study), // 1 |
473 index.CreateResource("c", ResourceType_Series), // 2 | 50 index.CreateResource("c", ResourceType_Series), // 2 |
512 { | 89 { |
513 ASSERT_EQ("d", l.back()); | 90 ASSERT_EQ("d", l.back()); |
514 ASSERT_EQ("e", l.front()); | 91 ASSERT_EQ("e", l.front()); |
515 } | 92 } |
516 | 93 |
517 | 94 index.AttachFile(a[4], "_json", "my json file", 21, 42, CompressionType_Zlib); |
518 index.AttachFile(a[4], "_json", "my json file", 42, CompressionType_Zlib); | 95 index.AttachFile(a[4], "_dicom", "my dicom file", 42); |
519 index.AttachFile(a[4], "_dicom", "my dicom file", 42, CompressionType_None); | 96 index.AttachFile(a[6], "_hello", "world", 44); |
520 index.SetMetadata(a[4], MetadataType_Instance_RemoteAet, "PINNACLE"); | 97 index.SetMetadata(a[4], MetadataType_Instance_RemoteAet, "PINNACLE"); |
98 | |
99 ASSERT_EQ(21 + 42 + 44, index.GetTotalCompressedSize()); | |
100 ASSERT_EQ(42 + 42 + 44, index.GetTotalUncompressedSize()); | |
521 | 101 |
522 DicomMap m; | 102 DicomMap m; |
523 m.SetValue(0x0010, 0x0010, "PatientName"); | 103 m.SetValue(0x0010, 0x0010, "PatientName"); |
524 index.SetMainDicomTags(a[3], m); | 104 index.SetMainDicomTags(a[3], m); |
525 | 105 |
539 ASSERT_FALSE(index.FindGlobalProperty(s, "Hello2")); | 119 ASSERT_FALSE(index.FindGlobalProperty(s, "Hello2")); |
540 ASSERT_EQ("World", s); | 120 ASSERT_EQ("World", s); |
541 ASSERT_EQ("World", index.GetGlobalProperty("Hello")); | 121 ASSERT_EQ("World", index.GetGlobalProperty("Hello")); |
542 ASSERT_EQ("None", index.GetGlobalProperty("Hello2", "None")); | 122 ASSERT_EQ("None", index.GetGlobalProperty("Hello2", "None")); |
543 | 123 |
544 size_t us; | 124 size_t us, cs; |
545 CompressionType ct; | 125 CompressionType ct; |
546 ASSERT_TRUE(index.FindFile(a[4], "_json", s, us, ct)); | 126 ASSERT_TRUE(index.FindFile(a[4], "_json", s, cs, us, ct)); |
547 ASSERT_EQ("my json file", s); | 127 ASSERT_EQ("my json file", s); |
128 ASSERT_EQ(21, cs); | |
548 ASSERT_EQ(42, us); | 129 ASSERT_EQ(42, us); |
549 ASSERT_EQ(CompressionType_Zlib, ct); | 130 ASSERT_EQ(CompressionType_Zlib, ct); |
550 | 131 |
132 ASSERT_EQ(0, listener.deletedFiles_.size()); | |
551 ASSERT_EQ(7, index.GetTableRecordCount("Resources")); | 133 ASSERT_EQ(7, index.GetTableRecordCount("Resources")); |
552 ASSERT_EQ(2, index.GetTableRecordCount("AttachedFiles")); | 134 ASSERT_EQ(3, index.GetTableRecordCount("AttachedFiles")); |
553 ASSERT_EQ(1, index.GetTableRecordCount("Metadata")); | 135 ASSERT_EQ(1, index.GetTableRecordCount("Metadata")); |
554 ASSERT_EQ(1, index.GetTableRecordCount("MainDicomTags")); | 136 ASSERT_EQ(1, index.GetTableRecordCount("MainDicomTags")); |
555 index.DeleteResource(a[0]); | 137 index.DeleteResource(a[0]); |
138 | |
139 ASSERT_EQ(2, listener.deletedFiles_.size()); | |
140 ASSERT_NE(listener.deletedFiles_.end(), listener.deletedFiles_.find("my json file")); | |
141 ASSERT_NE(listener.deletedFiles_.end(), listener.deletedFiles_.find("my dicom file")); | |
142 | |
556 ASSERT_EQ(2, index.GetTableRecordCount("Resources")); | 143 ASSERT_EQ(2, index.GetTableRecordCount("Resources")); |
557 ASSERT_EQ(0, index.GetTableRecordCount("Metadata")); | 144 ASSERT_EQ(0, index.GetTableRecordCount("Metadata")); |
145 ASSERT_EQ(1, index.GetTableRecordCount("AttachedFiles")); | |
146 ASSERT_EQ(0, index.GetTableRecordCount("MainDicomTags")); | |
147 index.DeleteResource(a[5]); | |
148 ASSERT_EQ(0, index.GetTableRecordCount("Resources")); | |
558 ASSERT_EQ(0, index.GetTableRecordCount("AttachedFiles")); | 149 ASSERT_EQ(0, index.GetTableRecordCount("AttachedFiles")); |
559 ASSERT_EQ(0, index.GetTableRecordCount("MainDicomTags")); | 150 ASSERT_EQ(1, index.GetTableRecordCount("GlobalProperties")); |
151 | |
152 ASSERT_EQ(3, listener.deletedFiles_.size()); | |
153 ASSERT_NE(listener.deletedFiles_.end(), listener.deletedFiles_.find("world")); | |
154 } | |
155 | |
156 | |
157 | |
158 | |
159 TEST(DatabaseWrapper, Upward) | |
160 { | |
161 ServerIndexListener listener; | |
162 DatabaseWrapper index(listener); | |
163 | |
164 int64_t a[] = { | |
165 index.CreateResource("a", ResourceType_Patient), // 0 | |
166 index.CreateResource("b", ResourceType_Study), // 1 | |
167 index.CreateResource("c", ResourceType_Series), // 2 | |
168 index.CreateResource("d", ResourceType_Instance), // 3 | |
169 index.CreateResource("e", ResourceType_Instance), // 4 | |
170 index.CreateResource("f", ResourceType_Study), // 5 | |
171 index.CreateResource("g", ResourceType_Series), // 6 | |
172 index.CreateResource("h", ResourceType_Series) // 7 | |
173 }; | |
174 | |
175 index.AttachChild(a[0], a[1]); | |
176 index.AttachChild(a[1], a[2]); | |
177 index.AttachChild(a[2], a[3]); | |
178 index.AttachChild(a[2], a[4]); | |
179 index.AttachChild(a[1], a[6]); | |
180 index.AttachChild(a[0], a[5]); | |
181 index.AttachChild(a[5], a[7]); | |
182 | |
183 listener.Reset(); | |
184 index.DeleteResource(a[3]); | |
185 ASSERT_EQ("c", listener.ancestorId_); | |
186 ASSERT_EQ(ResourceType_Series, listener.ancestorType_); | |
187 | |
188 listener.Reset(); | |
189 index.DeleteResource(a[4]); | |
190 ASSERT_EQ("b", listener.ancestorId_); | |
191 ASSERT_EQ(ResourceType_Study, listener.ancestorType_); | |
192 | |
193 listener.Reset(); | |
194 index.DeleteResource(a[7]); | |
195 ASSERT_EQ("a", listener.ancestorId_); | |
196 ASSERT_EQ(ResourceType_Patient, listener.ancestorType_); | |
197 | |
198 listener.Reset(); | |
560 index.DeleteResource(a[6]); | 199 index.DeleteResource(a[6]); |
561 ASSERT_EQ(0, index.GetTableRecordCount("Resources")); | 200 ASSERT_EQ("", listener.ancestorId_); // No more ancestor |
562 ASSERT_EQ(1, index.GetTableRecordCount("GlobalProperties")); | |
563 } | 201 } |