-
Notifications
You must be signed in to change notification settings - Fork 113
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds testing of IonValue's multithreaded contract over the 'good' ion…
…-tests files. (#769) * Adds testing of IonValue's multithreaded contract over the 'good' ion-tests files. * Update src/test/java/com/amazon/ion/GoodIonMultithreadedTest.java Co-authored-by: Matthew Pope <81593196+popematt@users.noreply.github.com> * Run spotlessApply to fix failing CI check --------- Co-authored-by: Matthew Pope <81593196+popematt@users.noreply.github.com> Co-authored-by: Matthew Pope <popematt@amazon.com>
- Loading branch information
1 parent
eb6c877
commit 2e6be64
Showing
3 changed files
with
191 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
191 changes: 191 additions & 0 deletions
191
src/test/java/com/amazon/ion/GoodIonMultithreadedTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
package com.amazon.ion; | ||
|
||
import com.amazon.ion.junit.Injected; | ||
import org.junit.Test; | ||
|
||
import java.io.File; | ||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.Collections; | ||
import java.util.Iterator; | ||
import java.util.List; | ||
import java.util.Random; | ||
import java.util.concurrent.CompletableFuture; | ||
|
||
import static com.amazon.ion.TestUtils.GLOBAL_SKIP_LIST; | ||
import static com.amazon.ion.TestUtils.GOOD_IONTESTS_FILES; | ||
import static com.amazon.ion.TestUtils.testdataFiles; | ||
|
||
/** | ||
* Tests IonValue's multithreaded contract over the `good` ion-tests files. | ||
* | ||
* For each file, the test randomly chooses a child value to test and marks that value as read-only. Then | ||
* it spawns many instances of an async task to concurrently perform read operations on the chosen value. | ||
* Finally, it asserts that all instances of the async task got the same results from the read operations. | ||
* | ||
* Testing every value at every depth would be more robust, but is likely more suited to a suite of | ||
* long-running tests that are executed periodically, rather than the unit tests that are executed on each build. | ||
* This test is expected to take only a few seconds to execute. | ||
*/ | ||
public class GoodIonMultithreadedTest extends IonTestCase { | ||
|
||
@Injected.Inject("testFile") | ||
public static final File[] FILES = testdataFiles(GLOBAL_SKIP_LIST, GOOD_IONTESTS_FILES); | ||
|
||
private File myTestFile; | ||
|
||
public void setTestFile(File file) { | ||
myTestFile = file; | ||
} | ||
|
||
/** | ||
* Wraps a byte array to provide an implementation of `equals` that compares the content of the array. | ||
*/ | ||
private static class ByteArrayWrapper { | ||
private final byte[] array; | ||
|
||
ByteArrayWrapper(byte[] array) { | ||
this.array = array; | ||
} | ||
|
||
@Override | ||
public boolean equals(Object that) { | ||
return that instanceof ByteArrayWrapper && Arrays.equals(array, ((ByteArrayWrapper) that).array); | ||
} | ||
} | ||
|
||
/** | ||
* Ensures all elements in the given list are equivalent to each other. | ||
* @param list the list to test. | ||
*/ | ||
private static void assertAllElementsEqual(List<?> list) { | ||
int i = 0; int j = 1; | ||
while (i < list.size() && j < list.size()) { | ||
assertEquals(list.get(i), list.get(j)); | ||
i++; | ||
j++; | ||
} | ||
} | ||
|
||
/** | ||
* Makes the given IonValue read-only, then accesses its attributes concurrently from many threads. Verifies that | ||
* the results are the same from each thread. | ||
* @param value the value to test. | ||
*/ | ||
private void testMultithreadedAccess(IonValue value) { | ||
// Make read-only so that concurrent access is legal. | ||
value.makeReadOnly(); | ||
// Concurrently access the value using many threads, recording the results. | ||
int numberOfTasks = 100; | ||
List<CompletableFuture<Void>> tasks = new ArrayList<>(); | ||
List<IonValue> parentContainers = Collections.synchronizedList(new ArrayList<>()); | ||
List<Integer> hashCodes = Collections.synchronizedList(new ArrayList<>()); | ||
List<SymbolToken> fieldNames = Collections.synchronizedList(new ArrayList<>()); | ||
List<List<SymbolToken>> annotations = Collections.synchronizedList(new ArrayList<>()); | ||
List<List<Object>> values = Collections.synchronizedList(new ArrayList<>()); | ||
for (int task = 0; task < numberOfTasks; task++) { | ||
tasks.add(CompletableFuture.runAsync( | ||
() -> { | ||
hashCodes.add(value.hashCode()); | ||
parentContainers.add(value.getContainer()); | ||
fieldNames.add(value.getFieldNameSymbol()); | ||
annotations.add(Arrays.asList(value.getTypeAnnotationSymbols())); | ||
if (value.isNullValue()) { | ||
values.add(Collections.singletonList(value.getType())); | ||
} else { | ||
switch (value.getType()) { | ||
case NULL: | ||
values.add(Collections.singletonList(value.getType())); | ||
break; | ||
case BOOL: | ||
values.add(Collections.singletonList(((IonBool) value).booleanValue())); | ||
break; | ||
case INT: | ||
switch (((IonInt) value).getIntegerSize()) { | ||
case INT: | ||
case LONG: | ||
values.add(Collections.singletonList(((IonInt) value).longValue())); | ||
break; | ||
case BIG_INTEGER: | ||
values.add(Collections.singletonList(((IonInt) value).bigIntegerValue())); | ||
break; | ||
} | ||
break; | ||
case FLOAT: | ||
values.add(Collections.singletonList(((IonFloat) value).doubleValue())); | ||
break; | ||
case DECIMAL: | ||
values.add(Collections.singletonList(((IonDecimal) value).decimalValue())); | ||
break; | ||
case TIMESTAMP: | ||
values.add(Collections.singletonList(((IonTimestamp) value).timestampValue())); | ||
break; | ||
case SYMBOL: | ||
values.add(Collections.singletonList(((IonSymbol) value).symbolValue())); | ||
break; | ||
case STRING: | ||
values.add(Collections.singletonList(((IonString) value).stringValue())); | ||
break; | ||
case CLOB: | ||
case BLOB: | ||
values.add(Collections.singletonList(new ByteArrayWrapper(((IonLob) value).getBytes()))); | ||
break; | ||
case LIST: | ||
case SEXP: | ||
case STRUCT: | ||
case DATAGRAM: | ||
IonContainer container = (IonContainer) value; | ||
List<Object> children = new ArrayList<>(); | ||
container.forEach(children::add); | ||
values.add(children); | ||
break; | ||
} | ||
} | ||
} | ||
)); | ||
} | ||
tasks.forEach(CompletableFuture::join); | ||
assertAllElementsEqual(parentContainers); | ||
assertAllElementsEqual(hashCodes); | ||
assertAllElementsEqual(fieldNames); | ||
assertAllElementsEqual(annotations); | ||
assertAllElementsEqual(values); | ||
} | ||
|
||
/** | ||
* Selects a child value of the container at an arbitrary depth and index. | ||
* @param random source of randomness for the selection. | ||
* @param container the container. | ||
* @return a value. | ||
*/ | ||
private IonValue selectValueToTest(Random random, IonContainer container) { | ||
int numberOfValues = container.size(); | ||
int valueToTarget = random.nextInt(numberOfValues + 1); | ||
IonValue value = null; | ||
if (valueToTarget == numberOfValues) { | ||
// Don't descend further. | ||
value = container; | ||
} else { | ||
Iterator<IonValue> iterator = container.iterator(); | ||
for (int i = 0; i <= valueToTarget; i++) { | ||
value = iterator.next(); | ||
} | ||
if (value instanceof IonContainer) { | ||
// Descend into the container and select one of its children. | ||
value = selectValueToTest(random, (IonContainer) value); | ||
} | ||
} | ||
return value; | ||
} | ||
|
||
@Test | ||
public void testRandomMultithreadedAccess() throws Exception { | ||
// Descend to a random child value, make that value read-only, then concurrently access that value. | ||
Random random = new Random(); | ||
IonDatagram datagram = load(myTestFile); | ||
IonValue value = selectValueToTest(random, datagram); | ||
testMultithreadedAccess(value); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters