OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 library db_impl_test; |
| 6 |
| 7 import 'dart:async'; |
| 8 |
| 9 import 'package:unittest/unittest.dart'; |
| 10 |
| 11 import 'package:gcloud/db.dart' as db; |
| 12 import 'package:appengine/src/appengine_context.dart'; |
| 13 import 'package:appengine/src/api_impl/raw_datastore_v3_impl.dart'; |
| 14 import 'package:appengine/src/protobuf_api/rpc/rpc_service_remote_api.dart'; |
| 15 |
| 16 @db.Kind() |
| 17 class Person extends db.Model { |
| 18 @db.StringProperty() |
| 19 String name; |
| 20 |
| 21 @db.IntProperty() |
| 22 int age; |
| 23 |
| 24 @db.ModelKeyProperty() |
| 25 db.Key wife; |
| 26 |
| 27 operator==(Object other) => sameAs(other); |
| 28 |
| 29 sameAs(Object other) { |
| 30 return other is Person && |
| 31 id == other.id && |
| 32 parentKey == other.parentKey && |
| 33 name == other.name && |
| 34 age == other.age && |
| 35 wife == other.wife; |
| 36 } |
| 37 |
| 38 String toString() => 'Person(id: $id, name: $name, age: $age)'; |
| 39 } |
| 40 |
| 41 |
| 42 @db.Kind() |
| 43 class User extends Person { |
| 44 @db.StringProperty() |
| 45 String nickname; |
| 46 |
| 47 @db.StringListProperty(propertyName: 'language') |
| 48 List<String> languages = const []; |
| 49 |
| 50 sameAs(Object other) { |
| 51 if (!(super.sameAs(other) && other is User && nickname == other.nickname)) |
| 52 return false; |
| 53 |
| 54 User user = other; |
| 55 if (languages == null) { |
| 56 if (user.languages == null) return true; |
| 57 return false; |
| 58 } |
| 59 if (languages.length != user.languages.length) { |
| 60 return false; |
| 61 } |
| 62 |
| 63 for (int i = 0; i < languages.length; i++) { |
| 64 if (languages[i] != user.languages[i]) { |
| 65 return false; |
| 66 } |
| 67 } |
| 68 return true; |
| 69 } |
| 70 |
| 71 String toString() => |
| 72 'User(${super.toString()}, nickname: $nickname, languages: $languages'; |
| 73 } |
| 74 |
| 75 |
| 76 @db.Kind() |
| 77 class ExpandoPerson extends db.ExpandoModel { |
| 78 @db.StringProperty() |
| 79 String name; |
| 80 |
| 81 @db.StringProperty(propertyName: 'NN') |
| 82 String nickname; |
| 83 |
| 84 operator==(Object other) { |
| 85 if (other is ExpandoPerson && id == other.id && name == other.name) { |
| 86 if (additionalProperties.length != other.additionalProperties.length) { |
| 87 return false; |
| 88 } |
| 89 for (var key in additionalProperties.keys) { |
| 90 if (additionalProperties[key] != other.additionalProperties[key]) { |
| 91 return false; |
| 92 } |
| 93 } |
| 94 return true; |
| 95 } |
| 96 return false; |
| 97 } |
| 98 } |
| 99 |
| 100 |
| 101 Future sleep(Duration duration) { |
| 102 var completer = new Completer(); |
| 103 new Timer(duration, completer.complete); |
| 104 return completer.future; |
| 105 } |
| 106 |
| 107 runTests(db.DatastoreDB store) { |
| 108 void compareModels(List<db.Model> expectedModels, |
| 109 List<db.Model> models, |
| 110 {bool anyOrder: false}) { |
| 111 expect(models.length, equals(expectedModels.length)); |
| 112 if (anyOrder) { |
| 113 // Do expensive O(n^2) search. |
| 114 for (var searchModel in expectedModels) { |
| 115 bool found = false; |
| 116 for (var m in models) { |
| 117 if (m == searchModel) { |
| 118 found = true; |
| 119 break; |
| 120 } |
| 121 } |
| 122 expect(found, isTrue); |
| 123 } |
| 124 } else { |
| 125 for (var i = 0; i < expectedModels.length; i++) { |
| 126 expect(models[i], equals(expectedModels[i])); |
| 127 } |
| 128 } |
| 129 } |
| 130 |
| 131 Future testInsertLookupDelete( |
| 132 List<db.Model> objects, {bool transactional: false}) { |
| 133 var keys = objects.map((db.Model obj) => obj.key).toList(); |
| 134 |
| 135 if (transactional) { |
| 136 return store.withTransaction((db.Transaction commitTransaction) { |
| 137 commitTransaction.queueMutations(inserts: objects); |
| 138 return commitTransaction.commit(); |
| 139 }).then((_) { |
| 140 return store.withTransaction((db.Transaction deleteTransaction) { |
| 141 return deleteTransaction.lookup(keys).then((List<db.Model> models) { |
| 142 compareModels(objects, models); |
| 143 deleteTransaction.queueMutations(deletes: keys); |
| 144 return deleteTransaction.commit(); |
| 145 }); |
| 146 }); |
| 147 }); |
| 148 } else { |
| 149 return store.commit(inserts: objects).then(expectAsync((_) { |
| 150 return store.lookup(keys).then(expectAsync((List<db.Model> models) { |
| 151 compareModels(objects, models); |
| 152 return store.commit(deletes: keys).then(expectAsync((_) { |
| 153 return store.lookup(keys).then(expectAsync((List<db.Model> models) { |
| 154 for (var i = 0; i < models.length; i++) { |
| 155 expect(models[i], isNull); |
| 156 } |
| 157 })); |
| 158 })); |
| 159 })); |
| 160 })); |
| 161 } |
| 162 } |
| 163 |
| 164 group('key', () { |
| 165 test('equal_and_hashcode', () { |
| 166 var k1 = store.emptyKey.append(User, id: 10).append(Person, id: 12); |
| 167 var k2 = store.newPartition(null) |
| 168 .emptyKey.append(User, id: 10).append(Person, id: 12); |
| 169 expect(k1, equals(k2)); |
| 170 expect(k1.hashCode, equals(k2.hashCode)); |
| 171 }); |
| 172 }); |
| 173 |
| 174 group('e2e_db', () { |
| 175 group('insert_lookup_delete', () { |
| 176 test('persons', () { |
| 177 var root = store.emptyKey; |
| 178 var persons = []; |
| 179 for (var i = 1; i <= 10; i++) { |
| 180 persons.add(new Person() |
| 181 ..id = i |
| 182 ..parentKey = root |
| 183 ..age = 42 + i |
| 184 ..name = 'user$i'); |
| 185 } |
| 186 persons.first.wife = persons.last.key; |
| 187 return testInsertLookupDelete(persons); |
| 188 }); |
| 189 test('users', () { |
| 190 var root = store.emptyKey; |
| 191 var users = []; |
| 192 for (var i = 1; i <= 10; i++) { |
| 193 users.add(new User() |
| 194 ..id = i |
| 195 ..parentKey = root |
| 196 ..age = 42 + i |
| 197 ..name = 'user$i' |
| 198 ..nickname = 'nickname${i%3}'); |
| 199 } |
| 200 return testInsertLookupDelete(users); |
| 201 }); |
| 202 test('expando_insert', () { |
| 203 var root = store.emptyKey; |
| 204 var expandoPersons = []; |
| 205 for (var i = 1; i <= 10; i++) { |
| 206 var expandoPerson = new ExpandoPerson() |
| 207 ..parentKey = root |
| 208 ..id = i |
| 209 ..name = 'user$i'; |
| 210 expandoPerson.foo = 'foo$i'; |
| 211 expandoPerson.bar = i; |
| 212 expect(expandoPerson.additionalProperties['foo'], equals('foo$i')); |
| 213 expect(expandoPerson.additionalProperties['bar'], equals(i)); |
| 214 expandoPersons.add(expandoPerson); |
| 215 } |
| 216 return testInsertLookupDelete(expandoPersons); |
| 217 }); |
| 218 test('transactional_insert', () { |
| 219 var root = store.emptyKey; |
| 220 var models = []; |
| 221 |
| 222 models.add(new Person() |
| 223 ..id = 1 |
| 224 ..parentKey = root |
| 225 ..age = 1 |
| 226 ..name = 'user1'); |
| 227 models.add(new User() |
| 228 ..id = 2 |
| 229 ..parentKey = root |
| 230 ..age = 2 |
| 231 ..name = 'user2' |
| 232 ..nickname = 'nickname2'); |
| 233 var expandoPerson = new ExpandoPerson() |
| 234 ..parentKey = root |
| 235 ..id = 3 |
| 236 ..name = 'user1'; |
| 237 expandoPerson.foo = 'foo1'; |
| 238 expandoPerson.bar = 2; |
| 239 |
| 240 return testInsertLookupDelete(models, transactional: true); |
| 241 }); |
| 242 |
| 243 test('parent_key', () { |
| 244 var root = store.emptyKey; |
| 245 var users = []; |
| 246 for (var i = 333; i <= 334; i++) { |
| 247 users.add(new User() |
| 248 ..id = i |
| 249 ..parentKey = root |
| 250 ..age = 42 + i |
| 251 ..name = 'user$i' |
| 252 ..nickname = 'nickname${i%3}'); |
| 253 } |
| 254 var persons = []; |
| 255 for (var i = 335; i <= 336; i++) { |
| 256 persons.add(new Person() |
| 257 ..id = i |
| 258 ..parentKey = root |
| 259 ..age = 42 + i |
| 260 ..name = 'person$i'); |
| 261 } |
| 262 |
| 263 // We test that we can insert + lookup |
| 264 // users[0], (persons[0] + users[0] as parent) |
| 265 // persons[1], (users[1] + persons[0] as parent) |
| 266 persons[0].parentKey = users[0].key; |
| 267 users[1].parentKey = persons[1].key; |
| 268 |
| 269 return testInsertLookupDelete([]..addAll(users)..addAll(persons)); |
| 270 }); |
| 271 |
| 272 test('auto_ids', () { |
| 273 var root = store.emptyKey; |
| 274 var persons = []; |
| 275 persons.add(new Person() |
| 276 ..id = 42 |
| 277 ..parentKey = root |
| 278 ..age = 80 |
| 279 ..name = 'user80'); |
| 280 // Auto id person with parentKey |
| 281 persons.add(new Person() |
| 282 ..parentKey = root |
| 283 ..age = 81 |
| 284 ..name = 'user81'); |
| 285 // Auto id person without parentKey |
| 286 persons.add(new Person() |
| 287 ..age = 82 |
| 288 ..name = 'user82'); |
| 289 // Auto id person with non-root parentKey |
| 290 var fatherKey = persons.first.parentKey; |
| 291 persons.add(new Person() |
| 292 ..parentKey = fatherKey |
| 293 ..age = 83 |
| 294 ..name = 'user83'); |
| 295 persons.add(new Person() |
| 296 ..id = 43 |
| 297 ..parentKey = root |
| 298 ..age = 84 |
| 299 ..name = 'user84'); |
| 300 return store.commit(inserts: persons).then(expectAsync((_) { |
| 301 // At this point, autoIds are allocated and are relfected in the |
| 302 // models (as well as parentKey if it was empty). |
| 303 |
| 304 var keys = persons.map((db.Model obj) => obj.key).toList(); |
| 305 |
| 306 for (var i = 0; i < persons.length; i++) { |
| 307 expect(persons[i].age, equals(80 + i)); |
| 308 expect(persons[i].name, equals('user${80 + i}')); |
| 309 } |
| 310 |
| 311 expect(persons[0].id, equals(42)); |
| 312 expect(persons[0].parentKey, equals(root)); |
| 313 |
| 314 expect(persons[1].id, isNotNull); |
| 315 expect(persons[1].id is int, isTrue); |
| 316 expect(persons[1].parentKey, equals(root)); |
| 317 |
| 318 expect(persons[2].id, isNotNull); |
| 319 expect(persons[2].id is int, isTrue); |
| 320 expect(persons[2].parentKey, equals(root)); |
| 321 |
| 322 expect(persons[3].id, isNotNull); |
| 323 expect(persons[3].id is int, isTrue); |
| 324 expect(persons[3].parentKey, equals(fatherKey)); |
| 325 |
| 326 expect(persons[4].id, equals(43)); |
| 327 expect(persons[4].parentKey, equals(root)); |
| 328 |
| 329 expect(persons[1].id != persons[2].id, isTrue); |
| 330 // NOTE: We can't make assumptions about the id of persons[3], |
| 331 // because an id doesn't need to be globally unique, only under |
| 332 // entities with the same parent. |
| 333 |
| 334 return store.lookup(keys).then(expectAsync((List<Person> models) { |
| 335 // Since the id/parentKey fields are set after commit and a lookup |
| 336 // returns new model instances, we can do full model comparision |
| 337 // here. |
| 338 compareModels(persons, models); |
| 339 return store.commit(deletes: keys).then(expectAsync((_) { |
| 340 return store.lookup(keys).then(expectAsync((List models) { |
| 341 for (var i = 0; i < models.length; i++) { |
| 342 expect(models[i], isNull); |
| 343 } |
| 344 })); |
| 345 })); |
| 346 })); |
| 347 })); |
| 348 }); |
| 349 }); |
| 350 |
| 351 test('query', () { |
| 352 var root = store.emptyKey; |
| 353 var users = []; |
| 354 for (var i = 1; i <= 10; i++) { |
| 355 var languages = []; |
| 356 if (i == 9) { |
| 357 languages = ['foo']; |
| 358 } else if (i == 10) { |
| 359 languages = ['foo', 'bar']; |
| 360 } |
| 361 users.add(new User() |
| 362 ..id = i |
| 363 ..parentKey = root |
| 364 ..age = 42 + i |
| 365 ..name = 'user$i' |
| 366 ..nickname = 'nickname${i%3}' |
| 367 ..languages = languages); |
| 368 } |
| 369 |
| 370 var expandoPersons = []; |
| 371 for (var i = 1; i <= 3; i++) { |
| 372 var expandoPerson = new ExpandoPerson() |
| 373 ..parentKey = root |
| 374 ..id = i |
| 375 ..name = 'user$i' |
| 376 ..nickname = 'nickuser$i'; |
| 377 expandoPerson.foo = 'foo$i'; |
| 378 expandoPerson.bar = i; |
| 379 expect(expandoPerson.additionalProperties['foo'], equals('foo$i')); |
| 380 expect(expandoPerson.additionalProperties['bar'], equals(i)); |
| 381 expandoPersons.add(expandoPerson); |
| 382 } |
| 383 |
| 384 var LOWER_BOUND = 'user2'; |
| 385 |
| 386 var usersSortedNameDescNicknameAsc = new List.from(users); |
| 387 usersSortedNameDescNicknameAsc.sort((User a, User b) { |
| 388 var result = b.name.compareTo(a.name); |
| 389 if (result == 0) return a.nickname.compareTo(b.nickname); |
| 390 return result; |
| 391 }); |
| 392 |
| 393 var usersSortedNameDescNicknameDesc = new List.from(users); |
| 394 usersSortedNameDescNicknameDesc.sort((User a, User b) { |
| 395 var result = b.name.compareTo(a.name); |
| 396 if (result == 0) return b.nickname.compareTo(a.nickname); |
| 397 return result; |
| 398 }); |
| 399 |
| 400 var usersSortedAndFilteredNameDescNicknameAsc = |
| 401 usersSortedNameDescNicknameAsc.where((User u) { |
| 402 return LOWER_BOUND.compareTo(u.name) <= 0; |
| 403 }).toList(); |
| 404 |
| 405 var usersSortedAndFilteredNameDescNicknameDesc = |
| 406 usersSortedNameDescNicknameDesc.where((User u) { |
| 407 return LOWER_BOUND.compareTo(u.name) <= 0; |
| 408 }).toList(); |
| 409 |
| 410 var fooUsers = users.where( |
| 411 (User u) => u.languages.contains('foo')).toList(); |
| 412 var barUsers = users.where( |
| 413 (User u) => u.languages.contains('bar')).toList(); |
| 414 |
| 415 var allInserts = [] |
| 416 ..addAll(users) |
| 417 ..addAll(expandoPersons); |
| 418 var allKeys = allInserts.map((db.Model model) => model.key).toList(); |
| 419 return store.commit(inserts: allInserts).then((_) { |
| 420 return waitUntilEntitiesReady(store, allKeys).then((_) { |
| 421 var tests = [ |
| 422 // Queries for [Person] return no results, we only have [User] |
| 423 // objects. |
| 424 () { |
| 425 return store.query(Person).run().toList() |
| 426 .then((List<db.Model> models) { |
| 427 compareModels([], models); |
| 428 }); |
| 429 }, |
| 430 |
| 431 // All users query |
| 432 () { |
| 433 return store.query(User).run().toList() |
| 434 .then((List<db.Model> models) { |
| 435 compareModels(users, models, anyOrder: true); |
| 436 }); |
| 437 }, |
| 438 |
| 439 // Sorted query |
| 440 () { |
| 441 return store.query(User) |
| 442 ..order('-name') |
| 443 ..order('nickname') |
| 444 ..run().toList().then((List<db.Model> models) { |
| 445 compareModels( |
| 446 usersSortedNameDescNicknameAsc, models); |
| 447 }); |
| 448 }, |
| 449 () { |
| 450 return store.query(User) |
| 451 ..order('-name') |
| 452 ..order('-nickname') |
| 453 ..run().toList().then((List<db.Model> models) { |
| 454 compareModels( |
| 455 usersSortedNameDescNicknameDesc, models); |
| 456 }); |
| 457 }, |
| 458 |
| 459 // Sorted query with filter |
| 460 () { |
| 461 return store.query(User) |
| 462 ..filter('name >=', LOWER_BOUND) |
| 463 ..order('-name') |
| 464 ..order('nickname') |
| 465 ..run().toList().then((List<db.Model> models) { |
| 466 compareModels(usersSortedAndFilteredNameDescNicknameAsc, |
| 467 models); |
| 468 }); |
| 469 }, |
| 470 () { |
| 471 return store.query(User) |
| 472 ..filter('name >=', LOWER_BOUND) |
| 473 ..order('-name') |
| 474 ..order('-nickname') |
| 475 ..run().toList().then((List<db.Model> models) { |
| 476 compareModels(usersSortedAndFilteredNameDescNicknameDesc, |
| 477 models); |
| 478 }); |
| 479 }, |
| 480 |
| 481 // Filter lists |
| 482 /* FIXME: TODO: FIXME: "IN" not supported in public proto/apiary */ |
| 483 () { |
| 484 return store.query(User) |
| 485 ..filter('languages IN', ['foo']) |
| 486 ..order('name') |
| 487 ..run().toList().then((List<db.Model> models) { |
| 488 compareModels(fooUsers, models, anyOrder: true); |
| 489 }); |
| 490 }, |
| 491 () { |
| 492 return store.query(User) |
| 493 ..filter('languages IN', ['bar']) |
| 494 ..order('name') |
| 495 ..run().toList().then((List<db.Model> models) { |
| 496 compareModels(barUsers, models, anyOrder: true); |
| 497 }); |
| 498 }, |
| 499 |
| 500 // Simple limit/offset test. |
| 501 () { |
| 502 return store.query(User) |
| 503 ..order('-name') |
| 504 ..order('nickname') |
| 505 ..offset(3) |
| 506 ..limit(4) |
| 507 ..run().toList().then((List<db.Model> models) { |
| 508 var expectedModels = |
| 509 usersSortedAndFilteredNameDescNicknameAsc.sublist(3, 7); |
| 510 compareModels(expectedModels, models); |
| 511 }); |
| 512 }, |
| 513 |
| 514 // Expando queries: Filter on normal property. |
| 515 () { |
| 516 return store.query(ExpandoPerson) |
| 517 ..filter('name =', expandoPersons.last.name) |
| 518 ..run().toList().then((List<db.Model> models) { |
| 519 compareModels([expandoPersons.last], models); |
| 520 }); |
| 521 }, |
| 522 // Expando queries: Filter on expanded String property |
| 523 () { |
| 524 return store.query(ExpandoPerson) |
| 525 ..filter('foo =', expandoPersons.last.foo) |
| 526 ..run().toList().then((List<db.Model> models) { |
| 527 compareModels([expandoPersons.last], models); |
| 528 }); |
| 529 }, |
| 530 // Expando queries: Filter on expanded int property |
| 531 () { |
| 532 return store.query(ExpandoPerson) |
| 533 ..filter('bar =', expandoPersons.last.bar) |
| 534 ..run().toList().then((List<db.Model> models) { |
| 535 compareModels([expandoPersons.last], models); |
| 536 }); |
| 537 }, |
| 538 // Expando queries: Filter normal property with different |
| 539 // propertyName (datastore name is 'NN'). |
| 540 () { |
| 541 return store.query(ExpandoPerson) |
| 542 ..filter('nickname =', expandoPersons.last.nickname) |
| 543 ..run().toList().then((List<db.Model> models) { |
| 544 compareModels([expandoPersons.last], models); |
| 545 }); |
| 546 }, |
| 547 |
| 548 // Delete results |
| 549 () => store.commit(deletes: allKeys), |
| 550 |
| 551 // Wait until the entity deletes are reflected in the indices. |
| 552 () => waitUntilEntitiesGone(store, allKeys), |
| 553 |
| 554 // Make sure queries don't return results |
| 555 () => store.lookup(allKeys).then((List<db.Model> models) { |
| 556 expect(models.length, equals(allKeys.length)); |
| 557 for (var model in models) { |
| 558 expect(model, isNull); |
| 559 } |
| 560 }), |
| 561 ]; |
| 562 return Future.forEach(tests, (f) => f()); |
| 563 }); |
| 564 }); |
| 565 }); |
| 566 }); |
| 567 } |
| 568 |
| 569 Future waitUntilEntitiesReady(db.DatastoreDB mdb, List<db.Key> keys) { |
| 570 return waitUntilEntitiesHelper(mdb, keys, true); |
| 571 } |
| 572 |
| 573 Future waitUntilEntitiesGone(db.DatastoreDB mdb, List<db.Key> keys) { |
| 574 return waitUntilEntitiesHelper(mdb, keys, false); |
| 575 } |
| 576 |
| 577 Future waitUntilEntitiesHelper(db.DatastoreDB mdb, |
| 578 List<db.Key> keys, |
| 579 bool positive) { |
| 580 var keysByKind = {}; |
| 581 for (var key in keys) { |
| 582 keysByKind.putIfAbsent(key.type, () => []).add(key); |
| 583 } |
| 584 |
| 585 Future waitForKeys(Type kind, List<db.Key> keys) { |
| 586 return mdb.query(kind).run().toList().then((List<db.Model> models) { |
| 587 for (var key in keys) { |
| 588 bool found = false; |
| 589 for (var model in models) { |
| 590 if (key == model.key) found = true; |
| 591 } |
| 592 if (positive) { |
| 593 if (!found) return waitForKeys(kind, keys); |
| 594 } else { |
| 595 if (found) return waitForKeys(kind, keys); |
| 596 } |
| 597 } |
| 598 return null; |
| 599 }); |
| 600 } |
| 601 |
| 602 return Future.forEach(keysByKind.keys.toList(), (Type kind) { |
| 603 return waitForKeys(kind, keysByKind[kind]); |
| 604 }); |
| 605 } |
| 606 |
| 607 void main() { |
| 608 var rpcService = new RPCServiceRemoteApi('127.0.0.1', 4444); |
| 609 var appengineContext = new AppengineContext( |
| 610 'dev', 'test-application', 'test-version', null, null, null); |
| 611 var datastore = |
| 612 new DatastoreV3RpcImpl(rpcService, appengineContext, '<invalid-ticket>'); |
| 613 |
| 614 runTests(new db.DatastoreDB(datastore)); |
| 615 } |
OLD | NEW |