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