1<?php 2/* 3 * 4 * Copyright 2018 gRPC authors. 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 * 18 */ 19 20class ChannelTest extends PHPUnit_Framework_TestCase 21{ 22 public function setUp() 23 { 24 } 25 26 public function tearDown() 27 { 28 if (!empty($this->channel)) { 29 $this->channel->close(); 30 } 31 } 32 33 public function testInsecureCredentials() 34 { 35 $this->channel = new Grpc\Channel('localhost:50000', 36 ['credentials' => Grpc\ChannelCredentials::createInsecure()]); 37 $this->assertSame('Grpc\Channel', get_class($this->channel)); 38 } 39 40 public function testGetConnectivityState() 41 { 42 $this->channel = new Grpc\Channel('localhost:50001', 43 ['credentials' => Grpc\ChannelCredentials::createInsecure()]); 44 $state = $this->channel->getConnectivityState(); 45 $this->assertEquals(0, $state); 46 } 47 48 public function testGetConnectivityStateWithInt() 49 { 50 $this->channel = new Grpc\Channel('localhost:50002', 51 ['credentials' => Grpc\ChannelCredentials::createInsecure()]); 52 $state = $this->channel->getConnectivityState(123); 53 $this->assertEquals(0, $state); 54 } 55 56 public function testGetConnectivityStateWithString() 57 { 58 $this->channel = new Grpc\Channel('localhost:50003', 59 ['credentials' => Grpc\ChannelCredentials::createInsecure()]); 60 $state = $this->channel->getConnectivityState('hello'); 61 $this->assertEquals(0, $state); 62 } 63 64 public function testGetConnectivityStateWithBool() 65 { 66 $this->channel = new Grpc\Channel('localhost:50004', 67 ['credentials' => Grpc\ChannelCredentials::createInsecure()]); 68 $state = $this->channel->getConnectivityState(true); 69 $this->assertEquals(0, $state); 70 } 71 72 public function testGetTarget() 73 { 74 $this->channel = new Grpc\Channel('localhost:50005', 75 ['credentials' => Grpc\ChannelCredentials::createInsecure()]); 76 $target = $this->channel->getTarget(); 77 $this->assertTrue(is_string($target)); 78 } 79 80 public function testWatchConnectivityState() 81 { 82 $this->channel = new Grpc\Channel('localhost:50006', 83 ['credentials' => Grpc\ChannelCredentials::createInsecure()]); 84 $now = Grpc\Timeval::now(); 85 $deadline = $now->add(new Grpc\Timeval(100*1000)); // 100ms 86 // we act as if 'CONNECTING'(=1) was the last state 87 // we saw, so the default state of 'IDLE' should be delivered instantly 88 $state = $this->channel->watchConnectivityState(1, $deadline); 89 $this->assertTrue($state); 90 unset($now); 91 unset($deadline); 92 } 93 94 public function testClose() 95 { 96 $this->channel = new Grpc\Channel('localhost:50007', 97 ['credentials' => Grpc\ChannelCredentials::createInsecure()]); 98 $this->assertNotNull($this->channel); 99 $this->channel->close(); 100 } 101 102 /** 103 * @expectedException InvalidArgumentException 104 */ 105 public function testInvalidConstructorWithNull() 106 { 107 $this->channel = new Grpc\Channel(); 108 $this->assertNull($this->channel); 109 } 110 111 /** 112 * @expectedException InvalidArgumentException 113 */ 114 public function testInvalidConstructorWith() 115 { 116 $this->channel = new Grpc\Channel('localhost:50008', 'invalid'); 117 $this->assertNull($this->channel); 118 } 119 120 /** 121 * @expectedException InvalidArgumentException 122 */ 123 public function testInvalidCredentials() 124 { 125 $this->channel = new Grpc\Channel('localhost:50009', 126 ['credentials' => new Grpc\Timeval(100)]); 127 } 128 129 /** 130 * @expectedException InvalidArgumentException 131 */ 132 public function testInvalidOptionsArray() 133 { 134 $this->channel = new Grpc\Channel('localhost:50010', 135 ['abc' => []]); 136 } 137 138 /** 139 * @expectedException InvalidArgumentException 140 */ 141 public function testInvalidGetConnectivityStateWithArray() 142 { 143 $this->channel = new Grpc\Channel('localhost:50011', 144 ['credentials' => Grpc\ChannelCredentials::createInsecure()]); 145 $this->channel->getConnectivityState([]); 146 } 147 148 /** 149 * @expectedException InvalidArgumentException 150 */ 151 public function testInvalidWatchConnectivityState() 152 { 153 $this->channel = new Grpc\Channel('localhost:50012', 154 ['credentials' => Grpc\ChannelCredentials::createInsecure()]); 155 $this->channel->watchConnectivityState([]); 156 } 157 158 /** 159 * @expectedException InvalidArgumentException 160 */ 161 public function testInvalidWatchConnectivityState2() 162 { 163 $this->channel = new Grpc\Channel('localhost:50013', 164 ['credentials' => Grpc\ChannelCredentials::createInsecure()]); 165 $this->channel->watchConnectivityState(1, 'hi'); 166 } 167 168 169 public function assertConnecting($state) { 170 $this->assertTrue($state == GRPC\CHANNEL_CONNECTING || 171 $state == GRPC\CHANNEL_TRANSIENT_FAILURE); 172 } 173 174 public function waitUntilNotIdle($channel) { 175 for ($i = 0; $i < 10; $i++) { 176 $now = Grpc\Timeval::now(); 177 $deadline = $now->add(new Grpc\Timeval(1000)); 178 if ($channel->watchConnectivityState(GRPC\CHANNEL_IDLE, 179 $deadline)) { 180 return true; 181 } 182 } 183 $this->assertTrue(false); 184 } 185 186 public function testPersistentChannelSameHost() 187 { 188 $this->channel1 = new Grpc\Channel('localhost:50014', [ 189 "grpc_target_persist_bound" => 3, 190 ]); 191 // the underlying grpc channel is the same by default 192 // when connecting to the same host 193 $this->channel2 = new Grpc\Channel('localhost:50014', []); 194 195 // both channels should be IDLE 196 $state = $this->channel1->getConnectivityState(); 197 $this->assertEquals(GRPC\CHANNEL_IDLE, $state); 198 $state = $this->channel2->getConnectivityState(); 199 $this->assertEquals(GRPC\CHANNEL_IDLE, $state); 200 201 // try to connect on channel1 202 $state = $this->channel1->getConnectivityState(true); 203 $this->waitUntilNotIdle($this->channel1); 204 205 // both channels should now be in the CONNECTING state 206 $state = $this->channel1->getConnectivityState(); 207 $this->assertConnecting($state); 208 $state = $this->channel2->getConnectivityState(); 209 $this->assertConnecting($state); 210 211 $this->channel1->close(); 212 $this->channel2->close(); 213 } 214 215 public function testPersistentChannelDifferentHost() 216 { 217 // two different underlying channels because different hostname 218 $this->channel1 = new Grpc\Channel('localhost:50015', [ 219 "grpc_target_persist_bound" => 3, 220 ]); 221 $this->channel2 = new Grpc\Channel('localhost:50016', []); 222 223 // both channels should be IDLE 224 $state = $this->channel1->getConnectivityState(); 225 $this->assertEquals(GRPC\CHANNEL_IDLE, $state); 226 $state = $this->channel2->getConnectivityState(); 227 $this->assertEquals(GRPC\CHANNEL_IDLE, $state); 228 229 // try to connect on channel1 230 $state = $this->channel1->getConnectivityState(true); 231 $this->waitUntilNotIdle($this->channel1); 232 233 // channel1 should now be in the CONNECTING state 234 $state = $this->channel1->getConnectivityState(); 235 $this->assertConnecting($state); 236 // channel2 should still be in the IDLE state 237 $state = $this->channel2->getConnectivityState(); 238 $this->assertEquals(GRPC\CHANNEL_IDLE, $state); 239 240 $this->channel1->close(); 241 $this->channel2->close(); 242 } 243 244 public function testPersistentChannelSameArgs() 245 { 246 $this->channel1 = new Grpc\Channel('localhost:50017', [ 247 "grpc_target_persist_bound" => 3, 248 "abc" => "def", 249 ]); 250 $this->channel2 = new Grpc\Channel('localhost:50017', ["abc" => "def"]); 251 252 // try to connect on channel1 253 $state = $this->channel1->getConnectivityState(true); 254 $this->waitUntilNotIdle($this->channel1); 255 256 $state = $this->channel1->getConnectivityState(); 257 $this->assertConnecting($state); 258 $state = $this->channel2->getConnectivityState(); 259 $this->assertConnecting($state); 260 261 $this->channel1->close(); 262 $this->channel2->close(); 263 } 264 265 public function testPersistentChannelDifferentArgs() 266 { 267 $this->channel1 = new Grpc\Channel('localhost:50018', [ 268 "grpc_target_persist_bound" => 3, 269 ]); 270 $this->channel2 = new Grpc\Channel('localhost:50018', ["abc" => "def"]); 271 272 // try to connect on channel1 273 $state = $this->channel1->getConnectivityState(true); 274 $this->waitUntilNotIdle($this->channel1); 275 276 $state = $this->channel1->getConnectivityState(); 277 $this->assertConnecting($state); 278 $state = $this->channel2->getConnectivityState(); 279 $this->assertEquals(GRPC\CHANNEL_IDLE, $state); 280 281 $this->channel1->close(); 282 $this->channel2->close(); 283 } 284 285 public function testPersistentChannelSameChannelCredentials() 286 { 287 $creds1 = Grpc\ChannelCredentials::createSsl(); 288 $creds2 = Grpc\ChannelCredentials::createSsl(); 289 290 $this->channel1 = new Grpc\Channel('localhost:50019', 291 ["credentials" => $creds1, 292 "grpc_target_persist_bound" => 3, 293 ]); 294 $this->channel2 = new Grpc\Channel('localhost:50019', 295 ["credentials" => $creds2]); 296 297 // try to connect on channel1 298 $state = $this->channel1->getConnectivityState(true); 299 $this->waitUntilNotIdle($this->channel1); 300 301 $state = $this->channel1->getConnectivityState(); 302 $this->assertConnecting($state); 303 $state = $this->channel2->getConnectivityState(); 304 $this->assertConnecting($state); 305 306 $this->channel1->close(); 307 $this->channel2->close(); 308 } 309 310 public function testPersistentChannelDifferentChannelCredentials() 311 { 312 $creds1 = Grpc\ChannelCredentials::createSsl(); 313 $creds2 = Grpc\ChannelCredentials::createSsl( 314 file_get_contents(dirname(__FILE__).'/../data/ca.pem')); 315 316 $this->channel1 = new Grpc\Channel('localhost:50020', 317 ["credentials" => $creds1, 318 "grpc_target_persist_bound" => 3, 319 ]); 320 $this->channel2 = new Grpc\Channel('localhost:50020', 321 ["credentials" => $creds2]); 322 323 // try to connect on channel1 324 $state = $this->channel1->getConnectivityState(true); 325 $this->waitUntilNotIdle($this->channel1); 326 327 $state = $this->channel1->getConnectivityState(); 328 $this->assertConnecting($state); 329 $state = $this->channel2->getConnectivityState(); 330 $this->assertEquals(GRPC\CHANNEL_IDLE, $state); 331 332 $this->channel1->close(); 333 $this->channel2->close(); 334 } 335 336 public function testPersistentChannelSameChannelCredentialsRootCerts() 337 { 338 $creds1 = Grpc\ChannelCredentials::createSsl( 339 file_get_contents(dirname(__FILE__).'/../data/ca.pem')); 340 $creds2 = Grpc\ChannelCredentials::createSsl( 341 file_get_contents(dirname(__FILE__).'/../data/ca.pem')); 342 343 $this->channel1 = new Grpc\Channel('localhost:50021', 344 ["credentials" => $creds1, 345 "grpc_target_persist_bound" => 3, 346 ]); 347 $this->channel2 = new Grpc\Channel('localhost:50021', 348 ["credentials" => $creds2]); 349 350 // try to connect on channel1 351 $state = $this->channel1->getConnectivityState(true); 352 $this->waitUntilNotIdle($this->channel1); 353 354 $state = $this->channel1->getConnectivityState(); 355 $this->assertConnecting($state); 356 $state = $this->channel2->getConnectivityState(); 357 $this->assertConnecting($state); 358 359 $this->channel1->close(); 360 $this->channel2->close(); 361 } 362 363 public function testPersistentChannelDifferentSecureChannelCredentials() 364 { 365 $creds1 = Grpc\ChannelCredentials::createSsl(); 366 $creds2 = Grpc\ChannelCredentials::createInsecure(); 367 368 $this->channel1 = new Grpc\Channel('localhost:50022', 369 ["credentials" => $creds1, 370 "grpc_target_persist_bound" => 3, 371 ]); 372 $this->channel2 = new Grpc\Channel('localhost:50022', 373 ["credentials" => $creds2]); 374 375 // try to connect on channel1 376 $state = $this->channel1->getConnectivityState(true); 377 $this->waitUntilNotIdle($this->channel1); 378 379 $state = $this->channel1->getConnectivityState(); 380 $this->assertConnecting($state); 381 $state = $this->channel2->getConnectivityState(); 382 $this->assertEquals(GRPC\CHANNEL_IDLE, $state); 383 384 $this->channel1->close(); 385 $this->channel2->close(); 386 } 387 388 public function testPersistentChannelSharedChannelClose1() 389 { 390 // same underlying channel 391 $this->channel1 = new Grpc\Channel('localhost:50123', [ 392 "grpc_target_persist_bound" => 3, 393 ]); 394 $this->channel2 = new Grpc\Channel('localhost:50123', []); 395 396 // close channel1 397 $this->channel1->close(); 398 399 // channel2 can still be use. We need to exclude the possible that 400 // in testPersistentChannelSharedChannelClose2, the exception is thrown 401 // by channel1. 402 $state = $this->channel2->getConnectivityState(); 403 $this->assertEquals(GRPC\CHANNEL_IDLE, $state); 404 } 405 406 /** 407 * @expectedException RuntimeException 408 */ 409 public function testPersistentChannelSharedChannelClose2() 410 { 411 // same underlying channel 412 $this->channel1 = new Grpc\Channel('localhost:50223', [ 413 "grpc_target_persist_bound" => 3, 414 ]); 415 $this->channel2 = new Grpc\Channel('localhost:50223', []); 416 417 // close channel1 418 $this->channel1->close(); 419 420 // channel2 can still be use 421 $state = $this->channel2->getConnectivityState(); 422 $this->assertEquals(GRPC\CHANNEL_IDLE, $state); 423 424 // channel 1 is closed 425 $state = $this->channel1->getConnectivityState(); 426 } 427 428 public function testPersistentChannelCreateAfterClose() 429 { 430 $this->channel1 = new Grpc\Channel('localhost:50024', [ 431 "grpc_target_persist_bound" => 3, 432 ]); 433 434 $this->channel1->close(); 435 436 $this->channel2 = new Grpc\Channel('localhost:50024', []); 437 $state = $this->channel2->getConnectivityState(); 438 $this->assertEquals(GRPC\CHANNEL_IDLE, $state); 439 440 $this->channel2->close(); 441 } 442 443 public function testPersistentChannelSharedMoreThanTwo() 444 { 445 $this->channel1 = new Grpc\Channel('localhost:50025', [ 446 "grpc_target_persist_bound" => 3, 447 ]); 448 $this->channel2 = new Grpc\Channel('localhost:50025', []); 449 $this->channel3 = new Grpc\Channel('localhost:50025', []); 450 451 // try to connect on channel1 452 $state = $this->channel1->getConnectivityState(true); 453 $this->waitUntilNotIdle($this->channel1); 454 455 // all 3 channels should be in CONNECTING state 456 $state = $this->channel1->getConnectivityState(); 457 $this->assertConnecting($state); 458 $state = $this->channel2->getConnectivityState(); 459 $this->assertConnecting($state); 460 $state = $this->channel3->getConnectivityState(); 461 $this->assertConnecting($state); 462 463 $this->channel1->close(); 464 } 465 466 public function callbackFunc($context) 467 { 468 return []; 469 } 470 471 public function callbackFunc2($context) 472 { 473 return ["k1" => "v1"]; 474 } 475 476 public function testPersistentChannelWithCallCredentials() 477 { 478 $creds = Grpc\ChannelCredentials::createSsl(); 479 $callCreds = Grpc\CallCredentials::createFromPlugin( 480 [$this, 'callbackFunc']); 481 $credsWithCallCreds = Grpc\ChannelCredentials::createComposite( 482 $creds, $callCreds); 483 484 // If a ChannelCredentials object is composed with a 485 // CallCredentials object, the underlying grpc channel will 486 // always be created new and NOT persisted. 487 $this->channel1 = new Grpc\Channel('localhost:50026', 488 ["credentials" => 489 $credsWithCallCreds, 490 "grpc_target_persist_bound" => 3, 491 ]); 492 $this->channel2 = new Grpc\Channel('localhost:50026', 493 ["credentials" => 494 $credsWithCallCreds]); 495 496 // try to connect on channel1 497 $state = $this->channel1->getConnectivityState(true); 498 $this->waitUntilNotIdle($this->channel1); 499 500 $state = $this->channel1->getConnectivityState(); 501 $this->assertConnecting($state); 502 $state = $this->channel2->getConnectivityState(); 503 $this->assertEquals(GRPC\CHANNEL_IDLE, $state); 504 505 $this->channel1->close(); 506 $this->channel2->close(); 507 } 508 509 public function testPersistentChannelWithDifferentCallCredentials() 510 { 511 $callCreds1 = Grpc\CallCredentials::createFromPlugin( 512 [$this, 'callbackFunc']); 513 $callCreds2 = Grpc\CallCredentials::createFromPlugin( 514 [$this, 'callbackFunc2']); 515 516 $creds1 = Grpc\ChannelCredentials::createSsl(); 517 $creds2 = Grpc\ChannelCredentials::createComposite( 518 $creds1, $callCreds1); 519 $creds3 = Grpc\ChannelCredentials::createComposite( 520 $creds1, $callCreds2); 521 522 // Similar to the test above, anytime a ChannelCredentials 523 // object is composed with a CallCredentials object, the 524 // underlying grpc channel will always be separate and not 525 // persisted 526 $this->channel1 = new Grpc\Channel('localhost:50027', 527 ["credentials" => $creds1, 528 "grpc_target_persist_bound" => 3, 529 ]); 530 $this->channel2 = new Grpc\Channel('localhost:50027', 531 ["credentials" => $creds2]); 532 $this->channel3 = new Grpc\Channel('localhost:50027', 533 ["credentials" => $creds3]); 534 535 // try to connect on channel1 536 $state = $this->channel1->getConnectivityState(true); 537 $this->waitUntilNotIdle($this->channel1); 538 539 $state = $this->channel1->getConnectivityState(); 540 $this->assertConnecting($state); 541 $state = $this->channel2->getConnectivityState(); 542 $this->assertEquals(GRPC\CHANNEL_IDLE, $state); 543 $state = $this->channel3->getConnectivityState(); 544 $this->assertEquals(GRPC\CHANNEL_IDLE, $state); 545 546 $this->channel1->close(); 547 $this->channel2->close(); 548 $this->channel3->close(); 549 } 550 551 public function testPersistentChannelForceNew() 552 { 553 $this->channel1 = new Grpc\Channel('localhost:50028', [ 554 "grpc_target_persist_bound" => 2, 555 ]); 556 // even though all the channel params are the same, channel2 557 // has a new and different underlying channel 558 $this->channel2 = new Grpc\Channel('localhost:50028', 559 ["force_new" => true]); 560 561 // try to connect on channel1 562 $state = $this->channel1->getConnectivityState(true); 563 $this->waitUntilNotIdle($this->channel1); 564 565 $state = $this->channel1->getConnectivityState(); 566 $this->assertConnecting($state); 567 $state = $this->channel2->getConnectivityState(); 568 $this->assertEquals(GRPC\CHANNEL_IDLE, $state); 569 570 $this->channel1->close(); 571 $this->channel2->close(); 572 } 573 574 public function testPersistentChannelForceNewOldChannelIdle1() 575 { 576 577 $this->channel1 = new Grpc\Channel('localhost:50029', [ 578 "grpc_target_persist_bound" => 2, 579 ]); 580 $this->channel2 = new Grpc\Channel('localhost:50029', 581 ["force_new" => true]); 582 // channel3 shares with channel1 583 $this->channel3 = new Grpc\Channel('localhost:50029', []); 584 585 // try to connect on channel2 586 $state = $this->channel2->getConnectivityState(true); 587 $this->waitUntilNotIdle($this->channel2); 588 $state = $this->channel1->getConnectivityState(); 589 $this->assertEquals(GRPC\CHANNEL_IDLE, $state); 590 $state = $this->channel2->getConnectivityState(); 591 $this->assertConnecting($state); 592 $state = $this->channel3->getConnectivityState(); 593 $this->assertEquals(GRPC\CHANNEL_IDLE, $state); 594 595 $this->channel1->close(); 596 $this->channel2->close(); 597 } 598 599 public function testPersistentChannelForceNewOldChannelIdle2() 600 { 601 602 $this->channel1 = new Grpc\Channel('localhost:50029', [ 603 "grpc_target_persist_bound" => 2, 604 ]); 605 $this->channel2 = new Grpc\Channel('localhost:50029', []); 606 607 // try to connect on channel2 608 $state = $this->channel1->getConnectivityState(true); 609 $this->waitUntilNotIdle($this->channel2); 610 $state = $this->channel1->getConnectivityState(); 611 $this->assertConnecting($state); 612 $state = $this->channel2->getConnectivityState(); 613 $this->assertConnecting($state); 614 615 $this->channel1->close(); 616 $this->channel2->close(); 617 } 618 619 public function testPersistentChannelForceNewOldChannelClose1() 620 { 621 622 $this->channel1 = new Grpc\Channel('localhost:50130', [ 623 "grpc_target_persist_bound" => 2, 624 ]); 625 $this->channel2 = new Grpc\Channel('localhost:50130', 626 ["force_new" => true]); 627 // channel3 shares with channel1 628 $this->channel3 = new Grpc\Channel('localhost:50130', []); 629 630 $this->channel1->close(); 631 632 $state = $this->channel2->getConnectivityState(); 633 $this->assertEquals(GRPC\CHANNEL_IDLE, $state); 634 635 // channel3 is still usable. We need to exclude the possibility that in 636 // testPersistentChannelForceNewOldChannelClose2, the exception is thrown 637 // by channel1 and channel2. 638 $state = $this->channel3->getConnectivityState(); 639 $this->assertEquals(GRPC\CHANNEL_IDLE, $state); 640 } 641 642 /** 643 * @expectedException RuntimeException 644 */ 645 public function testPersistentChannelForceNewOldChannelClose2() 646 { 647 648 $this->channel1 = new Grpc\Channel('localhost:50230', [ 649 "grpc_target_persist_bound" => 2, 650 ]); 651 $this->channel2 = new Grpc\Channel('localhost:50230', 652 ["force_new" => true]); 653 // channel3 shares with channel1 654 $this->channel3 = new Grpc\Channel('localhost:50230', []); 655 656 $this->channel1->close(); 657 658 $state = $this->channel2->getConnectivityState(); 659 $this->assertEquals(GRPC\CHANNEL_IDLE, $state); 660 661 // channel3 is still usable 662 $state = $this->channel3->getConnectivityState(); 663 $this->assertEquals(GRPC\CHANNEL_IDLE, $state); 664 665 // channel 1 is closed 666 $this->channel1->getConnectivityState(); 667 } 668 669 public function testPersistentChannelForceNewNewChannelClose() 670 { 671 672 $this->channel1 = new Grpc\Channel('localhost:50031', [ 673 "grpc_target_persist_bound" => 2, 674 ]); 675 $this->channel2 = new Grpc\Channel('localhost:50031', 676 ["force_new" => true]); 677 $this->channel3 = new Grpc\Channel('localhost:50031', []); 678 679 $this->channel2->close(); 680 681 $state = $this->channel1->getConnectivityState(); 682 $this->assertEquals(GRPC\CHANNEL_IDLE, $state); 683 684 // can still connect on channel1 685 $state = $this->channel1->getConnectivityState(true); 686 $this->waitUntilNotIdle($this->channel1); 687 688 $state = $this->channel1->getConnectivityState(); 689 $this->assertConnecting($state); 690 691 $this->channel1->close(); 692 } 693} 694