This is an automated email from the ASF dual-hosted git repository.

dcapwell pushed a commit to branch cep-15-accord
in repository https://gitbox.apache.org/repos/asf/cassandra.git


The following commit(s) were added to refs/heads/cep-15-accord by this push:
     new c9be786ded Create a fuzz test that randomizes topology changes, 
cluster actions, and CQL operations
c9be786ded is described below

commit c9be786ded8c06156b470fcdcc6d9549eb6e2976
Author: David Capwell <[email protected]>
AuthorDate: Fri Aug 30 09:06:39 2024 -0700

    Create a fuzz test that randomizes topology changes, cluster actions, and 
CQL operations
    
    patch by David Capwell; reviewed by Alex Petrov for CASSANDRA-19847
---
 modules/accord                                     |   2 +-
 .../cassandra/index/accord/RouteIndexTest.java     | 129 +++++++++------------
 .../cassandra/io/util/ChecksumedDataTest.java      |   5 +-
 .../cassandra/service/accord/EpochSyncTest.java    | 107 +++++++----------
 .../cassandra/utils/StatefulRangeTreeTest.java     |  98 +++++-----------
 5 files changed, 127 insertions(+), 214 deletions(-)

diff --git a/modules/accord b/modules/accord
index e2ccee4f51..a171322f41 160000
--- a/modules/accord
+++ b/modules/accord
@@ -1 +1 @@
-Subproject commit e2ccee4f51fe4c7c7f3ea8911897135ed7e37114
+Subproject commit a171322f417c117733ca5b514d03a5202b1ac202
diff --git a/test/unit/org/apache/cassandra/index/accord/RouteIndexTest.java 
b/test/unit/org/apache/cassandra/index/accord/RouteIndexTest.java
index 17de14e415..edc7f2c517 100644
--- a/test/unit/org/apache/cassandra/index/accord/RouteIndexTest.java
+++ b/test/unit/org/apache/cassandra/index/accord/RouteIndexTest.java
@@ -22,7 +22,6 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -49,7 +48,6 @@ import accord.primitives.TxnId;
 import accord.utils.Gen;
 import accord.utils.Gens;
 import accord.utils.Property.Command;
-import accord.utils.Property.Commands;
 import accord.utils.Property.UnitCommand;
 import accord.utils.RandomSource;
 import org.agrona.collections.Int2ObjectHashMap;
@@ -73,6 +71,7 @@ import org.apache.cassandra.utils.RTree;
 import org.apache.cassandra.utils.RangeTree;
 import org.assertj.core.api.Assertions;
 
+import static accord.utils.Property.commands;
 import static accord.utils.Property.stateful;
 
 public class RouteIndexTest extends CQLTester.InMemory
@@ -109,85 +108,61 @@ public class RouteIndexTest extends CQLTester.InMemory
         cfs().disableAutoCompaction(); // let the test control compaction
         //TODO (coverage): include with the ability to mark ranges as durable 
for compaction cleanup
         AccordService.unsafeSetNoop(); // disable accord service since 
