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    *      http://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.rng.examples.stress;
18  
19  import org.apache.commons.rng.simple.RandomSource;
20  
21  import picocli.CommandLine.Command;
22  import picocli.CommandLine.Mixin;
23  import picocli.CommandLine.Option;
24  
25  import java.io.IOException;
26  import java.util.ArrayList;
27  import java.util.Arrays;
28  import java.util.Formatter;
29  import java.util.HashSet;
30  import java.util.List;
31  import java.util.NoSuchElementException;
32  import java.util.Scanner;
33  import java.util.concurrent.Callable;
34  
35  /**
36   * Specification for the "list" command.
37   *
38   * <p>This command prints a list of available random generators to the console.</p>
39   */
40  @Command(name = "list",
41           description = "List random generators.")
42  class ListCommand implements Callable<Void> {
43      /** The standard options. */
44      @Mixin
45      private StandardOptions reusableOptions;
46  
47      /** The list format. */
48      @Option(names = {"-f", "--format"},
49              description = {"The list format (default: ${DEFAULT-VALUE}).",
50                             "Valid values: ${COMPLETION-CANDIDATES}."},
51              paramLabel = "<format>")
52      private ListFormat listFormat = ListFormat.STRESS_TEST;
53  
54      /** The provider type. */
55      @Option(names = {"--provider"},
56              description = {"The provider type (default: ${DEFAULT-VALUE}).",
57                             "Valid values: ${COMPLETION-CANDIDATES}."},
58              paramLabel = "<provider>")
59      private ProviderType providerType = ProviderType.ALL;
60  
61      /** The minimum entry in the provider enum. */
62      @Option(names = {"--min"},
63              description = {"The minimum entry in the provider enum (inclusive)."})
64      private int min = 0;
65  
66      /** The maximum entry in the provider enum. */
67      @Option(names = {"--max"},
68              description = {"The maximum entry in the provider enum (inclusive)."})
69      private int max = Integer.MAX_VALUE;
70  
71      /** The prefix for each ID in the template list of random generators. */
72      @Option(names = {"-p", "--prefix"},
73              description = {"The ID prefix.",
74                             "Used for the stress test format."})
75      private String idPrefix = "";
76  
77      /** The number of trials to put in the template list of random generators. */
78      @Option(names = {"-t", "--trials"},
79              description = {"The number of trials for each random generator.",
80                             "Used for the stress test format."})
81      private int trials = 1;
82  
83      /**
84       * The list format.
85       */
86      enum ListFormat {
87          /** The stress test format lists the data required for the stress test. */
88          STRESS_TEST,
89          /** The plain format lists only the name and optional arguments. */
90          PLAIN
91      }
92  
93      /**
94       * The type of provider.
95       */
96      enum ProviderType {
97          /** List all providers. */
98          ALL,
99          /** List int providers. */
100         INT,
101         /** List long providers. */
102         LONG,
103     }
104 
105     /**
106      * Prints a template generators list to stdout.
107      */
108     @Override
109     public Void call() throws Exception {
110         LogUtils.setLogLevel(reusableOptions.logLevel);
111         StressTestDataList list = new StressTestDataList(idPrefix, trials);
112         if (providerType == ProviderType.INT) {
113             list = list.subsetIntSource();
114         } else if (providerType == ProviderType.LONG) {
115             list = list.subsetLongSource();
116         }
117         if (min != 0 || max != Integer.MAX_VALUE) {
118             list = list.subsetRandomSource(min, max);
119         }
120         // Write in one call to the output
121         final StringBuilder sb = new StringBuilder();
122         switch (listFormat) {
123         case PLAIN:
124             writePlainData(sb, list);
125             break;
126         case STRESS_TEST:
127         default:
128             writeStressTestData(sb, list);
129             break;
130         }
131         // CHECKSTYLE: stop regexp
132         System.out.append(sb);
133         // CHECKSTYLE: resume regexp
134         return null;
135     }
136 
137     /**
138      * Write the test data.
139      *
140      * <p>Note: If the {@link Appendable} implements {@link java.io.Closeable Closeable} it
141      * is <strong>not</strong> closed by this method.
142      *
143      * @param appendable The appendable.
144      * @param testData The test data.
145      * @throws IOException Signals that an I/O exception has occurred.
146      */
147     static void writePlainData(Appendable appendable,
148                                Iterable<StressTestData> testData) throws IOException {
149         final String newLine = System.lineSeparator();
150         for (final StressTestData data : testData) {
151             appendable.append(data.getRandomSource().name());
152             if (data.getArgs() != null) {
153                 appendable.append(' ');
154                 appendable.append(Arrays.toString(data.getArgs()));
155             }
156             appendable.append(newLine);
157         }
158     }
159 
160     /**
161      * Write the test data.
162      *
163      * <p>Performs adjustment of the number of trials for each item:
164      *
165      * <ul>
166      *   <li>Any item with trials {@code <= 0} will be written as zero.
167      *   <li>Any item with trials {@code > 0} will be written as the maximum of the value and
168      *       the input parameter {@code numberOfTrials}.
169      * </ul>
170      *
171      * <p>This allows the output to contain a configurable number of trials for the
172      * list of data.
173      *
174      * <p>Note: If the {@link Appendable} implements {@link java.io.Closeable Closeable} it
175      * is <strong>not</strong> closed by this method.
176      *
177      * @param appendable The appendable.
178      * @param testData The test data.
179      * @throws IOException Signals that an I/O exception has occurred.
180      */
181     static void writeStressTestData(Appendable appendable,
182                                     Iterable<StressTestData> testData) throws IOException {
183         // Build the widths for each column
184         int idWidth = 1;
185         int randomSourceWidth = 15;
186         for (final StressTestData data : testData) {
187             idWidth = Math.max(idWidth, data.getId().length());
188             randomSourceWidth = Math.max(randomSourceWidth, data.getRandomSource().name().length());
189         }
190 
191         final String newLine = System.lineSeparator();
192 
193         appendable.append("# Random generators list.").append(newLine);
194         appendable.append("# Any generator with no trials is ignored during testing.").append(newLine);
195         appendable.append("#").append(newLine);
196 
197         String format = String.format("# %%-%ds   %%-%ds   trials   [constructor arguments ...]%%n",
198                 idWidth, randomSourceWidth);
199         // Do not use try-with-resources or the Formatter will close the Appendable
200         // if it implements Closeable. Just flush at the end.
201         @SuppressWarnings("resource")
202         final Formatter formatter = new Formatter(appendable);
203         formatter.format(format, "ID", "RandomSource");
204         format = String.format("%%-%ds   %%-%ds   ", idWidth + 2, randomSourceWidth);
205         for (final StressTestData data : testData) {
206             formatter.format(format, data.getId(), data.getRandomSource().name());
207             if (data.getArgs() == null) {
208                 appendable.append(Integer.toString(data.getTrials()));
209             } else {
210                 formatter.format("%-6d   %s", data.getTrials(), Arrays.toString(data.getArgs()));
211             }
212             appendable.append(newLine);
213         }
214         formatter.flush();
215     }
216 
217     /**
218      * Reads the test data. The {@link Readable} is not closed by this method.
219      *
220      * @param readable The readable.
221      * @return The test data.
222      * @throws IOException Signals that an I/O exception has occurred.
223      * @throws ApplicationException If there was an error parsing the expected format.
224      * @see java.io.Reader#close() Reader.close()
225      */
226     static Iterable<StressTestData> readStressTestData(Readable readable) throws IOException {
227         final List<StressTestData> list = new ArrayList<>();
228 
229         // Validate that all IDs are unique.
230         final HashSet<String> ids = new HashSet<>();
231 
232         // Do not use try-with-resources as the readable must not be closed
233         @SuppressWarnings("resource")
234         final Scanner scanner = new Scanner(readable);
235         try {
236             while (scanner.hasNextLine()) {
237                 // Expected format:
238                 //
239                 //# ID   RandomSource           trials    [constructor arguments ...]
240                 //12     TWO_CMRES              1
241                 //13     TWO_CMRES_SELECT       0         [1, 2]
242                 final String id = scanner.next();
243                 // Skip empty lines and comments
244                 if (id.isEmpty() || id.charAt(0) == '#') {
245                     scanner.nextLine();
246                     continue;
247                 }
248                 if (!ids.add(id)) {
249                     throw new ApplicationException("Non-unique ID in strest test data: " + id);
250                 }
251                 final RandomSource randomSource = RandomSource.valueOf(scanner.next());
252                 final int trials = scanner.nextInt();
253                 // The arguments are the rest of the line
254                 final String arguments = scanner.nextLine().trim();
255                 final Object[] args = parseArguments(randomSource, arguments);
256                 list.add(new StressTestData(id, randomSource, args, trials));
257             }
258         } catch (NoSuchElementException | IllegalArgumentException ex) {
259             if (scanner.ioException() != null) {
260                 throw scanner.ioException();
261             }
262             throw new ApplicationException("Failed to read stress test data", ex);
263         }
264 
265         return list;
266     }
267 
268     /**
269      * Parses the arguments string into an array of {@link Object}.
270      *
271      * <p>Returns {@code null} if the string is empty.
272      *
273      * @param randomSource the random source
274      * @param arguments the arguments
275      * @return the arguments {@code Object[]}
276      */
277     static Object[] parseArguments(RandomSource randomSource,
278                                    String arguments) {
279         // Empty string is null arguments
280         if (arguments.isEmpty()) {
281             return null;
282         }
283 
284         // Expected format:
285         // [data1, data2, ...]
286         final int len = arguments.length();
287         if (len < 2 || arguments.charAt(0) != '[' || arguments.charAt(len - 1) != ']') {
288             throw new ApplicationException("RandomSource arguments should be an [array]: " + arguments);
289         }
290 
291         // Split the text between the [] on commas
292         final String[] tokens = arguments.substring(1, len - 1).split(", *");
293         final ArrayList<Object> args = new ArrayList<>();
294         for (final String token : tokens) {
295             args.add(RNGUtils.parseArgument(token));
296         }
297         return args.toArray();
298     }
299 }