View Javadoc
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 }