blob: 5df60921cea656678c3968aee8d4c3d7fd95721d [file] [log] [blame]
Aaron Kruglikov3e29f662016-07-13 10:18:10 -07001/*
2 * Copyright 2016 Open Networking Laboratory
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package org.onosproject.store.primitives.resources.impl;
17
18import com.google.common.base.Throwables;
19import com.google.common.collect.Lists;
20import io.atomix.resource.ResourceType;
21import org.junit.AfterClass;
22import org.junit.BeforeClass;
23import org.junit.Test;
24import org.onlab.util.Tools;
25import org.onosproject.store.service.MapEvent;
26import org.onosproject.store.service.MapEventListener;
27
28import java.util.Arrays;
29import java.util.Collection;
30import java.util.List;
31import java.util.Map;
32import java.util.concurrent.ArrayBlockingQueue;
33import java.util.concurrent.BlockingQueue;
34import java.util.stream.Collectors;
35
36import static org.junit.Assert.assertArrayEquals;
37import static org.junit.Assert.assertEquals;
38import static org.junit.Assert.assertFalse;
39import static org.junit.Assert.assertNotNull;
40import static org.junit.Assert.assertNull;
41import static org.junit.Assert.assertTrue;
42
43/**
44 * Unit tests for {@link AtomixConsistentTreeMap}.
45 */
46public class AtomixConsistentTreeMapTest extends AtomixTestBase {
47 private final String keyFour = "hello";
48 private final String keyThree = "goodbye";
49 private final String keyTwo = "foo";
50 private final String keyOne = "bar";
51 private final byte[] valueOne = Tools.getBytesUtf8(keyOne);
52 private final byte[] valueTwo = Tools.getBytesUtf8(keyTwo);
53 private final byte[] valueThree = Tools.getBytesUtf8(keyThree);
54 private final byte[] valueFour = Tools.getBytesUtf8(keyFour);
55 private final byte[] spareValue = Tools.getBytesUtf8("spareValue");
56 private final List<String> allKeys = Lists.newArrayList(keyOne, keyTwo,
57 keyThree, keyFour);
58 private final List<byte[]> allValues = Lists.newArrayList(valueOne,
59 valueTwo,
60 valueThree,
61 valueFour);
62 @BeforeClass
63 public static void preTestSetup() throws Throwable {
64 createCopycatServers(3);
65 }
66
67 @AfterClass
68 public static void postTestCleanup() throws Throwable {
69 clearTests();
70 }
71
72 @Override
73 protected ResourceType resourceType() {
74 return new ResourceType(AtomixConsistentTreeMap.class);
75 }
76
77 /**
78 * Tests of the functionality associated with the
79 * {@link org.onosproject.store.service.AsyncConsistentMap} interface
80 * except transactions and listeners.
81 */
82 @Test
83 public void testBasicMapOperations() throws Throwable {
84 //Throughout the test there are isEmpty queries, these are intended to
85 //make sure that the previous section has been cleaned up, they serve
86 //the secondary purpose of testing isEmpty but that is not their
87 //primary purpose.
88 AtomixConsistentTreeMap map = createResource("basicTestMap");
89 //test size
90 map.size().thenAccept(result -> assertEquals(0, (int) result)).join();
91 map.isEmpty().thenAccept(result -> assertTrue(result)).join();
92 //test contains key
93 allKeys.forEach(key -> map.containsKey(key).
94 thenAccept(result -> assertFalse(result)).join());
95 //test contains value
96 allValues.forEach(value -> map.containsValue(value)
97 .thenAccept(result -> assertFalse(result)).join());
98 //test get
99 allKeys.forEach(key -> map.get(key).
100 thenAccept(result -> assertNull(result)).join());
101
102 //populate and redo prior three tests
103 allKeys.forEach(key -> map.put(key, allValues
104 .get(allKeys.indexOf(key))).thenAccept(
105 result -> assertNull(result)).join());
106 //test contains key
107 allKeys.forEach(key -> map.containsKey(key).
108 thenAccept(result -> assertTrue(result)).join());
109 //test contains value
110 allValues.forEach(value -> map.containsValue(value)
111 .thenAccept(result -> assertTrue(result)).join());
112 //test get
113 allKeys.forEach(key -> map.get(key).
114 thenAccept(
115 result -> assertArrayEquals(
116 allValues.get(allKeys.indexOf(key)),
117 result.value())).join());
118 //test all compute methods in this section
119 allKeys.forEach(key -> map.computeIfAbsent(
120 key, v ->allValues.get(allKeys.indexOf(key)
121 )).thenAccept(result ->
122 assertArrayEquals(
123 allValues.get(allKeys.indexOf(key)),
124 result.value())).join());
125 map.size().thenAccept(result -> assertEquals(4, (int) result)).join();
126 map.isEmpty().thenAccept(result -> assertFalse(result)).join();
127 allKeys.forEach(key -> map.computeIfPresent(key, (k, v) -> null).
128 thenAccept(result -> assertNull(result)).join());
129 map.isEmpty().thenAccept(result -> assertTrue(result)).join();
130 allKeys.forEach(key -> map.compute(key, (k, v) ->
131 allValues.get(allKeys.indexOf(key))).
132 thenAccept(result -> assertArrayEquals(
133 allValues.get(allKeys.indexOf(key)),
134 result.value())).join());
135 map.size().thenAccept(result -> assertEquals(4, (int) result)).join();
136 map.isEmpty().thenAccept(result -> assertFalse(result)).join();
137 allKeys.forEach(key -> map.computeIf(key,
138 (k) -> allKeys.indexOf(key) < 2,
139 (k, v) -> null).thenAccept(result -> {
140 if (allKeys.indexOf(key) < 2) {
141 assertNull(result);
142 } else {
143 assertArrayEquals(allValues.get(allKeys.indexOf(key)),
144 result.value());
145 }
146 }).join());
147 map.size().thenAccept(result -> assertEquals(2, (int) result)).join();
148 map.isEmpty().thenAccept(result -> assertFalse(result)).join();
149 //test simple put
150 allKeys.forEach(
151 key -> map.put(key, allValues.get(allKeys.indexOf(key)))
152 .thenAccept(result -> {
153 if (allKeys.indexOf(key) < 2) {
154 assertNull(result);
155 } else {
156 assertArrayEquals(
157 allValues.get(allKeys.indexOf(key)),
158 result.value());
159 }
160 }).join());
161 map.size().thenAccept(result -> assertEquals(4, (int) result)).join();
162 map.isEmpty().thenAccept(result -> assertFalse(result)).join();
163 //test put and get for version retrieval
164 allKeys.forEach(
165 key -> map.putAndGet(key, allValues.get(allKeys.indexOf(key))).
166 thenAccept(firstResult -> {
167 map.putAndGet(key, allValues.get(allKeys.indexOf(key))).
168 thenAccept(secondResult -> {
169 assertArrayEquals(allValues.get(allKeys.indexOf(key)),
170 firstResult.value());
171 assertArrayEquals(allValues.get(allKeys.indexOf(key)),
172 secondResult.value());
173 assertTrue((firstResult.version() + 1) ==
174 secondResult.version());
175 });
176 }).join());
177 //test removal
178 allKeys.forEach(key -> map.remove(key).thenAccept(
179 result -> assertArrayEquals(
180 allValues.get(allKeys.indexOf(key)), result.value()))
181 .join());
182 map.isEmpty().thenAccept(result -> assertTrue(result));
183 //repopulating, this is not mainly for testing
184 allKeys.forEach(key -> map.put(
185 key, allValues.get(allKeys.indexOf(key)))
186 .thenAccept(result -> {
187 assertNull(result);
188 }).join());
189
190 //Test various collections of keys, values and entries
191 map.keySet().thenAccept(
192 keys -> assertTrue(
193 stringArrayCollectionIsEqual(keys, allKeys)))
194 .join();
195 map.values().thenAccept(
196 values -> assertTrue(
197 byteArrayCollectionIsEqual(values.stream().map(
198 v -> v.value()).collect(
199 Collectors.toSet()), allValues)))
200 .join();
201 map.entrySet().thenAccept(entrySet -> {
202 entrySet.forEach(entry -> {
203 assertTrue(allKeys.contains(entry.getKey()));
204 assertTrue(Arrays.equals(entry.getValue().value(),
205 allValues.get(allKeys.indexOf(entry.getKey()))));
206 });
207 }).join();
208 map.clear().join();
209 map.isEmpty().thenAccept(result -> assertTrue(result)).join();
210
211 //test conditional put
212 allKeys.forEach(
213 key -> map.putIfAbsent(
214 key, allValues.get(allKeys.indexOf(key))).
215 thenAccept(result -> assertNull(result)).join());
216 allKeys.forEach(
217 key -> map.putIfAbsent(
218 key, null).
219 thenAccept(result ->
220 assertArrayEquals(result.value(),
221 allValues.get(allKeys.indexOf(key))))
222 .join());
223 // test alternate removes that specify value or version
224 allKeys.forEach(
225 key -> map.remove(key, spareValue).thenAccept(
226 result -> assertFalse(result)).join());
227 allKeys.forEach(
228 key -> map.remove(key, allValues.get(allKeys.indexOf(key)))
229 .thenAccept(result -> assertTrue(result)).join());
230 map.isEmpty().thenAccept(result -> assertTrue(result)).join();
231 List<Long> versions = Lists.newArrayList();
232
233 //repopulating set for version based removal
234 allKeys.forEach(
235 key -> map.putAndGet(key, allValues.get(allKeys.indexOf(key)))
236 .thenAccept(result -> versions.add(result.version())).join());
237 allKeys.forEach(
238 key -> map.remove(key, versions.get(0)).thenAccept(
239 result -> {
240 assertTrue(result);
241 versions.remove(0);
242 }).join());
243 map.isEmpty().thenAccept(result -> assertTrue(result)).join();
244 //Testing all replace both simple (k, v), and complex that consider
245 // previous mapping or version.
246 allKeys.forEach(
247 key -> map.put(key, allValues.get(allKeys.indexOf(key)))
248 .thenAccept(result -> assertNull(result)).join());
249 allKeys.forEach(key -> map.replace(
250 key, allValues.get(3 - allKeys.indexOf(key)))
251 .thenAccept(result -> assertArrayEquals(
252 allValues.get(allKeys.indexOf(key)), result.value()))
253 .join());
254 allKeys.forEach(key -> map.replace(key,
255 spareValue,
256 allValues.get(allKeys.indexOf(key)))
257 .thenAccept(result -> assertFalse(result))
258 .join());
259 allKeys.forEach(key -> map.replace(
260 key, allValues.get(3 - allKeys.indexOf(key)),
261 allValues.get(allKeys.indexOf(key)))
262 .thenAccept(result -> assertTrue(result)).join());
263 map.clear().join();
264 map.isEmpty().thenAccept(result -> assertTrue(result)).join();
265 versions.clear();
266 //populate for version based replacement
267 allKeys.forEach(
268 key -> map.putAndGet(
269 key, allValues.get(3 - allKeys.indexOf(key)))
270 .thenAccept(result ->
271 versions.add(result.version())).join());
272 allKeys.forEach(key -> map.replace(
273 key, 0, allValues.get(allKeys.indexOf(key)))
274 .thenAccept(result -> assertFalse(result))
275 .join());
276 allKeys.forEach(key -> map.replace(
277 key, versions.get(0), allValues.get(allKeys.indexOf(key)))
278 .thenAccept(result -> {
279 assertTrue(result);
280 versions.remove(0);
281 }).join());
282 }
283
284 @Test
285 public void mapListenerTests() throws Throwable {
286 final byte[] value1 = Tools.getBytesUtf8("value1");
287 final byte[] value2 = Tools.getBytesUtf8("value2");
288 final byte[] value3 = Tools.getBytesUtf8("value3");
289
290 AtomixConsistentTreeMap map = createResource("treeMapListenerTestMap");
291 TestMapEventListener listener = new TestMapEventListener();
292
293 // add listener; insert new value into map and verify an INSERT event
294 // is received.
295 map.addListener(listener).thenCompose(v -> map.put("foo", value1))
296 .join();
297 MapEvent<String, byte[]> event = listener.event();
298 assertNotNull(event);
299 assertEquals(MapEvent.Type.INSERT, event.type());
300 assertTrue(Arrays.equals(value1, event.newValue().value()));
301
302 // remove listener and verify listener is not notified.
303 map.removeListener(listener).thenCompose(v -> map.put("foo", value2))
304 .join();
305 assertFalse(listener.eventReceived());
306
307 // add the listener back and verify UPDATE events are received
308 // correctly
309 map.addListener(listener).thenCompose(v -> map.put("foo", value3))
310 .join();
311 event = listener.event();
312 assertNotNull(event);
313 assertEquals(MapEvent.Type.UPDATE, event.type());
314 assertTrue(Arrays.equals(value3, event.newValue().value()));
315
316 // perform a non-state changing operation and verify no events are
317 // received.
318 map.putIfAbsent("foo", value1).join();
319 assertFalse(listener.eventReceived());
320
321 // verify REMOVE events are received correctly.
322 map.remove("foo").join();
323 event = listener.event();
324 assertNotNull(event);
325 assertEquals(MapEvent.Type.REMOVE, event.type());
326 assertTrue(Arrays.equals(value3, event.oldValue().value()));
327
328 // verify compute methods also generate events.
329 map.computeIf("foo", v -> v == null, (k, v) -> value1).join();
330 event = listener.event();
331 assertNotNull(event);
332 assertEquals(MapEvent.Type.INSERT, event.type());
333 assertTrue(Arrays.equals(value1, event.newValue().value()));
334
335 map.compute("foo", (k, v) -> value2).join();
336 event = listener.event();
337 assertNotNull(event);
338 assertEquals(MapEvent.Type.UPDATE, event.type());
339 assertTrue(Arrays.equals(value2, event.newValue().value()));
340
341 map.computeIf(
342 "foo", v -> Arrays.equals(v, value2), (k, v) -> null).join();
343 event = listener.event();
344 assertNotNull(event);
345 assertEquals(MapEvent.Type.REMOVE, event.type());
346 assertTrue(Arrays.equals(value2, event.oldValue().value()));
347
348 map.removeListener(listener).join();
349 }
350
351 /**
352 * Tests functionality specified in the {@link AtomixConsistentTreeMap}
353 * interface, beyond the functionality provided in
354 * {@link org.onosproject.store.service.AsyncConsistentMap}.
355 */
356 @Test
357 public void treeMapFunctionsTest() {
358 AtomixConsistentTreeMap map = createResource("treeMapFunctionTestMap");
359 //Tests on empty map
360 map.firstKey().thenAccept(result -> assertNull(result)).join();
361 map.lastKey().thenAccept(result -> assertNull(result)).join();
362 map.ceilingEntry(keyOne).thenAccept(result -> assertNull(result))
363 .join();
364 map.floorEntry(keyOne).thenAccept(result -> assertNull(result)).join();
365 map.higherEntry(keyOne).thenAccept(result -> assertNull(result))
366 .join();
367 map.lowerEntry(keyOne).thenAccept(result -> assertNull(result)).join();
368 map.firstEntry().thenAccept(result -> assertNull(result)).join();
369 map.lastEntry().thenAccept(result -> assertNull(result)).join();
370 map.pollFirstEntry().thenAccept(result -> assertNull(result)).join();
371 map.pollLastEntry().thenAccept(result -> assertNull(result)).join();
372 map.lowerKey(keyOne).thenAccept(result -> assertNull(result)).join();
373 map.floorKey(keyOne).thenAccept(result -> assertNull(result)).join();
374 map.ceilingKey(keyOne).thenAccept(result -> assertNull(result))
375 .join();
376 map.higherKey(keyOne).thenAccept(result -> assertNull(result)).join();
377 map.delete().join();
378
379 allKeys.forEach(key -> map.put(
380 key, allValues.get(allKeys.indexOf(key)))
381 .thenAccept(result -> assertNull(result)).join());
382 //Note ordering keys are in their proper ordering in ascending order
383 //both in naming and in the allKeys list.
384
385 map.firstKey().thenAccept(result -> assertEquals(keyOne, result))
386 .join();
387 map.lastKey().thenAccept(result -> assertEquals(keyFour, result))
388 .join();
389 map.ceilingEntry(keyOne)
390 .thenAccept(result -> {
391 assertEquals(keyOne, result.getKey());
392 assertArrayEquals(valueOne, result.getValue().value());
393 })
394 .join();
395 //adding an additional letter to make keyOne an unacceptable response
396 map.ceilingEntry(keyOne + "a")
397 .thenAccept(result -> {
398 assertEquals(keyTwo, result.getKey());
399 assertArrayEquals(valueTwo, result.getValue().value());
400 })
401 .join();
402 map.ceilingEntry(keyFour + "a")
403 .thenAccept(result -> {
404 assertNull(result);
405 })
406 .join();
407 map.floorEntry(keyTwo).thenAccept(result -> {
408 assertEquals(keyTwo, result.getKey());
409 assertArrayEquals(valueTwo, result.getValue().value());
410 })
411 .join();
412 //shorten the key so it itself is not an acceptable reply
413 map.floorEntry(keyTwo.substring(0, 2)).thenAccept(result -> {
414 assertEquals(keyOne, result.getKey());
415 assertArrayEquals(valueOne, result.getValue().value());
416 })
417 .join();
418 // shorten least key so no acceptable response exists
419 map.floorEntry(keyOne.substring(0, 1)).thenAccept(
420 result -> assertNull(result))
421 .join();
422
423 map.higherEntry(keyTwo).thenAccept(result -> {
424 assertEquals(keyThree, result.getKey());
425 assertArrayEquals(valueThree, result.getValue().value());
426 })
427 .join();
428 map.higherEntry(keyFour).thenAccept(result -> assertNull(result))
429 .join();
430
431 map.lowerEntry(keyFour).thenAccept(result -> {
432 assertEquals(keyThree, result.getKey());
433 assertArrayEquals(valueThree, result.getValue().value());
434 })
435 .join();
436 map.lowerEntry(keyOne).thenAccept(result -> assertNull(result))
437 .join();
438 map.firstEntry().thenAccept(result -> {
439 assertEquals(keyOne, result.getKey());
440 assertArrayEquals(valueOne, result.getValue().value());
441 })
442 .join();
443 map.lastEntry().thenAccept(result -> {
444 assertEquals(keyFour, result.getKey());
445 assertArrayEquals(valueFour, result.getValue().value());
446 })
447 .join();
448 map.pollFirstEntry().thenAccept(result -> {
449 assertEquals(keyOne, result.getKey());
450 assertArrayEquals(valueOne, result.getValue().value());
451 });
452 map.containsKey(keyOne).thenAccept(result -> assertFalse(result))
453 .join();
454 map.size().thenAccept(result -> assertEquals(3, (int) result)).join();
455 map.pollLastEntry().thenAccept(result -> {
456 assertEquals(keyFour, result.getKey());
457 assertArrayEquals(valueFour, result.getValue().value());
458 });
459 map.containsKey(keyFour).thenAccept(result -> assertFalse(result))
460 .join();
461 map.size().thenAccept(result -> assertEquals(2, (int) result)).join();
462
463 //repopulate the missing entries
464 allKeys.forEach(key -> map.put(
465 key, allValues.get(allKeys.indexOf(key)))
466 .thenAccept(result -> {
467 if (key.equals(keyOne) || key.equals(keyFour)) {
468 assertNull(result);
469 } else {
470 assertArrayEquals(allValues.get(allKeys.indexOf(key)),
471 result.value());
472 }
473 })
474 .join());
475 map.lowerKey(keyOne).thenAccept(result -> assertNull(result)).join();
476 map.lowerKey(keyThree).thenAccept(
477 result -> assertEquals(keyTwo, result))
478 .join();
479 map.floorKey(keyThree).thenAccept(
480 result -> assertEquals(keyThree, result))
481 .join();
482 //shortening the key so there is no acceptable response
483 map.floorKey(keyOne.substring(0, 1)).thenAccept(
484 result -> assertNull(result))
485 .join();
486 map.ceilingKey(keyTwo).thenAccept(
487 result -> assertEquals(keyTwo, result))
488 .join();
489 //adding to highest key so there is no acceptable response
490 map.ceilingKey(keyFour + "a")
491 .thenAccept(reslt -> assertNull(reslt))
492 .join();
493 map.higherKey(keyThree).thenAccept(
494 result -> assertEquals(keyFour, result))
495 .join();
496 map.higherKey(keyFour).thenAccept(
497 result -> assertNull(result))
498 .join();
499 map.delete().join();
500
501 }
502
503 private AtomixConsistentTreeMap createResource(String mapName) {
504 try {
505 AtomixConsistentTreeMap map = createAtomixClient().
506 getResource(mapName, AtomixConsistentTreeMap.class)
507 .join();
508 return map;
509 } catch (Throwable e) {
510 throw new RuntimeException(e.toString());
511 }
512 }
513 private static class TestMapEventListener
514 implements MapEventListener<String, byte[]> {
515
516 private final BlockingQueue<MapEvent<String, byte[]>> queue =
517 new ArrayBlockingQueue<>(1);
518
519 @Override
520 public void event(MapEvent<String, byte[]> event) {
521 try {
522 queue.put(event);
523 } catch (InterruptedException e) {
524 Throwables.propagate(e);
525 }
526 }
527
528 public boolean eventReceived() {
529 return !queue.isEmpty();
530 }
531
532 public MapEvent<String, byte[]> event() throws InterruptedException {
533 return queue.take();
534 }
535 }
536
537 /**
538 * Returns two arrays contain the same set of elements,
539 * regardless of order.
540 * @param o1 first collection
541 * @param o2 second collection
542 * @return true if they contain the same elements
543 */
544 private boolean byteArrayCollectionIsEqual(
545 Collection<? extends byte[]> o1, Collection<? extends byte[]> o2) {
546 if (o1 == null || o2 == null || o1.size() != o2.size()) {
547 return false;
548 }
549 for (byte[] array1 : o1) {
550 boolean matched = false;
551 for (byte[] array2 : o2) {
552 if (Arrays.equals(array1, array2)) {
553 matched = true;
554 break;
555 }
556 }
557 if (!matched) {
558 return false;
559 }
560 }
561 return true;
562 }
563
564 /**
565 * Compares two collections of strings returns true if they contain the
566 * same strings, false otherwise.
567 * @param s1 string collection one
568 * @param s2 string collection two
569 * @return true if the two sets contain the same strings
570 */
571 private boolean stringArrayCollectionIsEqual(
572 Collection<? extends String> s1, Collection<? extends String> s2) {
573 if (s1 == null || s2 == null || s1.size() != s2.size()) {
574 return false;
575 }
576 for (String string1 : s1) {
577 boolean matched = false;
578 for (String string2 : s2) {
579 if (string1.equals(string2)) {
580 matched = true;
581 break;
582 }
583 }
584 if (!matched) {
585 return false;
586 }
587 }
588 return true;
589 }
590
591 /**
592 * Inner entry type for testing.
593 * @param <K>
594 * @param <V>
595 */
596 private class InnerEntry<K, V> implements Map.Entry<K, V> {
597 private K key;
598 private V value;
599 public InnerEntry(K key, V value) {
600 this.key = key;
601 this.value = value;
602 }
603
604 @Override
605 public K getKey() {
606 return key;
607 }
608
609 @Override
610 public V getValue() {
611 return value;
612 }
613
614 @Override
615 public V setValue(V value) {
616 V temp = this.value;
617 this.value = value;
618 return temp;
619 }
620
621 @Override
622 public boolean equals(Object o) {
623 if (!(o instanceof InnerEntry)) {
624 return false;
625 }
626 InnerEntry other = (InnerEntry) o;
627 boolean keysEqual = false;
628 boolean valuesEqual = false;
629 if (this.key instanceof byte[]) {
630 if (other.getKey() instanceof byte[]) {
631 keysEqual = Arrays.equals((byte[]) this.key,
632 (byte[]) other.getKey());
633 } else {
634 return false;
635 }
636 } else {
637 keysEqual = this.getKey().equals(other.getKey());
638 }
639
640 if (keysEqual) {
641 if (this.value instanceof byte[]) {
642 if (other.getValue() instanceof byte[]) {
643 return Arrays.equals((byte[]) this.value,
644 (byte[]) other.getValue());
645 } else {
646 return false;
647 }
648 } else {
649 return this.key.equals(other.getKey());
650 }
651 }
652 return false;
653 }
654
655 @Override
656 public int hashCode() {
657 return 0;
658 }
659 }
660}