1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.statistics.distribution;
18
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.lang.reflect.InvocationTargetException;
22 import java.lang.reflect.Method;
23 import java.lang.reflect.Modifier;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.List;
27 import java.util.Locale;
28 import java.util.Properties;
29 import java.util.function.Function;
30 import java.util.function.Predicate;
31 import java.util.stream.Stream;
32 import java.util.stream.Stream.Builder;
33 import org.junit.jupiter.api.Assertions;
34 import org.junit.jupiter.api.Assumptions;
35 import org.junit.jupiter.api.BeforeAll;
36 import org.junit.jupiter.api.Named;
37 import org.junit.jupiter.api.TestInstance;
38 import org.junit.jupiter.api.TestInstance.Lifecycle;
39 import org.junit.jupiter.api.extension.ParameterContext;
40 import org.junit.jupiter.params.ParameterizedTest;
41 import org.junit.jupiter.params.aggregator.AggregateWith;
42 import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
43 import org.junit.jupiter.params.aggregator.ArgumentsAggregationException;
44 import org.junit.jupiter.params.aggregator.ArgumentsAggregator;
45 import org.junit.jupiter.params.provider.Arguments;
46 import org.junit.jupiter.params.provider.MethodSource;
47
48 /**
49 * Abstract base class for distribution tests.
50 *
51 * <p>This class uses parameterized tests that are repeated for instances of a
52 * distribution. The distribution, test input and expected values are generated
53 * dynamically from properties files loaded from resources.
54 *
55 * <p>The class has two specializations for testing {@link ContinuousDistribution} and
56 * {@link DiscreteDistribution}. It is not intended to extend this class when creating
57 * a test for a new distribution. This class exists for the sole purpose of containing
58 * common functionality to search for and load properties files containing the distribution
59 * data.
60 *
61 * <p>To test a new distribution extend the specialized classes:
62 * <ul>
63 * <li>{@link BaseContinuousDistributionTest}
64 * <li>{@link BaseDiscreteDistributionTest}
65 * </ul>
66 *
67 * @param <T> Distribution type
68 * @param <D> Distribution data type
69 */
70 @TestInstance(Lifecycle.PER_CLASS)
71 abstract class BaseDistributionTest<T, D extends DistributionTestData> {
72 /**
73 * The smallest value (epsilon) for the relative error of a {@code double}.
74 * Set the relative error to an integer factor of this to test very
75 * small differences as errors of units in the last place (ULP).
76 * Assumes the relative error is:
77 * <pre>
78 * |x - y|
79 * -------------
80 * max(|x|, |y|)
81 * </pre>
82 *
83 * <p>Value is 2.220446049250313E-16.
84 */
85 static final double RELATIVE_EPS = Math.ulp(1.0);
86
87 /** The test data. Protected to allow use in sub-classes. */
88 protected final List<D> data = new ArrayList<>();
89
90 /**
91 * Setup the test using data loaded from resource files.
92 * Resource files are assumed to be named sequentially from 1:
93 * <pre>
94 * test.distname.1.properties
95 * test.distname.2.properties
96 * </pre>
97 * <p>Where {@code distname} is the name of the distribution. The name
98 * is dynamically created in {@link #getDistributionName()} and can be
99 * overridden by implementing classes.
100 */
101 @BeforeAll
102 void setup() {
103 final String key = getDistributionName().toLowerCase(Locale.ROOT);
104 // Set defaults
105 final Properties defaults = new Properties();
106 defaults.setProperty(DistributionTestData.KEY_TOLERANCE_ABSOLUTE, String.valueOf(getAbsoluteTolerance()));
107 defaults.setProperty(DistributionTestData.KEY_TOLERANCE_RELATIVE, String.valueOf(getRelativeTolerance()));
108 for (int i = 1; ; i++) {
109 final String filename = String.format("test.%s.%d.properties", key, i);
110 try (InputStream resource = this.getClass().getResourceAsStream(
111 filename)) {
112 if (resource == null) {
113 break;
114 }
115 // Load properties file
116 final Properties prop = new Properties(defaults);
117 prop.load(resource);
118 // Convert the properties to a D instance
119 data.add(makeDistributionData(prop));
120 } catch (IOException | NullPointerException | IllegalArgumentException e) {
121 Assertions.fail("Failed to load test data: " + filename, e);
122 }
123 }
124 }
125
126 /**
127 * Gets the default absolute tolerance used in comparing expected and returned values.
128 *
129 * <p>The initial value is 0.0 (disabled).
130 *
131 * <p>Override this method to set the <strong>default</strong> absolute tolerance for all test
132 * cases defined by a properties file. Any properties file with an absolute tolerance entry
133 * ignores this value.
134 *
135 * <p>Notes: Floating-point values are considered equal using the absolute or the relative tolerance.
136 * See {@link #createTolerance()}.
137 *
138 * @return the absolute tolerance
139 */
140 protected double getAbsoluteTolerance() {
141 return 0.0;
142 }
143
144 /**
145 * Gets the default relative tolerance used in comparing expected and returned values.
146 *
147 * <p>The initial value is 1e-14.
148 *
149 * <p>Override this method to set the <strong>default</strong> relative tolerance for all test
150 * cases defined by a properties file. Any properties file with a relative tolerance entry
151 * ignores this value.
152 *
153 * <p>Notes: Floating-point values are considered equal using the absolute or the relative tolerance.
154 * See {@link #createTolerance()}.
155 *
156 * @return the relative tolerance
157 */
158 protected double getRelativeTolerance() {
159 return 1e-14;
160 }
161
162 /**
163 * Gets the distribution name. This is used to search for test case resource files.
164 *
165 * <p>The default implementation removes the text {@code DistributionTest} from the
166 * simple class name.
167 *
168 * @return the distribution name
169 * @see Class#getSimpleName()
170 */
171 String getDistributionName() {
172 return getClass().getSimpleName().replace("DistributionTest", "");
173 }
174
175 /**
176 * Create a new distribution data instance from the properties.
177 *
178 * @param properties Properties
179 * @return the distribution data
180 */
181 abstract D makeDistributionData(Properties properties);
182
183 /**
184 * Create a new distribution instance from the parameters.
185 * It is assumed the parameters match the order of the parameter constructor.
186 *
187 * @param parameters Parameters of the distribution.
188 * @return the distribution
189 */
190 abstract T makeDistribution(Object... parameters);
191
192 /** Creates invalid parameters that are expected to throw an exception when passed to
193 * the {@link #makeDistribution(Object...)} method.
194 *
195 * <p>This may return as many inner parameter arrays as is required to test all permutations
196 * of invalid parameters to the distribution.
197 * @return Array of invalid parameter arrays
198 */
199 abstract Object[][] makeInvalidParameters();
200
201 /**
202 * Gets the parameter names.
203 * The names will be used with reflection to identify a parameter accessor in the distribution
204 * with the name {@code getX()} where {@code X} is the parameter name.
205 * The names should use the same order as {@link #makeDistribution(Object...)}.
206 *
207 * <p>Return {@code null} to ignore this test. Return {@code null} for an element of the
208 * returned array to ignore that parameter.
209 *
210 * @return the parameter names
211 */
212 abstract String[] getParameterNames();
213
214
215 //------------------------ Helper Methods to create test tolerances---------------------------
216
217 /**
218 * Creates the tolerance using an absolute error.
219 *
220 * <p>If the absolute tolerance is zero it is ignored and a tolerance of numerical
221 * equality is used.
222 *
223 * @param eps Absolute tolerance
224 * @return the tolerance
225 */
226 DoubleTolerance createAbsTolerance(double eps) {
227 return eps > 0 ? DoubleTolerances.absolute(eps) : DoubleTolerances.ulps(0);
228 }
229
230 /**
231 * Creates the tolerance using an relative error.
232 *
233 * <p>If the relative tolerance is zero it is ignored and a tolerance of numerical
234 * equality is used.
235 *
236 * @param eps Relative tolerance
237 * @return the tolerance
238 */
239 DoubleTolerance createRelTolerance(double eps) {
240 return eps > 0 ? DoubleTolerances.relative(eps) : DoubleTolerances.ulps(0);
241 }
242
243 /**
244 * Creates the tolerance using an {@code Or} combination of absolute and relative error.
245 *
246 * <p>If the absolute tolerance is zero it is ignored and a tolerance of numerical equality
247 * is used.
248 *
249 * <p>If the relative tolerance is zero it is ignored.
250 *
251 * @param absTolerance Absolute tolerance
252 * @param relTolerance Relative tolerance
253 * @return the tolerance
254 */
255 DoubleTolerance createAbsOrRelTolerance(double absTolerance, double relTolerance) {
256 final DoubleTolerance tol = createAbsTolerance(absTolerance);
257 return relTolerance > 0 ? tol.or(DoubleTolerances.relative(relTolerance)) : tol;
258 }
259
260 /**
261 * Creates the tolerance using an {@code Or} combination of absolute and relative error
262 * defined in the test data.
263 *
264 * <p>If the absolute tolerance is zero it is ignored and a tolerance of numerical equality
265 * is used.
266 *
267 * <p>If the relative tolerance is zero it is ignored.
268 *
269 * @param testData Test data
270 * @return the tolerance
271 */
272 DoubleTolerance createTestTolerance(D testData) {
273 final double abs = testData.getAbsoluteTolerance();
274 final double rel = testData.getRelativeTolerance();
275 return createAbsOrRelTolerance(abs, rel);
276 }
277
278 /**
279 * Creates the tolerance for the named test using an {@code Or} combination of absolute
280 * and relative error defined in the test data. If the named test tolerance is not defined
281 * then this uses the default tolerance.
282 *
283 * <p>If the absolute tolerance is zero it is ignored and a tolerance of numerical equality
284 * is used.
285 *
286 * <p>If the relative tolerance is zero it is ignored.
287 *
288 * @param testData Test data
289 * @param name Name of the function under test
290 * @return the tolerance
291 */
292 DoubleTolerance createTestTolerance(D testData, TestName name) {
293 final double abs = testData.getAbsoluteTolerance(name);
294 final double rel = testData.getRelativeTolerance(name);
295 return createAbsOrRelTolerance(abs, rel);
296 }
297
298 /**
299 * Creates the default tolerance.
300 *
301 * <p>If the absolute tolerance is zero it is ignored and a tolerance of numerical equality
302 * is used.
303 *
304 * <p>If the relative tolerance is zero it is ignored.
305 *
306 * @return the tolerance
307 */
308 DoubleTolerance createTolerance() {
309 return createAbsOrRelTolerance(getAbsoluteTolerance(),
310 getRelativeTolerance());
311 }
312
313 //------------------------ Methods to stream the test data -----------------------------
314
315 // The @MethodSource annotation will default to a no arguments method of the same name
316 // as the @ParameterizedTest method. These can be overridden by child classes to
317 // stream different arguments to the test case.
318
319 /**
320 * Create a named argument for the distribution from the parameters.
321 * This is a convenience method to present the distribution with a short name in a test report.
322 *
323 * <p>This is used to create a new instance of the distribution for a test.
324 *
325 * @param parameters Parameters of the distribution.
326 * @return the distribution argument
327 */
328 Named<T> namedDistribution(Object... parameters) {
329 final T dist = makeDistribution(parameters);
330 final String name = dist.getClass().getSimpleName() + " " + Arrays.toString(parameters);
331 return Named.of(name, dist);
332 }
333
334 /**
335 * Create a named argument for the array.
336 * This is a convenience method to present arrays with a short name in a test report.
337 * May be overridden for example to output more array details.
338 *
339 * @param name Name
340 * @param array Array
341 * @return the named argument
342 */
343 Named<?> namedArray(String name, Object array) {
344 if (array instanceof double[]) {
345 return namedArray(name, (double[]) array);
346 }
347 if (array instanceof int[]) {
348 return namedArray(name, (int[]) array);
349 }
350 return Named.of(name, array);
351 }
352
353 /**
354 * Create a named argument for the array.
355 * This is a convenience method to present arrays with a short name in a test report.
356 * May be overridden for example to output more array details.
357 *
358 * @param name Name
359 * @param array Array
360 * @return the named argument
361 */
362 Named<double[]> namedArray(String name, double[] array) {
363 // Create the name using the first 3 elements
364 final StringBuilder sb = new StringBuilder(75);
365 sb.append(name);
366 // Assume length is non-zero length
367 int i = 0;
368 sb.append(" [");
369 sb.append(array[i++]);
370 while (i < Math.min(3, array.length)) {
371 sb.append(", ");
372 sb.append(array[i++]);
373 }
374 if (i < array.length) {
375 sb.append(", ... ");
376 }
377 sb.append(']');
378 return Named.of(sb.toString(), array);
379 }
380
381 /**
382 * Create a named argument for the array.
383 * This is a convenience method to present arrays with a short name in a test report.
384 * May be overridden for example to output more array details.
385 *
386 * @param name Name
387 * @param array Array
388 * @return the named argument
389 */
390 Named<int[]> namedArray(String name, int[] array) {
391 // Create the name using the first 3 elements
392 final StringBuilder sb = new StringBuilder(75);
393 sb.append(name);
394 // Assume length is non-zero length
395 int i = 0;
396 sb.append(" [");
397 sb.append(array[i++]);
398 while (i < Math.min(3, array.length)) {
399 sb.append(", ");
400 sb.append(array[i++]);
401 }
402 if (i < array.length) {
403 sb.append(", ... ");
404 }
405 sb.append(']');
406 return Named.of(sb.toString(), array);
407 }
408
409 /**
410 * Create a stream of arguments containing the distribution to test.
411 *
412 * @return the stream
413 */
414 Stream<Arguments> streamDistribution() {
415 return data.stream().map(d -> Arguments.of(namedDistribution(d.getParameters())));
416 }
417
418 /**
419 * Create a stream of arguments containing the distribution to test and the test tolerance.
420 * The tolerance is identified using functions on the test instance data.
421 * The test data will be skipped if disabled.
422 *
423 * @param name Name of the function under test
424 * @return the stream
425 */
426 Stream<Arguments> stream(TestName name) {
427 final Builder<Arguments> b = Stream.builder();
428 final int[] size = {0};
429 data.forEach(d -> {
430 if (d.isDisabled(name)) {
431 return;
432 }
433 size[0]++;
434 b.accept(Arguments.of(namedDistribution(d.getParameters()),
435 createTestTolerance(d, name)));
436 });
437 Assumptions.assumeTrue(size[0] != 0, () -> "Distribution has no data for " + name);
438 return b.build();
439 }
440
441 /**
442 * Create a stream of arguments containing the distribution to test, the test
443 * points, and the test tolerance. The points and tolerance
444 * are identified using functions on the test instance data.
445 * The test data will be skipped if disabled or the length of the points is zero.
446 *
447 * <p>If all test data is skipped then a
448 * {@link org.opentest4j.TestAbortedException TestAbortedException} is raised.
449 *
450 * @param name Name of the function under test
451 * @param points Function to create the points
452 * @return the stream
453 */
454 <P> Stream<Arguments> stream(TestName name,
455 Function<D, P> points) {
456 final Builder<Arguments> b = Stream.builder();
457 final int[] size = {0};
458 data.forEach(d -> {
459 final P p = points.apply(d);
460 if (d.isDisabled(name) || TestUtils.getLength(p) == 0) {
461 return;
462 }
463 size[0]++;
464 b.accept(Arguments.of(namedDistribution(d.getParameters()),
465 namedArray("points", p),
466 createTestTolerance(d, name)));
467 });
468 Assumptions.assumeTrue(size[0] != 0, () -> "Distribution has no data for " + name);
469 return b.build();
470 }
471
472 /**
473 * Create a stream of arguments containing the distribution to test, the test
474 * points, test values and the test tolerance. The points, values and tolerance
475 * are identified using functions on the test instance data.
476 * The test data will be skipped if disabled or the length of the points or values is zero.
477 *
478 * <p>If all test data is skipped then a
479 * {@link org.opentest4j.TestAbortedException TestAbortedException} is raised.
480 *
481 * @param name Name of the function under test
482 * @param points Function to create the points
483 * @param values Function to create the values
484 * @return the stream
485 */
486 <P, V> Stream<Arguments> stream(TestName name,
487 Function<D, P> points,
488 Function<D, V> values) {
489 // Delegate
490 return stream(d -> d.isDisabled(name),
491 points,
492 values,
493 d -> createTestTolerance(d, name),
494 name.toString());
495 }
496
497 /**
498 * Create a stream of arguments containing the distribution to test, the test
499 * points, test values and the test tolerance. The points, values and tolerance
500 * are identified using functions on the test instance data.
501 * The test data will be skipped if disabled or the length of the points or values is zero.
502 *
503 * <p>If all test data is skipped then a
504 * {@link org.opentest4j.TestAbortedException TestAbortedException} is raised.
505 *
506 * @param filter Filter applied on the test data. If true the data is ignored.
507 * @param points Function to create the points
508 * @param values Function to create the values
509 * @param tolerance Function to create the tolerance
510 * @param name Name of the function under test
511 * @return the stream
512 */
513 <P, V> Stream<Arguments> stream(Predicate<D> filter,
514 Function<D, P> points,
515 Function<D, V> values,
516 Function<D, DoubleTolerance> tolerance,
517 String name) {
518 final Builder<Arguments> b = Stream.builder();
519 final int[] size = {0};
520 data.forEach(d -> {
521 final P p = points.apply(d);
522 final V v = values.apply(d);
523 if (filter.test(d) || TestUtils.getLength(p) == 0 || TestUtils.getLength(v) == 0) {
524 return;
525 }
526 size[0]++;
527 b.accept(Arguments.of(namedDistribution(d.getParameters()),
528 namedArray("points", p),
529 namedArray("values", v),
530 tolerance.apply(d)));
531 });
532 Assumptions.assumeTrue(size[0] != 0, () -> "Distribution has no data for " + name);
533 return b.build();
534 }
535
536 /**
537 * Create a stream of arguments built using the provided mapping function.
538 * The test data will be skipped if disabled.
539 *
540 * <p>If all test data is skipped then a
541 * {@link org.opentest4j.TestAbortedException TestAbortedException} is raised.
542 *
543 * @param name Name of the function under test
544 * @param mappingFunction Function to create the arguments for the test data
545 * @return the stream
546 */
547 Stream<Arguments> streamArguments(TestName name,
548 Function<D, Arguments> mappingFunction) {
549 final Builder<Arguments> b = Stream.builder();
550 final int[] size = {0};
551 data.forEach(d -> {
552 if (d.isDisabled(name)) {
553 return;
554 }
555 size[0]++;
556 b.accept(mappingFunction.apply(d));
557 });
558 Assumptions.assumeTrue(size[0] != 0, () -> "Distribution has no data for " + name);
559 return b.build();
560 }
561
562 /**
563 * Assert the probabilities require a high-precision computation. This verifies
564 * the approximation {@code (1 - p) ~ 1}. The tolerance is set at 2 ULP for the
565 * smallest p-value.
566 *
567 * <p>The test tolerance is verified that it can distinguish values a and b when
568 * separated by an absolute distance of 2 EPSILON (4.44e-16). This is a quick
569 * check to ensure any tests that have overridden the default absolute tolerance
570 * of 0 have correctly configured the absolute tolerance for the high-precision
571 * probabilities (which are expected to have some p-values {@code < 1e-16}).
572 *
573 * @param tolerance Test tolerance
574 * @param probabilities Probabilities
575 */
576 void assertHighPrecision(DoubleTolerance tolerance, double... probabilities) {
577 final double b = 2 * RELATIVE_EPS;
578 Assertions.assertFalse(tolerance.test(0.0, b),
579 () -> "Test tolerance cannot separate small values 0.0 and " + b + ": " + tolerance);
580
581 final long one = Double.doubleToRawLongBits(1.0);
582 final double expected = 2;
583 final int[] ulps = Arrays.stream(probabilities)
584 .mapToInt(p -> (int) (one - Double.doubleToRawLongBits(1.0 - p)))
585 .toArray();
586 final double min = Arrays.stream(ulps).min().orElse(0);
587 Assertions.assertFalse(min < 0, () -> "Invalid probability above 1.0: " + Arrays.toString(probabilities));
588 Assertions.assertTrue(min <= expected,
589 () -> "Not high-precision p-values: (1 - p) ulps from 1 = " + Arrays.toString(ulps));
590 }
591
592 /**
593 * Create arguments to test invalid parameters of the distribution. Each Object[]
594 * will be expected to raise an exception when passed to the {@link #makeDistribution(Object...)}
595 * method.
596 *
597 * @return the arguments
598 */
599 Object[][] testInvalidParameters() {
600 final Object[][] params = makeInvalidParameters();
601 Assumptions.assumeTrue(params != null, "Distribution has no invalid parameters");
602 return params;
603 }
604
605 /**
606 * Create a stream of arguments containing the parameters used to construct a distribution
607 * using {@link #makeDistribution(Object...)}.
608 *
609 * @return the stream
610 */
611 Stream<Arguments> testParameterAccessors() {
612 return data.stream().map(d -> Arguments.of(d.getParameters()));
613 }
614
615 /**
616 * Assert the named method on the class is not modified with the specified modifiers.
617 *
618 * <p>This uses reflection to traverse the object hierarchy to search for the named
619 * method. It can be used to assert that internal methods are not exposed in the API
620 * as public or protected.
621 *
622 * @param cls Class.
623 * @param modifiers Disallowed modifiers.
624 * @param name Name of the method.
625 * @param parameterTypes Array of parameter types for the method.
626 * @see Method#getModifiers()
627 * @see Class#getDeclaredMethod(String, Class...)
628 * @see java.lang.reflect.Modifier
629 */
630 static void assertMethodNotModified(Class<?> cls, int modifiers, String name, Class<?>... parameterTypes) {
631 // getMethod will only find public methods.
632 // using getDeclaredMethod can access private methods but it
633 // only applies to the current class so we traverse the hierarchy.
634 for (Class<?> c = cls; c != null; c = c.getSuperclass()) {
635 try {
636 final Method method = cls.getDeclaredMethod(name, parameterTypes);
637 final int flags = method.getModifiers() & modifiers;
638 Assertions.assertEquals(0, flags,
639 () -> "Method " + name + " has disallowed modifiers: " + Modifier.toString(flags));
640 } catch (NoSuchMethodException ignore) {
641 // The class does not declare the method
642 } catch (SecurityException e) {
643 Assertions.fail("Cannot search for " + name + " using reflection", e);
644 }
645 }
646 }
647
648 //------------------------ Tests -----------------------------
649
650 // Tests are final. It is expected that the test can be modified by overriding
651 // the method used to stream the arguments, for example to use a specific tolerance
652 // for a test in preference to the tolerance defined in the properties file.
653
654 /**
655 * Test invalid parameters will raise an exception when used to construct a distribution.
656 */
657 @ParameterizedTest
658 @MethodSource
659 final void testInvalidParameters(@AggregateWith(value = ArrayAggregator.class) Object[] parameters) {
660 Assertions.assertThrows(DistributionException.class, () -> makeDistribution(parameters));
661 }
662
663 /**
664 * Test the parameter accessors using the reflection API.
665 */
666 @ParameterizedTest
667 @MethodSource
668 final void testParameterAccessors(@AggregateWith(value = ArrayAggregator.class) Object[] parameters) {
669 final String[] names = getParameterNames();
670 Assumptions.assumeTrue(names != null, "No parameter accessors");
671 Assertions.assertEquals(parameters.length, names.length, "Parameter <-> names length mismatch");
672
673 final T dist = makeDistribution(parameters);
674 for (int i = 0; i < names.length; i++) {
675 final String name = names[i];
676 if (name == null) {
677 continue;
678 }
679 try {
680 final Method method = dist.getClass().getMethod("get" + name);
681 final Object o = method.invoke(dist);
682 Assertions.assertEquals(parameters[i], o, () -> "Invalid parameter for " + name);
683 } catch (NoSuchMethodException | SecurityException | IllegalAccessException |
684 IllegalArgumentException | InvocationTargetException e) {
685 Assertions.fail("Failed to find method accessor: " + name, e);
686 }
687 }
688 }
689
690 /**
691 * Aggregate all arguments as a single {@code Object[]} array.
692 *
693 * <p>Note: The default JUnit 5 behaviour for an Argument containing an {@code Object[]} is
694 * to uses each element of the Object array as an indexed argument. This aggregator changes
695 * the behaviour to pass the Object[] as argument index 0.
696 */
697 static class ArrayAggregator implements ArgumentsAggregator {
698 @Override
699 public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context)
700 throws ArgumentsAggregationException {
701 return accessor.toArray();
702 }
703 }
704 }