compaction touches it.  It would be nice to include this for cleanup support....
-        stateful().withExamples(50).check(new Commands<State, 
ColumnFamilyStore>()
-        {
-            @Override
-            public Gen<State> genInitialState()
-            {
-                return rs -> new State(rs);
-            }
-
-            @Override
-            public ColumnFamilyStore createSut(State state)
-            {
-                return cfs();
-            }
+        stateful().withExamples(50).check(commands(() -> State::new, i -> 
cfs())
+                                          .destroySut(sut -> 
sut.truncateBlocking())
+                                          .add(FLUSH)
+                                          .add(COMPACT)
+                                          .add((rs, state) -> {
+                                              int storeId = rs.nextInt(0, 
state.numStores);
+                                              Domain domain = 
state.domainGen.next(rs);
+                                              TxnId txnId = 
state.nextTxnId(domain);
+                                              Route<?> route = 
createRoute(state, rs, domain, rs.nextInt(1, 20));
+                                              return new InsertTxn(storeId, 
txnId, SaveStatus.PreAccepted, Durability.NotDurable, route);
+                                          })
+                                          .add((rs, state) -> new 
RangeSearch(rs.nextInt(0, state.numStores), state.rangeGen.next(rs)))
+                                          .addIf(state -> 
!state.storeToTableToRangesToTxns.isEmpty(), RouteIndexTest::rangeSearch)
+                                          .build());
+    }
 
-            @Override
-            public Gen<Command<State, ColumnFamilyStore, ?>> commands(State 
state)
+    private static RangeSearch rangeSearch(RandomSource rs, State state)
+    {
+        int storeId = 
rs.pickUnorderedSet(state.storeToTableToRangesToTxns.keySet());
+        var tables = state.storeToTableToRangesToTxns.get(storeId);
+        TableId tableId = rs.pickUnorderedSet(tables.keySet());
+        var ranges = tables.get(tableId);
+        TreeSet<TokenRange> distinctRanges = 
ranges.stream().map(Map.Entry::getKey).collect(Collectors.toCollection(() -> 
new TreeSet<>(TokenRange::compareTo)));
+        TokenRange range;
+        if (distinctRanges.size() == 1)
+        {
+            range = Iterables.getFirst(distinctRanges, null);
+        }
+        else
+        {
+            switch (rs.nextInt(0, 2))
             {
-                Map<Gen<Command<State, ColumnFamilyStore, ?>>, Integer> 
possible = new LinkedHashMap<>();
-                possible.put(ignore -> FLUSH, 1);
-                possible.put(ignore -> COMPACT, 1);
-                possible.put(rs -> {
-                    int storeId = rs.nextInt(0, state.numStores);
-                    Domain domain = state.domainGen.next(rs);
-                    TxnId txnId = state.nextTxnId(domain);
-                    Route<?> route = createRoute(state, rs, domain, 
rs.nextInt(1, 20));
-                    return new InsertTxn(storeId, txnId, 
SaveStatus.PreAccepted, Durability.NotDurable, route);
-                }, 10);
-                possible.put(rs -> new RangeSearch(rs.nextInt(0, 
state.numStores), state.rangeGen.next(rs)), 1);
-                if (!state.storeToTableToRangesToTxns.isEmpty())
+                case 0: // perfect match
+                    range = rs.pickOrderedSet(distinctRanges);
+                    break;
+                case 1: // mutli-match
                 {
-                    possible.put(rs -> {
-                        int storeId = 
rs.pickUnorderedSet(state.storeToTableToRangesToTxns.keySet());
-                        var tables = 
state.storeToTableToRangesToTxns.get(storeId);
-                        TableId tableId = rs.pickUnorderedSet(tables.keySet());
-                        var ranges = tables.get(tableId);
-                        TreeSet<TokenRange> distinctRanges = 
ranges.stream().map(Map.Entry::getKey).collect(Collectors.toCollection(() -> 
new TreeSet<>(TokenRange::compareTo)));
-                        TokenRange range;
-                        if (distinctRanges.size() == 1)
-                        {
-                            range = Iterables.getFirst(distinctRanges, null);
-                        }
-                        else
-                        {
-                            switch (rs.nextInt(0, 2))
-                            {
-                                case 0: // perfect match
-                                    range = rs.pickOrderedSet(distinctRanges);
-                                    break;
-                                case 1: // mutli-match
-                                {
-                                    TokenRange a = 
rs.pickOrderedSet(distinctRanges);
-                                    TokenRange b = 
rs.pickOrderedSet(distinctRanges);
-                                    while (a.equals(b))
-                                        b = rs.pickOrderedSet(distinctRanges);
-                                    if (b.compareTo(a) < 0)
-                                    {
-                                        TokenRange tmp = a;
-                                        a = b;
-                                        b = tmp;
-                                    }
-                                    range = new TokenRange((AccordRoutingKey) 
a.start(), (AccordRoutingKey) b.end());
-                                }
-                                break;
-                                default:
-                                    throw new AssertionError();
-                            }
-                        }
-                        return new RangeSearch(storeId, range);
-                    }, 5);
+                    TokenRange a = rs.pickOrderedSet(distinctRanges);
+                    TokenRange b = rs.pickOrderedSet(distinctRanges);
+                    while (a.equals(b))
+                        b = rs.pickOrderedSet(distinctRanges);
+                    if (b.compareTo(a) < 0)
+                    {
+                        TokenRange tmp = a;
+                        a = b;
+                        b = tmp;
+                    }
+                    range = new TokenRange((AccordRoutingKey) a.start(), 
(AccordRoutingKey) b.end());
                 }
-                return Gens.oneOf(possible);
+                break;
+                default:
+                    throw new AssertionError();
             }
-
-            @Override
-            public void destroySut(ColumnFamilyStore sut)
-            {
-                cfs().truncateBlocking();
-            }
-        });
+        }
+        return new RangeSearch(storeId, range);
     }
 
     private static ColumnFamilyStore cfs()
