Passed
Pull Request — master (#36)
by
unknown
15:58 queued 07:42
created

testTimeoutWaitWontWaitPastGracefulMaxExecutionTimeout()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 35
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 23
nc 1
nop 0
dl 0
loc 35
rs 9.552
c 1
b 0
f 0
1
<?php
2
3
namespace OldSound\RabbitMqBundle\Tests\RabbitMq;
4
5
use OldSound\RabbitMqBundle\Event\AfterProcessingMessageEvent;
6
use OldSound\RabbitMqBundle\Event\BeforeProcessingMessageEvent;
7
use OldSound\RabbitMqBundle\Event\OnConsumeEvent;
8
use OldSound\RabbitMqBundle\Event\OnIdleEvent;
9
use OldSound\RabbitMqBundle\RabbitMq\Consumer;
10
use PhpAmqpLib\Channel\AMQPChannel;
11
use PhpAmqpLib\Connection\AMQPConnection;
12
use PhpAmqpLib\Exception\AMQPTimeoutException;
13
use PhpAmqpLib\Message\AMQPMessage;
14
use OldSound\RabbitMqBundle\RabbitMq\ConsumerInterface;
15
use PHPUnit\Framework\Assert;
16
use PHPUnit\Framework\TestCase;
17
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
18
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as ContractsEventDispatcherInterface;
19
use Symfony\Bridge\PhpUnit\ClockMock;
0 ignored issues
show
Bug introduced by
The type Symfony\Bridge\PhpUnit\ClockMock was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
20
21
class ConsumerTest extends TestCase
22
{
23
    protected function getConsumer($amqpConnection, $amqpChannel)
24
    {
25
        return new Consumer($amqpConnection, $amqpChannel);
26
    }
27
28
    protected function prepareAMQPConnection()
29
    {
30
        return $this->getMockBuilder(AMQPConnection::class)
31
            ->disableOriginalConstructor()
32
            ->getMock();
33
    }
34
35
    protected function prepareAMQPChannel()
36
    {
37
        return $this->getMockBuilder(AMQPChannel::class)
38
            ->disableOriginalConstructor()
39
            ->getMock();
40
    }
41
42
    /**
43
     * Check if the message is requeued or not correctly.
44
     *
45
     * @dataProvider processMessageProvider
46
     */
47
    public function testProcessMessage($processFlag, $expectedMethod = null, $expectedRequeue = null)
48
    {
49
        $amqpConnection = $this->prepareAMQPConnection();
50
        $amqpChannel = $this->prepareAMQPChannel();
51
        $consumer = $this->getConsumer($amqpConnection, $amqpChannel);
52
53
        $callbackFunction = function () use ($processFlag) {
54
            return $processFlag;
55
        }; // Create a callback function with a return value set by the data provider.
56
        $consumer->setCallback($callbackFunction);
57
58
        // Create a default message
59
        $amqpMessage = new AMQPMessage('foo body');
60
        $amqpMessage->delivery_info['channel'] = $amqpChannel;
0 ignored issues
show
Deprecated Code introduced by
The property PhpAmqpLib\Message\AMQPMessage::$delivery_info has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

60
        /** @scrutinizer ignore-deprecated */ $amqpMessage->delivery_info['channel'] = $amqpChannel;
Loading history...
61
        $amqpMessage->delivery_info['delivery_tag'] = 0;
0 ignored issues
show
Deprecated Code introduced by
The property PhpAmqpLib\Message\AMQPMessage::$delivery_info has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

61
        /** @scrutinizer ignore-deprecated */ $amqpMessage->delivery_info['delivery_tag'] = 0;
Loading history...
62
63
        if ($expectedMethod) {
64
            $amqpChannel->expects($this->any())
65
                ->method('basic_reject')
66
                ->will($this->returnCallback(function ($delivery_tag, $requeue) use ($expectedMethod, $expectedRequeue) {
67
                    Assert::assertSame($expectedMethod, 'basic_reject'); // Check if this function should be called.
68
                    Assert::assertSame($requeue, $expectedRequeue); // Check if the message should be requeued.
69
                }));
70
71
            $amqpChannel->expects($this->any())
72
                ->method('basic_ack')
73
                ->will($this->returnCallback(function ($delivery_tag) use ($expectedMethod) {
0 ignored issues
show
Unused Code introduced by
The parameter $delivery_tag is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

73
                ->will($this->returnCallback(function (/** @scrutinizer ignore-unused */ $delivery_tag) use ($expectedMethod) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
74
                    Assert::assertSame($expectedMethod, 'basic_ack'); // Check if this function should be called.
75
                }));
76
        } else {
77
            $amqpChannel->expects($this->never())->method('basic_reject');
78
            $amqpChannel->expects($this->never())->method('basic_ack');
79
            $amqpChannel->expects($this->never())->method('basic_nack');
80
        }
81
82
        $eventDispatcher = $this->getMockBuilder(EventDispatcherInterface::class)
83
            ->disableOriginalConstructor()
84
            ->getMock();
85
86
        $consumer->setEventDispatcher($eventDispatcher);
87
88
        $eventDispatcher->expects($this->atLeastOnce())
89
            ->method('dispatch')
90
            ->withConsecutive(
91
                array(new BeforeProcessingMessageEvent($consumer, $amqpMessage), BeforeProcessingMessageEvent::NAME),
92
                array(new AfterProcessingMessageEvent($consumer, $amqpMessage), AfterProcessingMessageEvent::NAME)
93
            )
94
            ->willReturnOnConsecutiveCalls(
95
                new BeforeProcessingMessageEvent($consumer, $amqpMessage),
96
                new AfterProcessingMessageEvent($consumer, $amqpMessage)
97
            );
98
99
        $consumer->processMessage($amqpMessage);
100
    }
101
102
    public function processMessageProvider()
103
    {
104
        return array(
105
            array(null, 'basic_ack'), // Remove message from queue only if callback return not false
106
            array(true, 'basic_ack'), // Remove message from queue only if callback return not false
107
            array(false, 'basic_reject', true), // Reject and requeue message to RabbitMQ
108
            array(ConsumerInterface::MSG_ACK, 'basic_ack'), // Remove message from queue only if callback return not false
109
            array(ConsumerInterface::MSG_REJECT_REQUEUE, 'basic_reject', true), // Reject and requeue message to RabbitMQ
110
            array(ConsumerInterface::MSG_REJECT, 'basic_reject', false), // Reject and drop
111
            array(ConsumerInterface::MSG_ACK_SENT), // ack not sent by the consumer but should be sent by the implementer of ConsumerInterface
112
        );
113
    }
114
115
    /**
116
     * @return array
117
     */
118
    public function consumeProvider()
119
    {
120
        $testCases["All ok 4 callbacks"] = array(
0 ignored issues
show
Comprehensibility Best Practice introduced by
$testCases was never initialized. Although not strictly required by PHP, it is generally a good practice to add $testCases = array(); before regardless.
Loading history...
121
            array(
122
                "messages" => array(
123
                    "msgCallback1",
124
                    "msgCallback2",
125
                    "msgCallback3",
126
                    "msgCallback4",
127
                )
128
            )
129
        );
130
131
        $testCases["No callbacks"] = array(
132
            array(
133
                "messages" => array()
134
            )
135
        );
136
137
        return $testCases;
138
    }
139
140
    /**
141
     * @dataProvider consumeProvider
142
     *
143
     * @param array $data
144
     */
145
    public function testConsume(array $data)
146
    {
147
        $consumerCallBacks = $data['messages'];
148
149
        // set up amqp connection
150
        $amqpConnection = $this->prepareAMQPConnection();
151
        // set up amqp channel
152
        $amqpChannel = $this->prepareAMQPChannel();
153
        $amqpChannel->expects($this->atLeastOnce())
154
            ->method('getChannelId')
155
            ->with()
156
            ->willReturn(true);
157
        $amqpChannel->expects($this->once())
158
            ->method('basic_consume')
159
            ->withAnyParameters()
160
            ->willReturn(true);
161
162
        // set up consumer
163
        $consumer = $this->getConsumer($amqpConnection, $amqpChannel);
164
        // disable autosetup fabric so we do not mock more objects
165
        $consumer->disableAutoSetupFabric();
166
        $consumer->setChannel($amqpChannel);
167
        $amqpChannel->callbacks = $consumerCallBacks;
0 ignored issues
show
Bug introduced by
Accessing callbacks on the interface PHPUnit\Framework\MockObject\MockObject suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
168
169
        /**
170
         * Mock wait method and use a callback to remove one element each time from callbacks
171
         * This will simulate a basic consumer consume with provided messages count
172
         */
173
        $amqpChannel->expects($this->exactly(count($consumerCallBacks)))
174
            ->method('wait')
175
            ->with(null, false, $consumer->getIdleTimeout())
176
            ->will(
177
                $this->returnCallback(
178
                    function () use ($amqpChannel) {
179
                        /** remove an element on each loop like ... simulate an ACK */
180
                        array_splice($amqpChannel->callbacks, 0, 1);
0 ignored issues
show
Bug introduced by
Accessing callbacks on the interface PHPUnit\Framework\MockObject\MockObject suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
181
                    })
182
            );
183
184
        $eventDispatcher = $this->getMockBuilder(EventDispatcherInterface::class)
185
            ->disableOriginalConstructor()
186
            ->getMock();
187
188
        $eventDispatcher->expects($this->exactly(count($consumerCallBacks)))
189
            ->method('dispatch')
190
            ->with($this->isInstanceOf(OnConsumeEvent::class), OnConsumeEvent::NAME)
191
            ->willReturn($this->isInstanceOf(OnConsumeEvent::class));
192
193
        $consumer->setEventDispatcher($eventDispatcher);
194
        $consumer->consume(1);
195
    }
196
197
    public function testIdleTimeoutExitCode()
198
    {
199
        // set up amqp connection
200
        $amqpConnection = $this->prepareAMQPConnection();
201
        // set up amqp channel
202
        $amqpChannel = $this->prepareAMQPChannel();
203
        $amqpChannel->expects($this->atLeastOnce())
204
            ->method('getChannelId')
205
            ->with()
206
            ->willReturn(true);
207
        $amqpChannel->expects($this->once())
208
            ->method('basic_consume')
209
            ->withAnyParameters()
210
            ->willReturn(true);
211
212
        // set up consumer
213
        $consumer = $this->getConsumer($amqpConnection, $amqpChannel);
214
        // disable autosetup fabric so we do not mock more objects
215
        $consumer->disableAutoSetupFabric();
216
        $consumer->setChannel($amqpChannel);
217
        $consumer->setIdleTimeout(60);
218
        $consumer->setIdleTimeoutExitCode(2);
219
        $amqpChannel->callbacks = array('idle_timeout_exit_code');
0 ignored issues
show
Bug introduced by
Accessing callbacks on the interface PHPUnit\Framework\MockObject\MockObject suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
220
221
        $amqpChannel->expects($this->exactly(1))
222
            ->method('wait')
223
            ->with(null, false, $consumer->getIdleTimeout())
224
            ->willReturnCallback(function ($allowedMethods, $nonBlocking, $waitTimeout) use ($consumer) {
225
                // simulate time passing by moving the last activity date time
226
                $consumer->setLastActivityDateTime(new \DateTime("-$waitTimeout seconds"));
227
                throw new AMQPTimeoutException();
228
            });
229
230
        $this->assertTrue(2 == $consumer->consume(1));
231
    }
232
233
    public function testShouldAllowContinueConsumptionAfterIdleTimeout()
234
    {
235
        // set up amqp connection
236
        $amqpConnection = $this->prepareAMQPConnection();
237
        // set up amqp channel
238
        $amqpChannel = $this->prepareAMQPChannel();
239
        $amqpChannel->expects($this->atLeastOnce())
240
            ->method('getChannelId')
241
            ->with()
242
            ->willReturn(true);
243
        $amqpChannel->expects($this->once())
244
            ->method('basic_consume')
245
            ->withAnyParameters()
246
            ->willReturn(true);
247
248
        // set up consumer
249
        $consumer = $this->getConsumer($amqpConnection, $amqpChannel);
250
        // disable autosetup fabric so we do not mock more objects
251
        $consumer->disableAutoSetupFabric();
252
        $consumer->setChannel($amqpChannel);
253
        $consumer->setIdleTimeout(2);
254
        $amqpChannel->callbacks = array('idle_timeout_exit_code');
0 ignored issues
show
Bug introduced by
Accessing callbacks on the interface PHPUnit\Framework\MockObject\MockObject suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
255
256
        $amqpChannel->expects($this->exactly(2))
257
            ->method('wait')
258
            ->with(null, false, $consumer->getIdleTimeout())
259
            ->willReturnCallback(function ($allowedMethods, $nonBlocking, $waitTimeout) use ($consumer) {
260
                // simulate time passing by moving the last activity date time
261
                $consumer->setLastActivityDateTime(new \DateTime("-$waitTimeout seconds"));
262
                throw new AMQPTimeoutException();
263
            });
264
265
        $eventDispatcher = $this->getMockBuilder(EventDispatcherInterface::class)
266
            ->disableOriginalConstructor()
267
            ->getMock();
268
269
        $eventDispatcher->expects($this->at(1))
270
            ->method('dispatch')
271
            ->with($this->isInstanceOf(OnIdleEvent::class), OnIdleEvent::NAME)
272
            ->willReturnCallback(function (OnIdleEvent $event, $eventName) {
0 ignored issues
show
Unused Code introduced by
The parameter $eventName is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

272
            ->willReturnCallback(function (OnIdleEvent $event, /** @scrutinizer ignore-unused */ $eventName) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
273
                $event->setForceStop(false);
274
275
                return $event;
276
            });
277
278
        $eventDispatcher->expects($this->at(3))
279
            ->method('dispatch')
280
            ->with($this->isInstanceOf(OnIdleEvent::class), OnIdleEvent::NAME)
281
            ->willReturnCallback(function (OnIdleEvent $event, $eventName) {
0 ignored issues
show
Unused Code introduced by
The parameter $eventName is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

281
            ->willReturnCallback(function (OnIdleEvent $event, /** @scrutinizer ignore-unused */ $eventName) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
282
                $event->setForceStop(true);
283
284
                return $event;
285
            });
286
287
        $consumer->setEventDispatcher($eventDispatcher);
288
289
        $this->expectException(AMQPTimeoutException::class);
290
        $consumer->consume(10);
291
    }
292
293
    public function testGracefulMaxExecutionTimeoutExitCode()
294
    {
295
        // set up amqp connection
296
        $amqpConnection = $this->prepareAMQPConnection();
297
        // set up amqp channel
298
        $amqpChannel = $this->prepareAMQPChannel();
299
        $amqpChannel->expects($this->atLeastOnce())
300
            ->method('getChannelId')
301
            ->with()
302
            ->willReturn(true);
303
        $amqpChannel->expects($this->once())
304
            ->method('basic_consume')
305
            ->withAnyParameters()
306
            ->willReturn(true);
307
308
        // set up consumer
309
        $consumer = $this->getConsumer($amqpConnection, $amqpChannel);
310
        // disable autosetup fabric so we do not mock more objects
311
        $consumer->disableAutoSetupFabric();
312
        $consumer->setChannel($amqpChannel);
313
314
        $consumer->setGracefulMaxExecutionDateTimeFromSecondsInTheFuture(60);
315
        $consumer->setGracefulMaxExecutionTimeoutExitCode(10);
316
        $amqpChannel->callbacks = array('graceful_max_execution_timeout_test');
0 ignored issues
show
Bug introduced by
Accessing callbacks on the interface PHPUnit\Framework\MockObject\MockObject suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
317
318
        $amqpChannel->expects($this->exactly(1))
319
            ->method('wait')
320
            ->willReturnCallback(function ($allowedMethods, $nonBlocking, $waitTimeout) use ($consumer) {
321
                // simulate time passing by moving the max execution date time
322
                $consumer->setGracefulMaxExecutionDateTimeFromSecondsInTheFuture($waitTimeout * -1);
323
                throw new AMQPTimeoutException();
324
            });
325
326
        $this->assertSame(10, $consumer->consume(1));
327
    }
328
329
    public function testGracefulMaxExecutionWontWaitIfPastTheTimeout()
330
    {
331
        // set up amqp connection
332
        $amqpConnection = $this->prepareAMQPConnection();
333
        // set up amqp channel
334
        $amqpChannel = $this->prepareAMQPChannel();
335
        $amqpChannel->expects($this->atLeastOnce())
336
            ->method('getChannelId')
337
            ->with()
338
            ->willReturn(true);
339
        $amqpChannel->expects($this->once())
340
            ->method('basic_consume')
341
            ->withAnyParameters()
342
            ->willReturn(true);
343
344
        // set up consumer
345
        $consumer = $this->getConsumer($amqpConnection, $amqpChannel);
346
        // disable autosetup fabric so we do not mock more objects
347
        $consumer->disableAutoSetupFabric();
348
        $consumer->setChannel($amqpChannel);
349
350
        $consumer->setGracefulMaxExecutionDateTimeFromSecondsInTheFuture(0);
351
        $amqpChannel->callbacks = array('graceful_max_execution_timeout_test');
0 ignored issues
show
Bug introduced by
Accessing callbacks on the interface PHPUnit\Framework\MockObject\MockObject suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
352
353
        $amqpChannel->expects($this->never())
354
            ->method('wait');
355
356
        $consumer->consume(1);
357
    }
358
359
    public function testTimeoutWait()
360
    {
361
        // set up amqp connection
362
        $amqpConnection = $this->prepareAMQPConnection();
363
        // set up amqp channel
364
        $amqpChannel = $this->prepareAMQPChannel();
365
        $amqpChannel->expects($this->atLeastOnce())
366
            ->method('getChannelId')
367
            ->with()
368
            ->willReturn(true);
369
        $amqpChannel->expects($this->once())
370
            ->method('basic_consume')
371
            ->withAnyParameters()
372
            ->willReturn(true);
373
374
        // set up consumer
375
        $consumer = $this->getConsumer($amqpConnection, $amqpChannel);
376
        // disable autosetup fabric so we do not mock more objects
377
        $consumer->disableAutoSetupFabric();
378
        $consumer->setChannel($amqpChannel);
379
        $consumer->setTimeoutWait(30);
380
        $consumer->setGracefulMaxExecutionDateTimeFromSecondsInTheFuture(60);
381
        $consumer->setIdleTimeout(50);
382
383
        $amqpChannel->callbacks = array('timeout_wait_test');
0 ignored issues
show
Bug introduced by
Accessing callbacks on the interface PHPUnit\Framework\MockObject\MockObject suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
384
385
        $amqpChannel->expects($this->exactly(2))
386
            ->method('wait')
387
            ->with(null, false, 30)
388
            ->willReturnCallback(function ($allowedMethods, $nonBlocking, $waitTimeout) use ($consumer) {
389
                // ensure max execution date time "counts down"
390
                $consumer->setGracefulMaxExecutionDateTime(
391
                    $consumer->getGracefulMaxExecutionDateTime()->modify("-$waitTimeout seconds")
392
                );
393
                // ensure last activity just occurred so idle timeout is not reached
394
                $consumer->setLastActivityDateTime(new \DateTime());
395
                throw new AMQPTimeoutException();
396
            });
397
398
        $consumer->consume(1);
399
    }
400
401
    public function testTimeoutWaitWontWaitPastGracefulMaxExecutionTimeout()
402
    {
403
        // set up amqp connection
404
        $amqpConnection = $this->prepareAMQPConnection();
405
        // set up amqp channel
406
        $amqpChannel = $this->prepareAMQPChannel();
407
        $amqpChannel->expects($this->atLeastOnce())
408
            ->method('getChannelId')
409
            ->with()
410
            ->willReturn(true);
411
        $amqpChannel->expects($this->once())
412
            ->method('basic_consume')
413
            ->withAnyParameters()
414
            ->willReturn(true);
415
416
        // set up consumer
417
        $consumer = $this->getConsumer($amqpConnection, $amqpChannel);
418
        // disable autosetup fabric so we do not mock more objects
419
        $consumer->disableAutoSetupFabric();
420
        $consumer->setChannel($amqpChannel);
421
        $consumer->setTimeoutWait(20);
422
423
        $consumer->setGracefulMaxExecutionDateTimeFromSecondsInTheFuture(10);
424
        $amqpChannel->callbacks = array('graceful_max_execution_timeout_test');
0 ignored issues
show
Bug introduced by
Accessing callbacks on the interface PHPUnit\Framework\MockObject\MockObject suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
425
426
        $amqpChannel->expects($this->once())
427
            ->method('wait')
428
            ->with(null, false, 10)
429
            ->willReturnCallback(function ($allowedMethods, $nonBlocking, $waitTimeout) use ($consumer) {
430
                // simulate time passing by moving the max execution date time
431
                $consumer->setGracefulMaxExecutionDateTimeFromSecondsInTheFuture($waitTimeout * -1);
432
                throw new AMQPTimeoutException();
433
            });
434
435
        $consumer->consume(1);
436
    }
437
438
    public function testTimeoutWaitWontWaitPastIdleTimeout()
439
    {
440
        // set up amqp connection
441
        $amqpConnection = $this->prepareAMQPConnection();
442
        // set up amqp channel
443
        $amqpChannel = $this->prepareAMQPChannel();
444
        $amqpChannel->expects($this->atLeastOnce())
445
            ->method('getChannelId')
446
            ->with()
447
            ->willReturn(true);
448
        $amqpChannel->expects($this->once())
449
            ->method('basic_consume')
450
            ->withAnyParameters()
451
            ->willReturn(true);
452
453
        // set up consumer
454
        $consumer = $this->getConsumer($amqpConnection, $amqpChannel);
455
        // disable autosetup fabric so we do not mock more objects
456
        $consumer->disableAutoSetupFabric();
457
        $consumer->setChannel($amqpChannel);
458
        $consumer->setTimeoutWait(20);
459
        $consumer->setIdleTimeout(10);
460
        $consumer->setIdleTimeoutExitCode(2);
461
462
        $amqpChannel->callbacks = array('idle_timeout_test');
0 ignored issues
show
Bug introduced by
Accessing callbacks on the interface PHPUnit\Framework\MockObject\MockObject suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
463
464
        $amqpChannel->expects($this->once())
465
            ->method('wait')
466
            ->with(null, false, 10)
467
            ->willReturnCallback(function ($allowedMethods, $nonBlocking, $waitTimeout) use ($consumer) {
468
                // simulate time passing by moving the last activity date time
469
                $consumer->setLastActivityDateTime(new \DateTime("-$waitTimeout seconds"));
470
                throw new AMQPTimeoutException();
471
            });
472
473
        $this->assertEquals(2, $consumer->consume(1));
474
    }
475
}
476