@@ -343,7 +318,7 @@ public class RouteIndexTest extends CQLTester.InMemory
         }
     }
 
-    private class RangeSearch implements Command<State, ColumnFamilyStore, 
Set<TxnId>>
+    private static class RangeSearch implements Command<State, 
ColumnFamilyStore, Set<TxnId>>
     {
         private final int storeId;
         private final TokenRange range;
diff --git a/test/unit/org/apache/cassandra/io/util/ChecksumedDataTest.java 
b/test/unit/org/apache/cassandra/io/util/ChecksumedDataTest.java
index d7b6754a2d..5389c79bb7 100644
--- a/test/unit/org/apache/cassandra/io/util/ChecksumedDataTest.java
+++ b/test/unit/org/apache/cassandra/io/util/ChecksumedDataTest.java
@@ -24,6 +24,8 @@ import java.util.function.Supplier;
 import java.util.zip.CRC32C;
 import java.util.zip.Checksum;
 
+import javax.annotation.Nullable;
+
 import org.junit.Test;
 
 import accord.utils.Gen;
@@ -149,8 +151,9 @@ public class ChecksumedDataTest
             }
 
             @Override
-            public void destroySut(List<StatefulChecksumCommand<?>> sut) 
throws Throwable
+            public void destroySut(List<StatefulChecksumCommand<?>> sut, 
@Nullable Throwable t) throws Throwable
             {
+                if (t != null) return;
                 ChecksumedDataInputPlus in = new ChecksumedDataInputPlus(new 
DataInputBuffer(out.unsafeGetBufferAndFlip(), false), CHECKSUM_SUPPLIER);
                 for (StatefulChecksumCommand<?> cmd : sut)
                 {
diff --git a/test/unit/org/apache/cassandra/service/accord/EpochSyncTest.java 
b/test/unit/org/apache/cassandra/service/accord/EpochSyncTest.java
index 0c2fc4a79b..216889dd72 100644
--- a/test/unit/org/apache/cassandra/service/accord/EpochSyncTest.java
+++ b/test/unit/org/apache/cassandra/service/accord/EpochSyncTest.java
@@ -28,7 +28,6 @@ import java.util.EnumMap;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.NavigableSet;
@@ -60,10 +59,7 @@ import accord.primitives.Ranges;
 import accord.topology.Topology;
 import accord.topology.TopologyManager;
 import accord.utils.Gen;
-import accord.utils.Gens;
 import accord.utils.Invariants;
-import accord.utils.Property.Command;
-import accord.utils.Property.Commands;
 import accord.utils.Property.UnitCommand;
 import accord.utils.RandomSource;
 import accord.utils.async.AsyncChain;
@@ -109,6 +105,7 @@ import org.apache.cassandra.utils.ByteArrayUtil;
 import org.apache.cassandra.utils.Pair;
 import org.assertj.core.api.Assertions;
 
+import static accord.utils.Property.commands;
 import static accord.utils.Property.stateful;
 
 public class EpochSyncTest
@@ -126,72 +123,48 @@ public class EpochSyncTest
     @Test
     public void test()
     {
-        stateful().withExamples(50).check(new Commands<Cluster, Void>()
-        {
-            @Override
-            public Gen<Cluster> genInitialState()
-            {
-                return Cluster::new;
-            }
-
-            @Override
-            public Void createSut(Cluster Cluster)
-            {
-                return null;
-            }
+        stateful().withExamples(50).check(commands(() -> Cluster::new)
+                                          .destroyState(cluster -> {
+                                              cluster.processAll();
+                                              cluster.validate(true);
+                                          })
+                                          .addIf(cluster -> 
cluster.alive().size() <= cluster.maxNodes, EpochSyncTest::addNode)
+                                          .addIf(cluster -> 
cluster.alive().size() > cluster.minNodes, EpochSyncTest::removeNode)
+                                          .addIf(cluster -> cluster.hasWork(), 
EpochSyncTest::processSome)
+                                          .add(rs -> new 
SimpleCommand("Validate", c -> c.validate(false)))
+                                          .add((rs, cluster) -> new 
SimpleCommand("Bump Epoch " + (cluster.current.epoch.getEpoch() + 1), 
Cluster::bumpEpoch))
+                                          .build());
+    }
 
-            @Override
-            public Gen<Command<Cluster, Void, ?>> commands(Cluster cluster)
-            {
-                List<Node.Id> alive = cluster.alive();
-                Map<Gen<Command<Cluster, Void, ?>>, Integer> possible = new 
LinkedHashMap<>();
-                if (alive.size() < cluster.maxNodes)
-                {
-                    // add node
-                    possible.put(rs -> {
-                        Node.Id id = new Node.Id(++cluster.nodeCounter);
-                        long token = cluster.tokenGen.nextLong(rs);
-                        while (cluster.tokens.contains(token))
-                            token = cluster.tokenGen.nextLong(rs);
-                        long epoch = cluster.current.epoch.getEpoch() + 1;
-                        long finalToken = token;
-                        return new SimpleCommand("Add Node " + id + "; token=" 
+ token + ", epoch=" + epoch,
-                                                 c -> c.addNode(id, 
finalToken));
-                    }, 5);
-                }
-                if (alive.size() > cluster.minNodes)
-                {
-                    possible.put(rs -> {
-                        Node.Id pick = rs.pick(alive);
-                        long token = cluster.instances.get(pick).token;
-                        long epoch = cluster.current.epoch.getEpoch() + 1;
-                        return new SimpleCommand("Remove Node " + pick + "; 
token=" + token + "; epoch=" + epoch, c -> c.removeNode(pick));
-                    }, 3);
-                }
-                if (cluster.hasWork())
-                {
-                    possible.put(rs -> new SimpleCommand("Process Some",
-                                                         c -> {//noinspection 
StatementWithEmptyBody
-                                                             for (int i = 0, 
attempts = rs.nextInt(1, 100); i < attempts && c.processOne(); i++)
-                                                             {
-                                                             }
-                                                         }), 10);
-                }
+    private static SimpleCommand addNode(RandomSource rs, Cluster cluster)
+    {
+        Node.Id id = new Node.Id(++cluster.nodeCounter);
+        long token = cluster.tokenGen.nextLong(rs);
+        while (cluster.tokens.contains(token))
+            token = cluster.tokenGen.nextLong(rs);
+        long epoch = cluster.current.epoch.getEpoch() + 1;
+        long finalToken = token;
+        return new SimpleCommand("Add Node " + id + "; token=" + token + ", 
epoch=" + epoch,
+                                 c -> c.addNode(id, finalToken));
+    }
 
-                possible.put(rs -> new SimpleCommand("Validate",
-                                                     c -> c.validate(false)), 
1);
-                possible.put(rs -> new SimpleCommand("Bump Epoch " + 
(cluster.current.epoch.getEpoch() + 1),
-                                                     Cluster::bumpEpoch), 10);
-                return Gens.oneOf(possible);
-            }
+    private static SimpleCommand removeNode(RandomSource rs, Cluster cluster)
+    {
+        List<Node.Id> alive = cluster.alive();
+        Node.Id pick = rs.pick(alive);
+        long token = cluster.instances.get(pick).token;
+        long epoch = cluster.current.epoch.getEpoch() + 1;
+        return new SimpleCommand("Remove Node " + pick + "; token=" + token + 
"; epoch=" + epoch, c -> c.removeNode(pick));
+    }
 
-            @Override
-            public void destroyState(Cluster cluster)
-            {
-                cluster.processAll();
-                cluster.validate(true);
-            }
-        });
+    private static SimpleCommand processSome(RandomSource rs)
+    {
+        return new SimpleCommand("Process Some",
+                                 c -> {//noinspection StatementWithEmptyBody
+                                     for (int i = 0, attempts = rs.nextInt(1, 
100); i < attempts && c.processOne(); i++)
+                                     {
+                                     }
+                                 });
     }
 
     private static class SimpleCommand implements UnitCommand<Cluster, Void>
diff --git a/test/unit/org/apache/cassandra/utils/StatefulRangeTreeTest.java 
b/test/unit/org/apache/cassandra/utils/StatefulRangeTreeTest.java
index e3e471b550..139f172dfb 100644
--- a/test/unit/org/apache/cassandra/utils/StatefulRangeTreeTest.java
+++ b/test/unit/org/apache/cassandra/utils/StatefulRangeTreeTest.java
@@ -21,7 +21,6 @@ package org.apache.cassandra.utils;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeSet;
@@ -35,12 +34,12 @@ import accord.primitives.Range;
 import accord.utils.Gen;
 import accord.utils.Gens;
 import accord.utils.Property.Command;
-import accord.utils.Property.Commands;
 import accord.utils.Property.UnitCommand;
 import accord.utils.RandomSource;
 import org.apache.cassandra.service.accord.RangeTreeRangeAccessor;
 import org.assertj.core.api.Assertions;
 
+import static accord.utils.Property.commands;
 import static accord.utils.Property.stateful;
 
 public class StatefulRangeTreeTest
@@ -61,7 +60,7 @@ public class StatefulRangeTreeTest
 
     /**
      * Stateful test for RTree.
-     *
+     * <p>
      * This test is very similar to {@link RangeTreeTest#test} but is fully 
mutable, so can not
      * use the immutable search trees (else rebuidling becomes a large cost).  
Both tests should exist as they use different
      * models, which helps build confidence that the RTree does the correct 
thing; that test also covers start and end
@@ -70,57 +69,25 @@ public class StatefulRangeTreeTest
     @Test
     public void test()
     {
-        stateful().check(new Commands<State, Sut>()
-        {
-            @Override
-            public Gen<State> genInitialState()
-            {
-                return rs -> {
-                    Gen<Range> rangeGen = rangeGen(rs);
-                    int numChildren = NUM_CHILDREN_GEN.nextInt(rs);
-                    int sizeTarget = 
SIZE_TARGET_DISTRIBUTION.next(rs).filter(s -> s > numChildren).nextInt(rs);
-                    int createWeight = rs.nextInt(1, 100);
-                    int updateWeight = rs.nextInt(1, 20);
-                    int deleteWeight = rs.nextInt(1, 20);
-                    int clearWeight = rs.nextInt(0, 2); // either disabled or 
enabled with weight=1
-                    int readWeight = rs.nextInt(1, 20);
-                    return new State(sizeTarget, numChildren,
-                                     TOKEN_DISTRIBUTION.next(rs), rangeGen,
-                                     createWeight, updateWeight, deleteWeight, 
clearWeight, readWeight);
-                };
-            }
-
-            @Override
-            public Sut createSut(State state)
-            {
-                return new Sut(state.sizeTarget, state.numChildren);
-            }
-
-            @Override
-            public Gen<Command<State, Sut, ?>> commands(State state)
-            {
-                Map<Gen<Command<State, Sut, ?>>, Integer> possible = new 
LinkedHashMap<>();
-                possible.put(rs -> new Create(state.newRange(rs), 
SMALL_INT_GEN.nextInt(rs)), state.createWeight);
-                possible.put(rs -> new Read(state.newRange(rs)), 
state.readWeight);
-                possible.put(rs -> new 
KeyRead(IntKey.routing(state.tokenGen.nextInt(rs))), state.readWeight);
-                possible.put(rs -> new RangeRead(state.rangeGen.next(rs)), 
state.readWeight);
-                possible.put(ignore -> Iterate.instance, state.readWeight);
-                possible.put(ignore -> Clear.instance, state.clearWeight);
-                if (!state.uniqRanges.isEmpty())
-                {
-                    possible.put(rs -> new 
Read(rs.pickOrderedSet(state.uniqRanges)), state.readWeight);
-                    possible.put(rs -> {
-                        Range range = rs.pickOrderedSet(state.uniqRanges);
-                        int token = rs.nextInt(((IntKey.Routing) 
range.start()).key, ((IntKey.Routing) range.end()).key) + 1;
-                        return new KeyRead(IntKey.routing(token));
-                    }, state.readWeight);
-                    possible.put(rs -> new 
RangeRead(rs.pickOrderedSet(state.uniqRanges)), state.readWeight);
-                    possible.put(rs -> new 
Update(rs.pickOrderedSet(state.uniqRanges), SMALL_INT_GEN.nextInt(rs)), 
state.updateWeight);
-                    possible.put(rs -> new 
Delete(rs.pickOrderedSet(state.uniqRanges)), state.deleteWeight);
-                }
-                return Gens.oneOf(possible);
-            }
-        });
+        stateful().check(commands(() -> State::new, state -> new 
Sut(state.sizeTarget, state.numChildren))
+                         .add((rs, state) -> new Create(state.newRange(rs), 
SMALL_INT_GEN.nextInt(rs)))
+                         .add((rs, state) -> new Read(state.newRange(rs)))
+                         .add((rs, state) -> new 
KeyRead(IntKey.routing(state.tokenGen.nextInt(rs))))
+                         .add((rs, state) -> new 
RangeRead(state.rangeGen.next(rs)))
+                         .add(Iterate.instance)
+                         .add(Clear.instance)
+                         .addAllIf(state -> !state.uniqRanges.isEmpty(),
+                                   b -> b.add((rs, state) -> new 
Read(rs.pickOrderedSet(state.uniqRanges)))
+                                         .add((rs, state) -> {
+                                             Range range = 
rs.pickOrderedSet(state.uniqRanges);
+                                             int token = 
rs.nextInt(((IntKey.Routing) range.start()).key, ((IntKey.Routing) 
range.end()).key) + 1;
+                                             return new 
KeyRead(IntKey.routing(token));
+                                         })
+                                         .add((rs, state) -> new 
RangeRead(rs.pickOrderedSet(state.uniqRanges)))
+                                         .add((rs, state) -> new 
Update(rs.pickOrderedSet(state.uniqRanges), SMALL_INT_GEN.nextInt(rs)))
+                                         .add((rs, state) -> new 
Delete(rs.pickOrderedSet(state.uniqRanges)))
+                         )
+                         .build());
     }
 
     private static Gen<Range> rangeGen(RandomSource rand)
@@ -401,6 +368,7 @@ public class StatefulRangeTreeTest
     static class Iterate extends AbstractRead<Map.Entry<Range, Integer>>
     {
         static final Iterate instance = new Iterate();
+
         public Iterate()
         {
             super(COMPARATOR);
@@ -432,27 +400,21 @@ public class StatefulRangeTreeTest
         private final int sizeTarget, numChildren;
         private final Gen.IntGen tokenGen;
         private final Gen<Range> rangeGen;
-        private final int createWeight, updateWeight, deleteWeight, 
clearWeight, readWeight;
 
-        private State(int sizeTarget, int numChildren,
-                      Gen.IntGen tokenGen, Gen<Range> rangeGen,
-                      int createWeight, int updateWeight, int deleteWeight, 
int clearWeight, int readWeight)
+        private State(RandomSource rs)
         {
-            this.sizeTarget = sizeTarget;
-            this.numChildren = numChildren;
-            this.tokenGen = tokenGen;
-            this.rangeGen = rangeGen;
-            this.createWeight = createWeight;
-            this.updateWeight = updateWeight;
-            this.deleteWeight = deleteWeight;
-            this.clearWeight = clearWeight;
-            this.readWeight = readWeight;
+            this.numChildren = NUM_CHILDREN_GEN.nextInt(rs);
+            this.sizeTarget = SIZE_TARGET_DISTRIBUTION.next(rs).filter(s -> s 
> numChildren).nextInt(rs);
+            this.tokenGen = TOKEN_DISTRIBUTION.next(rs);
+            this.rangeGen = rangeGen(rs);
         }
 
         public Range newRange(RandomSource rs)
         {
             Range range;
-            while ((uniqRanges.contains(range = rangeGen.next(rs)))) {}
+            while ((uniqRanges.contains(range = rangeGen.next(rs))))
+            {
+            }
             return range;
         }
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to